//! L12 integration tests for [`FsSkillStore`] against a real temp directory and a
//! real [`LocalFileSystem`]: md + `index.json` round-trip in **both** scopes,
//! scope isolation (a `Project` skill never appears under `Global` and vice
//! versa), upsert, delete, tolerant reads, and the on-disk layout.

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

use domain::ids::SkillId;
use domain::markdown::MarkdownDoc;
use domain::ports::{FileSystem, RemotePath, SkillStore, StoreError};
use domain::project::ProjectPath;
use domain::skill::{Skill, SkillScope};
use infrastructure::{FsSkillStore, LocalFileSystem};
use uuid::Uuid;

/// A unique scratch directory under the OS temp dir, cleaned up on drop. It plays
/// **both** roles: its own path is the global app-data dir, and a `project/`
/// child is the project root (so the two scopes resolve to disjoint subtrees).
struct TempDir(PathBuf);
impl TempDir {
    fn new() -> Self {
        let p = std::env::temp_dir().join(format!("idea-l12-skill-{}", 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 project_root(&self) -> ProjectPath {
        ProjectPath::new(self.0.join("project").to_string_lossy().into_owned()).unwrap()
    }
    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) -> FsSkillStore {
    let fs: Arc<dyn FileSystem> = Arc::new(LocalFileSystem::new());
    FsSkillStore::new(fs, tmp.app_data_dir())
}

fn sid(n: u128) -> SkillId {
    SkillId::from_uuid(Uuid::from_u128(n))
}

fn skill(id: SkillId, name: &str, content: &str, scope: SkillScope) -> Skill {
    Skill::new(id, name, MarkdownDoc::new(content), scope).unwrap()
}

#[tokio::test]
async fn missing_index_lists_empty_in_both_scopes() {
    let tmp = TempDir::new();
    let s = store(&tmp);
    let root = tmp.project_root();
    assert!(s.list(SkillScope::Global, &root).await.unwrap().is_empty());
    assert!(s.list(SkillScope::Project, &root).await.unwrap().is_empty());
}

#[tokio::test]
async fn global_roundtrip_save_get_list() {
    let tmp = TempDir::new();
    let s = store(&tmp);
    let root = tmp.project_root();

    let sk = skill(
        sid(1),
        "refactor",
        "# Refactor workflow",
        SkillScope::Global,
    );
    s.save(&sk, &root).await.unwrap();

    assert_eq!(s.get(SkillScope::Global, &root, sid(1)).await.unwrap(), sk);
    assert_eq!(
        s.list(SkillScope::Global, &root).await.unwrap(),
        vec![sk.clone()]
    );

    // The Markdown landed under the global skills dir.
    let fs = LocalFileSystem::new();
    let bytes = fs
        .read(&tmp.child(&format!("skills/md/{}.md", sid(1))))
        .await
        .unwrap();
    assert_eq!(String::from_utf8(bytes).unwrap(), sk.content_md.as_str());
}

#[tokio::test]
async fn project_roundtrip_lands_under_ideai() {
    let tmp = TempDir::new();
    let s = store(&tmp);
    let root = tmp.project_root();

    let sk = skill(sid(1), "review", "# Review workflow", SkillScope::Project);
    s.save(&sk, &root).await.unwrap();

    assert_eq!(s.get(SkillScope::Project, &root, sid(1)).await.unwrap(), sk);

    let fs = LocalFileSystem::new();
    let bytes = fs
        .read(&tmp.child(&format!("project/.ideai/skills/md/{}.md", sid(1))))
        .await
        .unwrap();
    assert_eq!(String::from_utf8(bytes).unwrap(), sk.content_md.as_str());
}

#[tokio::test]
async fn scopes_are_isolated() {
    let tmp = TempDir::new();
    let s = store(&tmp);
    let root = tmp.project_root();

    s.save(
        &skill(sid(1), "g", "global body", SkillScope::Global),
        &root,
    )
    .await
    .unwrap();
    s.save(
        &skill(sid(2), "p", "project body", SkillScope::Project),
        &root,
    )
    .await
    .unwrap();

    // A global skill never surfaces in the project scope and vice-versa.
    let globals = s.list(SkillScope::Global, &root).await.unwrap();
    let projects = s.list(SkillScope::Project, &root).await.unwrap();
    assert_eq!(globals.len(), 1);
    assert_eq!(projects.len(), 1);
    assert_eq!(globals[0].id, sid(1));
    assert_eq!(projects[0].id, sid(2));

    assert!(matches!(
        s.get(SkillScope::Global, &root, sid(2)).await.unwrap_err(),
        StoreError::NotFound
    ));
    assert!(matches!(
        s.get(SkillScope::Project, &root, sid(1)).await.unwrap_err(),
        StoreError::NotFound
    ));
}

#[tokio::test]
async fn save_upserts_content() {
    let tmp = TempDir::new();
    let s = store(&tmp);
    let root = tmp.project_root();

    s.save(&skill(sid(1), "k", "v1", SkillScope::Global), &root)
        .await
        .unwrap();
    s.save(&skill(sid(1), "k", "v2", SkillScope::Global), &root)
        .await
        .unwrap();

    assert_eq!(
        s.get(SkillScope::Global, &root, sid(1))
            .await
            .unwrap()
            .content_md
            .as_str(),
        "v2"
    );
    assert_eq!(
        s.list(SkillScope::Global, &root).await.unwrap().len(),
        1,
        "upsert, not append"
    );
}

#[tokio::test]
async fn delete_removes_from_index_and_is_not_found_twice() {
    let tmp = TempDir::new();
    let s = store(&tmp);
    let root = tmp.project_root();
    s.save(&skill(sid(1), "k", "x", SkillScope::Project), &root)
        .await
        .unwrap();

    s.delete(SkillScope::Project, &root, sid(1)).await.unwrap();
    assert!(s.list(SkillScope::Project, &root).await.unwrap().is_empty());
    assert!(matches!(
        s.delete(SkillScope::Project, &root, sid(1))
            .await
            .unwrap_err(),
        StoreError::NotFound
    ));
}

#[tokio::test]
async fn index_is_camelcase_with_content_hash() {
    let tmp = TempDir::new();
    let s = store(&tmp);
    let root = tmp.project_root();
    s.save(
        &skill(sid(1), "refactor", "hello", SkillScope::Global),
        &root,
    )
    .await
    .unwrap();

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