//! R0c tests for [`ReconcileLayouts`].
//!
//! At project open, a persisted `layouts.json` may already hold several leaves
//! pinning the **same** agent id. The use case de-duplicates them through the
//! pure domain op [`domain::LayoutTree::reconcile_duplicate_agents`] and
//! persists the doc **only** when something changed (idempotent no-op
//! otherwise).
//!
//! Harness mirrors `tests/snapshot_running_agents.rs` (its twin use case): the
//! same in-memory `FakeFs` / `FakeStore`. The fake FS additionally counts the
//! number of `write` **calls** so test 7 can prove that a no-op performs zero
//! writes (file-count alone cannot, since `layouts.json` already exists).

use std::collections::{HashMap, HashSet};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};

use async_trait::async_trait;
use domain::layout::Workspace;
use domain::ports::{DirEntry, FileSystem, FsError, ProjectStore, RemotePath, StoreError};
use domain::{
    AgentId, Direction, LayoutId, LayoutNode, LayoutTree, LeafCell, NodeId, Project, ProjectId,
    ProjectPath, RemoteRef, SplitContainer, WeightedChild,
};
use uuid::Uuid;

use application::{ReconcileLayouts, ReconcileLayoutsInput};

// ---------------------------------------------------------------------------
// Fakes
// ---------------------------------------------------------------------------

#[derive(Default)]
struct FakeFsInner {
    files: HashMap<String, Vec<u8>>,
    dirs: HashSet<String>,
}

#[derive(Default, Clone)]
struct FakeFs {
    inner: Arc<Mutex<FakeFsInner>>,
    /// Count of `write` **calls** (not files). Lets a no-op be asserted even
    /// though the target file already exists.
    write_calls: Arc<AtomicUsize>,
}

impl FakeFs {
    fn read_file(&self, path: &str) -> Option<Vec<u8>> {
        self.inner.lock().unwrap().files.get(path).cloned()
    }
    fn put(&self, path: &str, data: &[u8]) {
        self.inner
            .lock()
            .unwrap()
            .files
            .insert(path.to_owned(), data.to_vec());
    }
    fn write_calls(&self) -> usize {
        self.write_calls.load(Ordering::SeqCst)
    }
}

#[async_trait]
impl FileSystem for FakeFs {
    async fn read(&self, path: &RemotePath) -> Result<Vec<u8>, FsError> {
        self.inner
            .lock()
            .unwrap()
            .files
            .get(path.as_str())
            .cloned()
            .ok_or_else(|| FsError::NotFound(path.as_str().to_owned()))
    }
    async fn write(&self, path: &RemotePath, data: &[u8]) -> Result<(), FsError> {
        self.write_calls.fetch_add(1, Ordering::SeqCst);
        self.inner
            .lock()
            .unwrap()
            .files
            .insert(path.as_str().to_owned(), data.to_vec());
        Ok(())
    }
    async fn exists(&self, path: &RemotePath) -> Result<bool, FsError> {
        let inner = self.inner.lock().unwrap();
        Ok(inner.files.contains_key(path.as_str()) || inner.dirs.contains(path.as_str()))
    }
    async fn create_dir_all(&self, path: &RemotePath) -> Result<(), FsError> {
        self.inner
            .lock()
            .unwrap()
            .dirs
            .insert(path.as_str().to_owned());
        Ok(())
    }
    async fn list(&self, _path: &RemotePath) -> Result<Vec<DirEntry>, FsError> {
        Ok(Vec::new())
    }
    async fn symlink(&self, _src: &RemotePath, _dst: &RemotePath) -> Result<(), FsError> {
        Ok(())
    }
}

#[derive(Default)]
struct FakeStoreInner {
    projects: Vec<Project>,
}

#[derive(Default, Clone)]
struct FakeStore(Arc<Mutex<FakeStoreInner>>);

