//! L1 tests for the IPC DTO (de)serialisation contract: camelCase on the wire,
//! stable `ErrorDto.code`, and the `DomainEvent -> DomainEventDto` mapping with
//! its tagged, camelCase JSON shape.

use app_tauri_lib::dto::{
    parse_node_id, parse_session_id, ErrorDto, HealthRequestDto, HealthResponseDto, LayoutDto,
    LayoutOperationDto, OpenTerminalRequestDto, ReattachResultDto, ResizeTerminalRequestDto,
    TerminalClosedDto, WriteTerminalRequestDto,
};
use app_tauri_lib::events::{DomainEventDto, DOMAIN_EVENT};
use application::{CloseTerminalOutput, LayoutOperation, LoadLayoutOutput, OpenTerminalInput};
use domain::{Direction, LayoutNode, LayoutTree, LeafCell, NodeId};

use application::{AppError, HealthInput};
use domain::events::DomainEvent;
use domain::ids::{AgentId, ProfileId, SessionId};
use domain::ProjectId;
use domain::TemplateId;
use domain::TemplateVersion;
use serde_json::json;
use uuid::Uuid;

#[test]
fn health_response_serialises_camel_case() {
    let dto = HealthResponseDto {
        version: "1.2.3".into(),
        alive: true,
        time_millis: 42,
        correlation_id: "abc".into(),
        note: Some("hi".into()),
    };
    let v = serde_json::to_value(&dto).unwrap();
    assert_eq!(
        v,
        json!({
            "version": "1.2.3",
            "alive": true,
            "timeMillis": 42,
            "correlationId": "abc",
            "note": "hi",
        })
    );
}

#[test]
fn health_request_deserialises_camel_case_and_defaults() {
    let dto: HealthRequestDto = serde_json::from_value(json!({ "note": "x" })).unwrap();
    assert_eq!(HealthInput::from(dto).note.as_deref(), Some("x"));

    // Missing note defaults to None.
    let empty: HealthRequestDto = serde_json::from_value(json!({})).unwrap();
    assert_eq!(HealthInput::from(empty).note, None);
}

#[test]
fn error_dto_carries_stable_code_and_message() {
    let dto = ErrorDto::from(AppError::NotFound("project".into()));
    assert_eq!(dto.code, "NOT_FOUND");
    assert!(dto.message.contains("project"));

    let v = serde_json::to_value(&dto).unwrap();
    assert_eq!(v["code"], "NOT_FOUND");
    assert!(v.get("message").is_some());
}

#[test]
fn domain_event_relay_name_is_frozen() {
    assert_eq!(DOMAIN_EVENT, "domain://event");
}

#[test]
fn domain_event_dto_is_tagged_and_camel_case() {
    let pid = ProjectId::from_uuid(Uuid::nil());
    let dto = DomainEventDto::from(&DomainEvent::ProjectCreated { project_id: pid });
    let v = serde_json::to_value(&dto).unwrap();
    assert_eq!(
        v,
        json!({ "type": "projectCreated", "projectId": pid.to_string() })
    );
}

#[test]
fn domain_event_dto_maps_agent_launched() {
    let aid = AgentId::from_uuid(Uuid::nil());
    let sid = SessionId::from_uuid(Uuid::nil());
    let dto = DomainEventDto::from(&DomainEvent::AgentLaunched {
        agent_id: aid,
        session_id: sid,
    });
    let v = serde_json::to_value(&dto).unwrap();
    assert_eq!(v["type"], "agentLaunched");
    assert_eq!(v["agentId"], aid.to_string());
    assert_eq!(v["sessionId"], sid.to_string());
}

#[test]
fn domain_event_dto_maps_agent_profile_changed() {
    // LOT A0 : round-trip camelCase agentId/profileId du miroir DTO.
    let aid = AgentId::from_uuid(Uuid::from_u128(1));
    let pid = ProfileId::from_uuid(Uuid::from_u128(2));
    let dto = DomainEventDto::from(&DomainEvent::AgentProfileChanged {
        agent_id: aid,
        profile_id: pid,
    });
    let v = serde_json::to_value(&dto).unwrap();
    assert_eq!(
        v,
        json!({
            "type": "agentProfileChanged",
            "agentId": aid.to_string(),
            "profileId": pid.to_string(),
        })
    );
}

