//! JSON round-trip (serde) of persisted domain types, plus camelCase / tagging
//! checks (ARCHITECTURE §7.3, §9).

mod helpers;

use domain::{
    Agent, AgentManifest, AgentOrigin, AgentProfile, AgentTemplate, ContextInjection,
    CustomPluginLayoutCell, Direction, LayoutNode, LayoutTree, LeafCell, ManifestEntry,
    MarkdownDoc, PluginId, PluginLayoutType, Project, ProjectPath, RemoteRef, SessionStrategy,
    Skill, SkillId, SkillRef, SkillScope, SplitContainer, SshAuth, TemplateVersion, WeightedChild,
};
use helpers::{node, session};
use uuid::Uuid;

fn pid(n: u128) -> domain::ProjectId {
    domain::ProjectId::from_uuid(Uuid::from_u128(n))
}
fn profid(n: u128) -> domain::ProfileId {
    domain::ProfileId::from_uuid(Uuid::from_u128(n))
}
fn tid(n: u128) -> domain::TemplateId {
    domain::TemplateId::from_uuid(Uuid::from_u128(n))
}
fn aid(n: u128) -> domain::AgentId {
    domain::AgentId::from_uuid(Uuid::from_u128(n))
}

fn roundtrip<T>(value: &T) -> T
where
    T: serde::Serialize + serde::de::DeserializeOwned + PartialEq + std::fmt::Debug,
{
    let json = serde_json::to_string(value).expect("serialize");
    serde_json::from_str(&json).expect("deserialize")
}

// ---------------------------------------------------------------------------
// Project
// ---------------------------------------------------------------------------

#[test]
fn project_roundtrip() {
    let p = Project::new(
        pid(1),
        "demo",
        ProjectPath::new("/srv/demo").unwrap(),
        RemoteRef::ssh("h", 22, "u", SshAuth::Agent, "/srv").unwrap(),
        123,
    )
    .unwrap();
    assert_eq!(roundtrip(&p), p);
}

#[test]
fn project_uses_camel_case_and_tagged_remote() {
    let p = Project::new(
        pid(1),
        "demo",
        ProjectPath::new("/srv/demo").unwrap(),
        RemoteRef::local(),
        123,
    )
    .unwrap();
    let json = serde_json::to_string(&p).unwrap();
    assert!(json.contains("\"createdAt\":123"), "json was {json}");
    // RemoteRef tagged with `kind`, camelCased "local".
    assert!(json.contains("\"kind\":\"local\""), "json was {json}");
}

// ---------------------------------------------------------------------------
// RemoteRef variants
// ---------------------------------------------------------------------------

#[test]
fn remote_ssh_roundtrip_and_tags() {
    let r = RemoteRef::ssh(
        "host",
        2222,
        "me",
        SshAuth::Key { path: "/k".into() },
        "/srv",
    )
    .unwrap();
    assert_eq!(roundtrip(&r), r);
    let json = serde_json::to_string(&r).unwrap();
    assert!(json.contains("\"kind\":\"ssh\""), "json was {json}");
    // SshAuth tagged with `type`.
    assert!(json.contains("\"type\":\"key\""), "json was {json}");
    // Enum-variant fields must be camelCased on the wire (ARCHITECTURE §9).
    assert!(json.contains("\"remoteRoot\""), "json was {json}");
    assert!(!json.contains("\"remote_root\""), "json was {json}");
}

#[test]
fn remote_wsl_roundtrip() {
    let r = RemoteRef::wsl("Ubuntu").unwrap();
    assert_eq!(roundtrip(&r), r);
}

// ---------------------------------------------------------------------------
// AgentProfile + ContextInjection (tagged with `strategy`)
// ---------------------------------------------------------------------------

#[test]
fn profile_roundtrip_all_injection_variants() {
    for ci in [
        ContextInjection::convention_file("CLAUDE.md").unwrap(),
        ContextInjection::flag("-f {path}").unwrap(),
        ContextInjection::stdin(),
        ContextInjection::env("CTX").unwrap(),
    ] {
        let p = AgentProfile::new(
            profid(1),
            "Name",
            "claude",
            vec!["a".into(), "b".into()],
            ci,
            Some("claude --version".into()),
            "{projectRoot}",
            None,
        )
        .unwrap();
        assert_eq!(roundtrip(&p), p);
    }
}

