//! L2 integration tests for [`FsProjectStore`] against a real temp directory,
//! using a real [`LocalFileSystem`] so the full persistence path (JSON layout,
//! tolerant reads, upsert) is exercised end-to-end.

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

use domain::ids::ProjectId;
use domain::layout::Workspace;
use domain::ports::{FileSystem, ProjectStore, RemotePath};
use domain::project::{Project, ProjectPath};
use domain::remote::RemoteRef;
use infrastructure::{FsProjectStore, LocalFileSystem};
use uuid::Uuid;

/// A unique scratch directory under the OS temp dir, cleaned up on drop.
struct TempDir(PathBuf);
impl TempDir {
    fn new() -> Self {
        let p = std::env::temp_dir().join(format!("idea-l2-store-{}", Uuid::new_v4()));
        std::fs::create_dir_all(&p).unwrap();
        Self(p)
    }
    /// The app-data dir as a plain string, as the composition root would pass it.
    fn app_data_dir(&self) -> String {
        self.0.to_string_lossy().into_owned()
    }
    fn child(&self, name: &str) -> RemotePath {
        RemotePath::new(self.0.join(name).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) -> FsProjectStore {
    let fs: Arc<dyn FileSystem> = Arc::new(LocalFileSystem::new());
    FsProjectStore::new(fs, tmp.app_data_dir())
}

fn sample_project(id: ProjectId, name: &str, root: &str) -> Project {
    Project::new(
        id,
        name,
        ProjectPath::new(root).unwrap(),
        RemoteRef::local(),
        1_700_000_000_000,
    )
    .unwrap()
}

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

    let p = sample_project(ProjectId::new_random(), "alpha", "/home/me/alpha");
    store.save_project(&p).await.unwrap();

    let listed = store.list_projects().await.unwrap();
    assert_eq!(listed, vec![p]);
}

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

    let id = ProjectId::new_random();
    let first = sample_project(id, "before", "/home/me/proj");
    store.save_project(&first).await.unwrap();

    // Same id, changed fields: must update in place, not append.
    let updated = sample_project(id, "after", "/home/me/proj-renamed");
    store.save_project(&updated).await.unwrap();

    let listed = store.list_projects().await.unwrap();
    assert_eq!(listed.len(), 1, "upsert must not duplicate by id");
    assert_eq!(listed[0], updated);
    assert_eq!(listed[0].name, "after");
}

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

    // No projects.json written yet: tolerant read returns an empty list.
    let listed = store.list_projects().await.unwrap();
    assert!(listed.is_empty());
}

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

    // Missing workspace returns the default.
    let loaded = store.load_workspace().await.unwrap();
    assert_eq!(loaded, Workspace::default());

    let ws = Workspace::default();
    store.save_workspace(&ws).await.unwrap();
    let back = store.load_workspace().await.unwrap();
    assert_eq!(back, ws);
}

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

    let p = sample_project(ProjectId::new_random(), "jsoncheck", "/srv/app");
    store.save_project(&p).await.unwrap();

    // Read the raw bytes the store wrote and assert the camelCase shape.
    let fs = LocalFileSystem::new();
    let bytes = fs.read(&tmp.child("projects.json")).await.unwrap();
    let json: serde_json::Value = serde_json::from_slice(&bytes).unwrap();

    assert!(json.get("version").is_some(), "top-level `version` present");
    let projects = json
        .get("projects")
        .and_then(|v| v.as_array())
        .expect("top-level `projects` array");
    assert_eq!(projects.len(), 1);

    let entry = &projects[0];
    // camelCase serialization of `created_at`.
    assert!(
        entry.get("createdAt").is_some(),
        "project uses camelCase `createdAt`, got {entry}"
    );
    assert!(entry.get("created_at").is_none(), "no snake_case leak");
    assert_eq!(
        entry.get("name").and_then(|v| v.as_str()),
        Some("jsoncheck")
    );
    assert_eq!(entry.get("root").and_then(|v| v.as_str()), Some("/srv/app"));
}
