//! L7 integration tests for [`FsTemplateStore`] against a real temp directory and
//! a real [`LocalFileSystem`]: md + `index.json` round-trip, version persistence,
//! upsert, delete, tolerant reads, and the on-disk layout (`templates/md/<id>.md`).

use std::path::PathBuf;
use std::sync::Arc;

use domain::ids::{ProfileId, TemplateId};
use domain::markdown::MarkdownDoc;
use domain::ports::{FileSystem, RemotePath, StoreError, TemplateStore};
use domain::template::AgentTemplate;
use infrastructure::{FsTemplateStore, LocalFileSystem};
use uuid::Uuid;

struct TempDir(PathBuf);
impl TempDir {
    fn new() -> Self {
        let p = std::env::temp_dir().join(format!("idea-l7-tpl-{}", Uuid::new_v4()));
        std::fs::create_dir_all(&p).unwrap();
        Self(p)
    }
    fn app_data_dir(&self) -> String {
        self.0.to_string_lossy().into_owned()
    }
    fn child(&self, rel: &str) -> RemotePath {
        RemotePath::new(self.0.join(rel).to_string_lossy().into_owned())
    }
}
impl Drop for TempDir {
    fn drop(&mut self) {
        let _ = std::fs::remove_dir_all(&self.0);
    }
}

fn store(tmp: &TempDir) -> FsTemplateStore {
    let fs: Arc<dyn FileSystem> = Arc::new(LocalFileSystem::new());
    FsTemplateStore::new(fs, tmp.app_data_dir())
}

fn tid(n: u128) -> TemplateId {
    TemplateId::from_uuid(Uuid::from_u128(n))
}
fn pid(n: u128) -> ProfileId {
    ProfileId::from_uuid(Uuid::from_u128(n))
}

fn template(id: TemplateId, name: &str, content: &str) -> AgentTemplate {
    AgentTemplate::new(id, name, MarkdownDoc::new(content), pid(1)).unwrap()
}

#[tokio::test]
async fn missing_index_lists_empty() {
    let tmp = TempDir::new();
    assert!(store(&tmp).list().await.unwrap().is_empty());
}

#[tokio::test]
async fn save_then_get_and_list_roundtrip() {
    let tmp = TempDir::new();
    let store = store(&tmp);

    let t = template(tid(1), "Backend", "# Backend template");
    store.save(&t).await.unwrap();

    assert_eq!(store.get(tid(1)).await.unwrap(), t);
    assert_eq!(store.list().await.unwrap(), vec![t.clone()]);

    // The Markdown actually landed at templates/md/<id>.md.
    let fs = LocalFileSystem::new();
    let bytes = fs
        .child_read(&tmp, &format!("templates/md/{}.md", tid(1)))
        .await;
    assert_eq!(String::from_utf8(bytes).unwrap(), t.content_md.as_str());
}

#[tokio::test]
async fn save_upserts_and_persists_bumped_version() {
    let tmp = TempDir::new();
    let store = store(&tmp);

    let t0 = template(tid(1), "Backend", "v1");
    store.save(&t0).await.unwrap();
    let t1 = t0.with_updated_content(MarkdownDoc::new("v2"));
    store.save(&t1).await.unwrap();

    let back = store.get(tid(1)).await.unwrap();
    assert_eq!(back.version.get(), 2, "bumped version persisted");
    assert_eq!(back.content_md.as_str(), "v2");
    assert_eq!(store.list().await.unwrap().len(), 1, "upsert, not append");
}

#[tokio::test]
async fn get_unknown_is_not_found() {
    let tmp = TempDir::new();
    assert!(matches!(
        store(&tmp).get(tid(404)).await.unwrap_err(),
        StoreError::NotFound
    ));
}

#[tokio::test]
async fn delete_removes_from_index() {
    let tmp = TempDir::new();
    let store = store(&tmp);
    store.save(&template(tid(1), "T", "x")).await.unwrap();

    store.delete(tid(1)).await.unwrap();
    assert!(store.list().await.unwrap().is_empty());
    assert!(matches!(
        store.delete(tid(1)).await.unwrap_err(),
        StoreError::NotFound
    ));
}

#[tokio::test]
async fn index_is_camelcase_with_content_hash() {
    let tmp = TempDir::new();
    let store = store(&tmp);
    store
        .save(&template(tid(1), "Backend", "hello"))
        .await
        .unwrap();

    let fs = LocalFileSystem::new();
    let bytes = fs.read(&tmp.child("templates/index.json")).await.unwrap();
    let json: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
    let entry = &json.get("templates").unwrap().as_array().unwrap()[0];
    assert_eq!(entry.get("name").and_then(|v| v.as_str()), Some("Backend"));
    assert!(
        entry.get("contentHash").is_some(),
        "camelCase contentHash present"
    );
    assert!(entry.get("defaultProfileId").is_some());
    assert!(entry.get("content_hash").is_none(), "no snake_case leak");
}

/// Tiny read helper so the md-path assertion stays readable.
trait ChildRead {
    async fn child_read(&self, tmp: &TempDir, rel: &str) -> Vec<u8>;
}
impl ChildRead for LocalFileSystem {
    async fn child_read(&self, tmp: &TempDir, rel: &str) -> Vec<u8> {
        self.read(&tmp.child(rel)).await.unwrap()
    }
}