#[test]
fn context_injection_strategy_tag_is_camel_case() {
    let json =
        serde_json::to_string(&ContextInjection::convention_file("CLAUDE.md").unwrap()).unwrap();
    assert!(
        json.contains("\"strategy\":\"conventionFile\""),
        "json was {json}"
    );
    let json = serde_json::to_string(&ContextInjection::stdin()).unwrap();
    assert!(json.contains("\"strategy\":\"stdin\""), "json was {json}");
}

#[test]
fn profile_cwd_template_is_camel_case() {
    let p = AgentProfile::new(
        profid(1),
        "n",
        "c",
        vec![],
        ContextInjection::stdin(),
        None,
        "{projectRoot}",
        None,
    )
    .unwrap();
    let json = serde_json::to_string(&p).unwrap();
    assert!(json.contains("\"cwdTemplate\""), "json was {json}");
}

#[test]
fn profile_with_session_roundtrips_and_uses_camel_case() {
    let session = SessionStrategy::new(Some("--session-id".into()), "--resume").unwrap();
    let p = AgentProfile::new(
        profid(1),
        "Claude Code",
        "claude",
        vec![],
        ContextInjection::stdin(),
        None,
        "{agentRunDir}",
        Some(session),
    )
    .unwrap();
    let json = serde_json::to_string(&p).unwrap();
    assert!(
        json.contains("\"session\":{\"assignFlag\":\"--session-id\",\"resumeFlag\":\"--resume\"}"),
        "json was {json}"
    );
    assert_eq!(roundtrip(&p), p);
}

#[test]
fn profile_without_session_omits_the_field() {
    let p = AgentProfile::new(
        profid(1),
        "n",
        "c",
        vec![],
        ContextInjection::stdin(),
        None,
        "{projectRoot}",
        None,
    )
    .unwrap();
    let json = serde_json::to_string(&p).unwrap();
    assert!(!json.contains("session"), "json was {json}");
    assert_eq!(roundtrip(&p), p);
}

#[test]
fn legacy_profile_without_session_deserialises_to_none() {
    // A `profiles.json` produced before the `session` field existed.
    let json = r#"{
        "id": "00000000-0000-0000-0000-000000000001",
        "name": "Claude Code",
        "command": "claude",
        "args": [],
        "contextInjection": { "strategy": "stdin" },
        "detect": null,
        "cwdTemplate": "{agentRunDir}"
    }"#;
    let p: AgentProfile = serde_json::from_str(json).expect("legacy profile must deserialise");
    assert_eq!(p.session, None);
}

#[test]
fn session_assign_flag_omitted_when_none() {
    let session = SessionStrategy::new(None, "--continue").unwrap();
    let json = serde_json::to_string(&session).unwrap();
    assert!(!json.contains("assignFlag"), "json was {json}");
    assert!(
        json.contains("\"resumeFlag\":\"--continue\""),
        "json was {json}"
    );
    assert_eq!(roundtrip(&session), session);
}

// ---------------------------------------------------------------------------
// AgentTemplate
// ---------------------------------------------------------------------------

#[test]
fn template_roundtrip() {
    let t = AgentTemplate::new(tid(1), "T", MarkdownDoc::new("# hi"), profid(2))
        .unwrap()
        .with_updated_content(MarkdownDoc::new("# bye"));
    assert_eq!(roundtrip(&t), t);
    let json = serde_json::to_string(&t).unwrap();
    assert!(json.contains("\"contentMd\""), "json was {json}");
    assert!(json.contains("\"defaultProfileId\""), "json was {json}");
}

// ---------------------------------------------------------------------------
// Agent + manifest
// ---------------------------------------------------------------------------

#[test]
fn agent_roundtrip_from_template() {
    let a = Agent::new(
        aid(1),
        "Backend",
        "agents/backend.md",
        profid(2),
        AgentOrigin::FromTemplate {
            template_id: tid(3),
            synced_template_version: TemplateVersion(4),
        },
        true,
    )
    .unwrap();
    assert_eq!(roundtrip(&a), a);
    let json = serde_json::to_string(&a).unwrap();
    assert!(json.contains("\"contextPath\""), "json was {json}");
    // AgentOrigin tagged with `type`, camelCased.
    assert!(
        json.contains("\"type\":\"fromTemplate\""),
        "json was {json}"
    );
    // Inner fields must be camelCased per ARCHITECTURE §9.1:
    // { "type":"fromTemplate", "templateId":"...", "syncedTemplateVersion":N }.
    assert!(json.contains("\"templateId\""), "json was {json}");
    assert!(
        json.contains("\"syncedTemplateVersion\":4"),
        "json was {json}"
    );
    assert!(!json.contains("\"template_id\""), "json was {json}");
    assert!(
        !json.contains("\"synced_template_version\""),
        "json was {json}"
    );
    assert!(!json.contains("\"synced_version\""), "json was {json}");
}