#[test]
fn domain_event_dto_maps_template_updated_version() {
    let tid = TemplateId::from_uuid(Uuid::nil());
    let dto = DomainEventDto::from(&DomainEvent::TemplateUpdated {
        template_id: tid,
        version: TemplateVersion::INITIAL,
    });
    let v = serde_json::to_value(&dto).unwrap();
    assert_eq!(v["type"], "templateUpdated");
    assert_eq!(v["templateId"], tid.to_string());
    assert!(v["version"].is_u64());
}

#[test]
fn domain_event_dto_maps_pty_output_bytes() {
    let sid = SessionId::from_uuid(Uuid::nil());
    let dto = DomainEventDto::from(&DomainEvent::PtyOutput {
        session_id: sid,
        bytes: vec![1, 2, 3],
    });
    let v = serde_json::to_value(&dto).unwrap();
    assert_eq!(v["type"], "ptyOutput");
    assert_eq!(v["bytes"], json!([1, 2, 3]));
}

// ---------------------------------------------------------------------------
// M4 — orchestration source on `orchestratorRequestProcessed`
// ---------------------------------------------------------------------------

#[test]
fn orchestration_source_dto_serialises_lowercase() {
    use app_tauri_lib::events::OrchestrationSourceDto;
    use domain::events::OrchestrationSource;

    // File → "file"; Mcp → "mcp" (camelCase rename on a unit enum = lowercase).
    let file =
        serde_json::to_value(OrchestrationSourceDto::from(OrchestrationSource::File)).unwrap();
    assert_eq!(file, json!("file"));

    let mcp = serde_json::to_value(OrchestrationSourceDto::from(OrchestrationSource::Mcp)).unwrap();
    assert_eq!(mcp, json!("mcp"));
}

#[test]
fn domain_event_dto_maps_orchestrator_request_processed_with_file_source() {
    use domain::events::OrchestrationSource;

    let dto = DomainEventDto::from(&DomainEvent::OrchestratorRequestProcessed {
        requester_id: "Main".into(),
        action: "spawn_agent".into(),
        ok: true,
        source: OrchestrationSource::File,
    });
    let v = serde_json::to_value(&dto).unwrap();
    assert_eq!(
        v,
        json!({
            "type": "orchestratorRequestProcessed",
            "requesterId": "Main",
            "action": "spawn_agent",
            "ok": true,
            "source": "file",
        })
    );
}

#[test]
fn domain_event_dto_maps_orchestrator_request_processed_with_mcp_source() {
    use domain::events::OrchestrationSource;

    let dto = DomainEventDto::from(&DomainEvent::OrchestratorRequestProcessed {
        requester_id: "mcp".into(),
        action: "idea_ask_agent".into(),
        ok: false,
        source: OrchestrationSource::Mcp,
    });
    let v = serde_json::to_value(&dto).unwrap();
    assert_eq!(v["type"], "orchestratorRequestProcessed");
    assert_eq!(v["requesterId"], "mcp");
    assert_eq!(v["action"], "idea_ask_agent");
    assert_eq!(v["ok"], json!(false));
    assert_eq!(v["source"], "mcp", "MCP door must badge as `mcp`");
}

// ---------------------------------------------------------------------------
// Terminal DTOs (L3)
// ---------------------------------------------------------------------------