#[async_trait]
impl ProjectStore for FakeStore {
    async fn list_projects(&self) -> Result<Vec<Project>, StoreError> {
        Ok(self.0.lock().unwrap().projects.clone())
    }
    async fn load_project(&self, id: ProjectId) -> Result<Project, StoreError> {
        self.0
            .lock()
            .unwrap()
            .projects
            .iter()
            .find(|p| p.id == id)
            .cloned()
            .ok_or(StoreError::NotFound)
    }
    async fn save_project(&self, project: &Project) -> Result<(), StoreError> {
        self.0.lock().unwrap().projects.push(project.clone());
        Ok(())
    }
    async fn save_workspace(&self, _w: &Workspace) -> Result<(), StoreError> {
        Ok(())
    }
    async fn load_workspace(&self) -> Result<Workspace, StoreError> {
        Ok(Workspace::default())
    }
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

const ROOT: &str = "/home/me/proj";
const LAYOUTS_PATH: &str = "/home/me/proj/.ideai/layouts.json";

fn pid(n: u128) -> ProjectId {
    ProjectId::from_uuid(Uuid::from_u128(n))
}
fn nid(n: u128) -> NodeId {
    NodeId::from_uuid(Uuid::from_u128(n))
}
fn aid(n: u128) -> AgentId {
    AgentId::from_uuid(Uuid::from_u128(n))
}
fn lid(n: u128) -> LayoutId {
    LayoutId::from_uuid(Uuid::from_u128(n))
}

fn agent_leaf(node: NodeId, agent: Option<AgentId>, conv: Option<&str>, running: bool) -> LeafCell {
    LeafCell {
        id: node,
        session: None,
        agent,
        conversation_id: conv.map(str::to_string),
        engine_session_id: None,
        agent_was_running: running,
    }
}

/// Two leaves of the same agent, both flagged running, under a Row split.
fn duplicate_tree(agent: AgentId, a: NodeId, b: NodeId) -> LayoutTree {
    LayoutTree::new(LayoutNode::Split(SplitContainer {
        id: nid(1),
        direction: Direction::Row,
        children: vec![
            WeightedChild {
                node: LayoutNode::Leaf(agent_leaf(a, Some(agent), Some("c-a"), true)),
                weight: 1.0,
            },
            WeightedChild {
                node: LayoutNode::Leaf(agent_leaf(b, Some(agent), Some("c-b"), true)),
                weight: 1.0,
            },
        ],
    }))
}

async fn register_project(store: &FakeStore, id: ProjectId) {
    let project = Project::new(
        id,
        "Demo",
        ProjectPath::new(ROOT).unwrap(),
        RemoteRef::Local,
        0,
    )
    .unwrap();
    store.save_project(&project).await.unwrap();
}

fn seed_layouts(fs: &FakeFs, id: LayoutId, tree: &LayoutTree) {
    let doc = serde_json::json!({
        "version": 1,
        "activeId": id.to_string(),
        "layouts": [ { "id": id.to_string(), "name": "Default", "tree": tree } ],
    });
    fs.put(LAYOUTS_PATH, &serde_json::to_vec(&doc).unwrap());
}

fn doc_json(fs: &FakeFs) -> serde_json::Value {
    serde_json::from_slice(&fs.read_file(LAYOUTS_PATH).expect("layouts.json present")).unwrap()
}

/// `agent_was_running` for the leaf `node` in the active persisted layout.
fn was_running(fs: &FakeFs, node: NodeId) -> Option<bool> {
    let doc = doc_json(fs);
    let tree: LayoutTree =
        serde_json::from_value(doc["layouts"][0]["tree"].clone()).expect("tree parseable");
    fn find(node: &LayoutNode, target: NodeId) -> Option<bool> {
        match node {
            LayoutNode::Leaf(l) if l.id == target => Some(l.agent_was_running),
            LayoutNode::Leaf(_) => None,
            LayoutNode::Split(s) => s.children.iter().find_map(|c| find(&c.node, target)),
            LayoutNode::Grid(g) => g.cells.iter().find_map(|c| find(&c.node, target)),
        }
    }
    find(&tree.root, node)
}

fn make_use_case(store: &FakeStore, fs: &FakeFs) -> ReconcileLayouts {
    ReconcileLayouts::new(
        Arc::new(store.clone()) as Arc<dyn ProjectStore>,
        Arc::new(fs.clone()) as Arc<dyn FileSystem>,
    )
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

/// 6. A persisted layout with a duplicate agent ⇒ `changed == true` and the
/// persisted version keeps a single "was running" leaf for the agent.
#[tokio::test]
async fn reconcile_persists_and_reports_changed_on_duplicate() {
    let store = FakeStore::default();
    let fs = FakeFs::default();
    register_project(&store, pid(1)).await;

    let a = nid(10);
    let b = nid(11);
    let agent = aid(100);
    seed_layouts(&fs, lid(1), &duplicate_tree(agent, a, b));

    let uc = make_use_case(&store, &fs);
    let out = uc
        .execute(ReconcileLayoutsInput { project_id: pid(1) })
        .await
        .unwrap();

    assert_eq!(out.changed, true, "duplicate must be reconciled");

    // Exactly one of the two leaves is still flagged running in the persisted doc.
    let ra = was_running(&fs, a).expect("leaf a");
    let rb = was_running(&fs, b).expect("leaf b");
    assert_eq!(
        [ra, rb].iter().filter(|x| **x).count(),
        1,
        "only one host stays running after reconciliation"
    );
    // First in pre-order is the host (both carried a signal).
    assert_eq!(ra, true);
    assert_eq!(rb, false);
}

/// 7. No-op: a second `execute` on an already-reconciled project ⇒
/// `changed == false` and **zero** writes occur.
#[tokio::test]
async fn reconcile_second_pass_is_noop_no_write() {
    let store = FakeStore::default();
    let fs = FakeFs::default();
    register_project(&store, pid(1)).await;

    let a = nid(10);
    let b = nid(11);
    let agent = aid(100);
    seed_layouts(&fs, lid(1), &duplicate_tree(agent, a, b));

    let uc = make_use_case(&store, &fs);

    // First pass reconciles + writes once.
    let first = uc
        .execute(ReconcileLayoutsInput { project_id: pid(1) })
        .await
        .unwrap();
    assert_eq!(first.changed, true);
    assert_eq!(fs.write_calls(), 1, "first pass persists once");

    // Second pass must be a strict no-op.
    let writes_before = fs.write_calls();
    let second = uc
        .execute(ReconcileLayoutsInput { project_id: pid(1) })
        .await
        .unwrap();
    assert_eq!(second.changed, false, "already reconciled → no change");
    assert_eq!(
        fs.write_calls(),
        writes_before,
        "no-op must not write anything"
    );
}

/// 8. A layout WITHOUT duplicates ⇒ `changed == false`, unchanged, no write.
#[tokio::test]
async fn reconcile_no_duplicate_is_noop() {
    let store = FakeStore::default();
    let fs = FakeFs::default();
    register_project(&store, pid(1)).await;

    let a = nid(10);
    let b = nid(11);
    // Distinct agents — no duplicate.
    let tree = LayoutTree::new(LayoutNode::Split(SplitContainer {
        id: nid(1),
        direction: Direction::Row,
        children: vec![
            WeightedChild {
                node: LayoutNode::Leaf(agent_leaf(a, Some(aid(100)), Some("c-a"), true)),
                weight: 1.0,
            },
            WeightedChild {
                node: LayoutNode::Leaf(agent_leaf(b, Some(aid(101)), None, false)),
                weight: 1.0,
            },
        ],
    }));
    seed_layouts(&fs, lid(1), &tree);
    let before = doc_json(&fs);
    let writes_before = fs.write_calls();

    let uc = make_use_case(&store, &fs);
    let out = uc
        .execute(ReconcileLayoutsInput { project_id: pid(1) })
        .await
        .unwrap();

    assert_eq!(out.changed, false, "no duplicate → no change");
    assert_eq!(fs.write_calls(), writes_before, "no write on no-op");
    assert_eq!(doc_json(&fs), before, "persisted doc unchanged");
    assert_eq!(was_running(&fs, a), Some(true));
}