// ---------------------------------------------------------------------------
// SessionKind (tagged enum: `type`, camelCased variant fields)
// ---------------------------------------------------------------------------

#[test]
fn session_kind_agent_roundtrip_and_camel_case() {
    use domain::SessionKind;
    let k = SessionKind::Agent { agent_id: aid(7) };
    assert_eq!(roundtrip(&k), k);
    let json = serde_json::to_string(&k).unwrap();
    assert!(json.contains("\"type\":\"agent\""), "json was {json}");
    assert!(json.contains("\"agentId\""), "json was {json}");
    assert!(!json.contains("\"agent_id\""), "json was {json}");

    // Plain variant carries no fields.
    let plain = serde_json::to_string(&SessionKind::Plain).unwrap();
    assert!(plain.contains("\"type\":\"plain\""), "json was {plain}");
}

#[test]
fn manifest_roundtrip_and_camel_case() {
    let e1 = ManifestEntry::new(
        aid(1),
        "Alpha",
        "agents/a.md",
        profid(9),
        Some(tid(2)),
        true,
        Some(TemplateVersion(5)),
    )
    .unwrap();
    let e2 =
        ManifestEntry::new(aid(3), "Beta", "agents/b.md", profid(9), None, false, None).unwrap();
    let m = AgentManifest::new(1, vec![e1, e2]).unwrap();
    assert_eq!(roundtrip(&m), m);
    let json = serde_json::to_string(&m).unwrap();
    // entries are serialized under "agents".
    assert!(json.contains("\"agents\":["), "json was {json}");
    assert!(json.contains("\"mdPath\""), "json was {json}");
    assert!(
        json.contains("\"syncedTemplateVersion\":5"),
        "json was {json}"
    );
    // Non-synchronized entry omits optional template fields (skip_serializing_if).
    assert!(!json.contains("\"templateId\":null"), "json was {json}");
}

// ---------------------------------------------------------------------------
// Skill (L12) — round-trip, camelCase scope tag, manifest skills back-compat
// ---------------------------------------------------------------------------

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

#[test]
fn skill_roundtrip_and_camel_case_scope() {
    let s = Skill::new(
        sid(1),
        "code-review",
        MarkdownDoc::new("body"),
        SkillScope::Global,
    )
    .unwrap();
    assert_eq!(roundtrip(&s), s);
    let json = serde_json::to_string(&s).unwrap();
    assert!(json.contains("\"scope\":\"global\""), "json was {json}");
    assert!(json.contains("\"contentMd\""), "json was {json}");

    let p = Skill::new(
        sid(2),
        "simplify",
        MarkdownDoc::new("b"),
        SkillScope::Project,
    )
    .unwrap();
    let pj = serde_json::to_string(&p).unwrap();
    assert!(pj.contains("\"scope\":\"project\""), "json was {pj}");
}

#[test]
fn manifest_entry_skills_roundtrip_and_camel_case() {
    let entry = ManifestEntry::from_agent(
        &Agent::new(
            aid(1),
            "dev",
            "agents/dev.md",
            profid(9),
            AgentOrigin::Scratch,
            false,
        )
        .unwrap()
        .with_skills(vec![SkillRef::new(sid(5), SkillScope::Project)]),
    );
    assert_eq!(roundtrip(&entry), entry);
    let json = serde_json::to_string(&entry).unwrap();
    assert!(json.contains("\"skillId\""), "json was {json}");
    assert!(json.contains("\"scope\":\"project\""), "json was {json}");
}