#[test]
fn open_terminal_request_deserialises_camel_case_with_defaults() {
    // Minimal payload: command/args omitted → None / empty.
    let dto: OpenTerminalRequestDto =
        serde_json::from_value(json!({ "cwd": "/p", "rows": 24, "cols": 80 })).unwrap();
    let input = OpenTerminalInput::from(dto);
    assert_eq!(input.cwd, "/p");
    assert_eq!(input.rows, 24);
    assert_eq!(input.cols, 80);
    assert_eq!(input.command, None);
    assert!(input.args.is_empty());
    assert_eq!(input.node_id, None);

    // Full payload with explicit command + args.
    let full: OpenTerminalRequestDto = serde_json::from_value(json!({
        "cwd": "/q", "rows": 10, "cols": 20,
        "command": "/bin/zsh", "args": ["-l"],
    }))
    .unwrap();
    let input = OpenTerminalInput::from(full);
    assert_eq!(input.command.as_deref(), Some("/bin/zsh"));
    assert_eq!(input.args, vec!["-l".to_owned()]);
}

#[test]
fn write_terminal_request_deserialises_session_id_and_data() {
    let sid = SessionId::from_uuid(Uuid::nil());
    let dto: WriteTerminalRequestDto = serde_json::from_value(json!({
        "sessionId": sid.to_string(),
        "data": [104, 105],
    }))
    .unwrap();
    let input = dto.into_input().expect("valid session id");
    assert_eq!(input.session_id, sid);
    assert_eq!(input.data, vec![104u8, 105]);
}

#[test]
fn write_terminal_request_rejects_bad_session_id() {
    let dto: WriteTerminalRequestDto =
        serde_json::from_value(json!({ "sessionId": "not-a-uuid", "data": [] })).unwrap();
    let err = dto.into_input().expect_err("malformed id rejected");
    assert_eq!(err.code, "INVALID");
}

#[test]
fn resize_terminal_request_deserialises_camel_case() {
    let sid = SessionId::from_uuid(Uuid::nil());
    let dto: ResizeTerminalRequestDto = serde_json::from_value(json!({
        "sessionId": sid.to_string(), "rows": 40, "cols": 120,
    }))
    .unwrap();
    let input = dto.into_input().expect("valid");
    assert_eq!(input.session_id, sid);
    assert_eq!(input.rows, 40);
    assert_eq!(input.cols, 120);
}

#[test]
fn terminal_closed_dto_serialises_code_camel_case() {
    let dto = TerminalClosedDto::from(CloseTerminalOutput { code: Some(0) });
    let v = serde_json::to_value(&dto).unwrap();
    assert_eq!(v, json!({ "code": 0 }));

    // Signalled (None) round-trips as null.
    let none = TerminalClosedDto::from(CloseTerminalOutput { code: None });
    assert_eq!(
        serde_json::to_value(&none).unwrap(),
        json!({ "code": null })
    );
}

#[test]
fn reattach_result_dto_serialises_camel_case() {
    let dto = ReattachResultDto {
        session_id: "sess-1".into(),
        scrollback: vec![104, 105],
    };
    let v = serde_json::to_value(&dto).unwrap();
    assert_eq!(
        v,
        json!({ "sessionId": "sess-1", "scrollback": [104, 105] })
    );
}

#[test]
fn parse_session_id_accepts_uuid_and_rejects_garbage() {
    let sid = SessionId::from_uuid(Uuid::nil());
    assert_eq!(parse_session_id(&sid.to_string()).unwrap(), sid);
    assert_eq!(parse_session_id("nope").unwrap_err().code, "INVALID");
}

// ---------------------------------------------------------------------------
// Layout (L4)
// ---------------------------------------------------------------------------

fn nid(n: u128) -> NodeId {
    NodeId::from_uuid(Uuid::from_u128(n))
}

#[test]
fn layout_dto_serialises_camelcase_tagged_tree() {
    // A single-leaf tree → transparent over LayoutTree, tagged `{type,node}`.
    let tree = LayoutTree::single(LeafCell {
        id: nid(1),
        session: None,
        agent: None,
        conversation_id: None,
        engine_session_id: None,
        agent_was_running: false,
    });
    let dto = LayoutDto::from(LoadLayoutOutput {
        layout_id: domain::LayoutId::new_random(),
        layout: tree,
    });
    let v = serde_json::to_value(&dto).unwrap();
    assert_eq!(v["root"]["type"], "leaf", "enum tagged on `type`");
    assert_eq!(v["root"]["node"]["id"], nid(1).to_string());
    // Empty session is skipped (skip_serializing_if).
    assert!(v["root"]["node"].get("session").is_none());
}

#[test]
fn layout_operation_dto_split_deserialises_camelcase() {
    let json = json!({
        "type": "split",
        "target": nid(1).to_string(),
        "direction": "row",
        "newLeaf": nid(2).to_string(),
        "container": nid(9).to_string(),
    });
    let dto: LayoutOperationDto = serde_json::from_value(json).unwrap();
    match dto.into_operation().unwrap() {
        LayoutOperation::Split {
            target,
            direction,
            new_leaf,
            container,
        } => {
            assert_eq!(target, nid(1));
            assert_eq!(direction, Direction::Row);
            assert_eq!(new_leaf, nid(2));
            assert_eq!(container, nid(9));
        }
        other => panic!("expected Split, got {other:?}"),
    }
}

#[test]
fn layout_operation_dto_set_session_accepts_null_session() {
    // `session: null` → detach (None).
    let json = json!({
        "type": "setSession",
        "target": nid(1).to_string(),
        "session": null,
    });
    let dto: LayoutOperationDto = serde_json::from_value(json).unwrap();
    match dto.into_operation().unwrap() {
        LayoutOperation::SetSession { target, session } => {
            assert_eq!(target, nid(1));
            assert!(session.is_none());
        }
        other => panic!("expected SetSession, got {other:?}"),
    }
}

#[test]
fn layout_operation_dto_resize_carries_weights() {
    let json = json!({
        "type": "resize",
        "container": nid(9).to_string(),
        "weights": [3.0, 1.0],
    });
    let dto: LayoutOperationDto = serde_json::from_value(json).unwrap();
    match dto.into_operation().unwrap() {
        LayoutOperation::Resize { container, weights } => {
            assert_eq!(container, nid(9));
            assert_eq!(weights, vec![3.0, 1.0]);
        }
        other => panic!("expected Resize, got {other:?}"),
    }
}

#[test]
fn layout_operation_dto_rejects_malformed_uuid_as_invalid() {
    let json = json!({
        "type": "merge",
        "container": "not-a-uuid",
        "keepIndex": 0,
    });
    let dto: LayoutOperationDto = serde_json::from_value(json).unwrap();
    let err = dto.into_operation().expect_err("malformed uuid rejected");
    assert_eq!(err.code, "INVALID", "got {err:?}");
}

#[test]
fn parse_node_id_accepts_uuid_and_rejects_garbage() {
    assert_eq!(parse_node_id(&nid(7).to_string()).unwrap(), nid(7));
    assert_eq!(parse_node_id("garbage").unwrap_err().code, "INVALID");
}

#[test]
fn layout_dto_round_trips_a_split_tree_shape() {
    // Build a split tree, serialise via the DTO, and confirm the tagged shape
    // re-parses into an equivalent LayoutTree.
    let tree = LayoutTree::single(LeafCell {
        id: nid(1),
        session: None,
        agent: None,
        conversation_id: None,
        engine_session_id: None,
        agent_was_running: false,
    })
    .split(
        nid(1),
        Direction::Column,
        LeafCell {
            id: nid(2),
            session: None,
            agent: None,
            conversation_id: None,
            engine_session_id: None,
            agent_was_running: false,
        },
        nid(9),
    )
    .unwrap();
    let dto = LayoutDto::from(LoadLayoutOutput {
        layout_id: domain::LayoutId::new_random(),
        layout: tree.clone(),
    });
    let v = serde_json::to_value(&dto).unwrap();
    let back: LayoutTree = serde_json::from_value(v).unwrap();
    assert_eq!(back, tree, "DTO serialisation round-trips the tree");
    assert!(matches!(back.root, LayoutNode::Split(_)));
}