#[test]
fn manifest_entry_without_skills_omits_field_and_deserialises() {
    // An entry with no skills must not emit "skills" (skip_serializing_if),
    // and a pre-L12 manifest JSON (no skills key) must deserialise to empty.
    let entry =
        ManifestEntry::new(aid(1), "dev", "agents/dev.md", profid(9), None, false, None).unwrap();
    let json = serde_json::to_string(&entry).unwrap();
    assert!(!json.contains("\"skills\""), "json was {json}");

    let legacy = r#"{"agentId":"00000000-0000-0000-0000-000000000001","name":"dev","mdPath":"agents/dev.md","profileId":"00000000-0000-0000-0000-000000000009","synchronized":false}"#;
    let parsed: ManifestEntry = serde_json::from_str(legacy).unwrap();
    assert!(parsed.skills.is_empty());
}

// ---------------------------------------------------------------------------
// LayoutTree (tagged enum: type/node)
// ---------------------------------------------------------------------------

#[test]
fn layout_roundtrip() {
    let tree = LayoutTree::new(LayoutNode::Split(SplitContainer {
        id: node(9),
        direction: Direction::Column,
        children: vec![
            WeightedChild {
                node: LayoutNode::Leaf(LeafCell {
                    id: node(1),
                    session: Some(session(100)),
                    agent: None,
                    conversation_id: None,
                    engine_session_id: None,
                    agent_was_running: false,
                }),
                weight: 1.5,
            },
            WeightedChild {
                node: LayoutNode::Leaf(LeafCell {
                    id: node(2),
                    session: None,
                    agent: None,
                    conversation_id: None,
                    engine_session_id: None,
                    agent_was_running: false,
                }),
                weight: 2.5,
            },
        ],
    }));
    assert_eq!(roundtrip(&tree), tree);
    let json = serde_json::to_string(&tree).unwrap();
    // enum adjacently tagged: type + node ; direction camelCase.
    assert!(json.contains("\"type\":\"split\""), "json was {json}");
    assert!(json.contains("\"type\":\"leaf\""), "json was {json}");
    assert!(json.contains("\"direction\":\"column\""), "json was {json}");
    // empty session leaf omits the field.
    assert!(!json.contains("\"session\":null"), "json was {json}");
}

#[test]
fn leaf_with_agent_roundtrip_and_omits_null() {
    use domain::ids::AgentId;
    let agent_uuid = Uuid::from_u128(0xABC);
    let tree = LayoutTree::new(LayoutNode::Leaf(LeafCell {
        id: node(1),
        session: None,
        agent: Some(AgentId::from_uuid(agent_uuid)),
        conversation_id: None,
        engine_session_id: None,
        agent_was_running: false,
    }));
    let rt = roundtrip(&tree);
    match rt.root {
        LayoutNode::Leaf(l) => assert_eq!(l.agent, Some(AgentId::from_uuid(agent_uuid))),
        _ => panic!("expected leaf"),
    }
    let json = serde_json::to_string(&tree).unwrap();
    // agent present when set
    assert!(
        json.contains("\"agent\""),
        "agent field should be present when set; json was {json}"
    );
    // null variant omitted
    let tree_no_agent = LayoutTree::new(LayoutNode::Leaf(LeafCell {
        id: node(2),
        session: None,
        agent: None,
        conversation_id: None,
        engine_session_id: None,
        agent_was_running: false,
    }));
    let json2 = serde_json::to_string(&tree_no_agent).unwrap();
    assert!(
        !json2.contains("\"agent\""),
        "agent field should be omitted when None; json was {json2}"
    );
}

#[test]
fn custom_plugin_layout_roundtrips_with_opaque_state() {
    let tree = LayoutTree::new(LayoutNode::CustomPluginLayout(CustomPluginLayoutCell {
        id: node(43),
        plugin_id: PluginId::new("dev.acme.gitgraph").unwrap(),
        layout_type: PluginLayoutType::new("dev.acme.gitgraph.layout").unwrap(),
        state: serde_json::json!({"branch":"main","zoom":2}),
    }));

    assert_eq!(roundtrip(&tree), tree);
    let json = serde_json::to_string(&tree).unwrap();
    assert!(
        json.contains("\"type\":\"customPluginLayout\""),
        "json was {json}"
    );
    assert!(
        json.contains("\"pluginId\":\"dev.acme.gitgraph\""),
        "json was {json}"
    );
    assert!(
        json.contains("\"layoutType\":\"dev.acme.gitgraph.layout\""),
        "json was {json}"
    );
    assert!(json.contains("\"state\""), "json was {json}");
}
