//! Tests for the layout-management (#4) and per-cell-agent (#3) DTOs:
//! camelCase wire shape, `parse_layout_id` error behaviour, `ListLayoutsDto` /
//! `CreateLayoutResultDto` / `DeleteLayoutResultDto` mappings, and
//! `setCellAgent` operation deserialisation.

use app_tauri_lib::dto::{
    parse_layout_id, CreateLayoutRequestDto, CreateLayoutResultDto, DeleteLayoutRequestDto,
    DeleteLayoutResultDto, LayoutInfoDto, LayoutOperationDto, ListLayoutsDto,
    RenameLayoutRequestDto, SetActiveLayoutRequestDto,
};
use application::{
    CreateLayoutOutput, DeleteLayoutOutput, LayoutInfo, LayoutKind, ListLayoutsOutput,
    PluginLayoutOrigin,
};
use domain::ids::{AgentId, LayoutId, NodeId};
use domain::{PluginId, PluginLayoutType};
use serde_json::json;
use uuid::Uuid;

fn lid(n: u128) -> LayoutId {
    LayoutId::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))
}

// ---------------------------------------------------------------------------
// LayoutInfoDto
// ---------------------------------------------------------------------------

#[test]
fn layout_info_dto_serialises_camelcase() {
    let info = LayoutInfo {
        id: lid(1),
        name: "Default".to_owned(),
        kind: LayoutKind::Terminal,
    };
    let dto = LayoutInfoDto::from(info);
    let v = serde_json::to_value(&dto).unwrap();
    assert_eq!(v["id"], lid(1).to_string());
    assert_eq!(v["name"], "Default");
    assert_eq!(v["kind"], "terminal");
}

#[test]
fn layout_info_dto_git_graph_kind() {
    let info = LayoutInfo {
        id: lid(2),
        name: "Graph".to_owned(),
        kind: LayoutKind::GitGraph,
    };
    let dto = LayoutInfoDto::from(info);
    let v = serde_json::to_value(&dto).unwrap();
    assert_eq!(v["kind"], "gitGraph");
}

#[test]
fn layout_info_dto_plugin_kind() {
    let info = LayoutInfo {
        id: lid(3),
        name: "Android Health".to_owned(),
        kind: LayoutKind::Plugin {
            plugin_origin: PluginLayoutOrigin {
                plugin_id: PluginId::new("dev.idea.android-plugin").unwrap(),
                layout_type: PluginLayoutType::new("idea-android.health").unwrap(),
            },
            state: json!({}),
        },
    };
    let dto = LayoutInfoDto::from(info);
    let v = serde_json::to_value(&dto).unwrap();
    assert_eq!(v["kind"], "plugin");
}

// ---------------------------------------------------------------------------
// ListLayoutsDto
// ---------------------------------------------------------------------------

#[test]
fn list_layouts_dto_from_output() {
    let out = ListLayoutsOutput {
        layouts: vec![
            LayoutInfo {
                id: lid(1),
                name: "Default".to_owned(),
                kind: LayoutKind::Terminal,
            },
            LayoutInfo {
                id: lid(2),
                name: "Backend".to_owned(),
                kind: LayoutKind::GitGraph,
            },
        ],
        active_id: lid(1),
    };
    let dto = ListLayoutsDto::from(out);
    let v = serde_json::to_value(&dto).unwrap();

    let layouts = v["layouts"].as_array().unwrap();
    assert_eq!(layouts.len(), 2);
    assert_eq!(layouts[0]["name"], "Default");
    assert_eq!(layouts[0]["kind"], "terminal");
    assert_eq!(layouts[1]["name"], "Backend");
    assert_eq!(layouts[1]["kind"], "gitGraph");
    assert_eq!(v["activeId"], lid(1).to_string(), "camelCase activeId");
    assert!(v.get("active_id").is_none(), "no snake_case leak");
}

// ---------------------------------------------------------------------------
// CreateLayoutResultDto
// ---------------------------------------------------------------------------

#[test]
fn create_layout_result_dto_from_output() {
    let out = CreateLayoutOutput { layout_id: lid(42) };
    let dto = CreateLayoutResultDto::from(out);
    let v = serde_json::to_value(&dto).unwrap();
    assert_eq!(v["layoutId"], lid(42).to_string(), "camelCase layoutId");
    assert!(v.get("layout_id").is_none(), "no snake_case leak");
}

// ---------------------------------------------------------------------------
// DeleteLayoutResultDto
// ---------------------------------------------------------------------------

#[test]
fn delete_layout_result_dto_from_output() {
    let out = DeleteLayoutOutput { active_id: lid(1) };
    let dto = DeleteLayoutResultDto::from(out);
    let v = serde_json::to_value(&dto).unwrap();
    assert_eq!(v["activeId"], lid(1).to_string(), "camelCase activeId");
}

// ---------------------------------------------------------------------------
// Request DTO deserialisation
// ---------------------------------------------------------------------------

#[test]
fn create_layout_request_deserialises_camelcase() {
    let project_id = Uuid::from_u128(10).to_string();
    let raw = json!({ "projectId": project_id, "name": "Backend" });
    let dto: CreateLayoutRequestDto = serde_json::from_value(raw).unwrap();
    assert_eq!(dto.project_id, project_id);
    assert_eq!(dto.name, "Backend");
    assert!(dto.kind.is_none(), "kind defaults to None when absent");
}

#[test]
fn create_layout_request_with_git_graph_kind() {
    let project_id = Uuid::from_u128(11).to_string();
    let raw = json!({ "projectId": project_id, "name": "Graph", "kind": "gitGraph" });
    let dto: CreateLayoutRequestDto = serde_json::from_value(raw).unwrap();
    assert_eq!(dto.kind.as_deref(), Some("gitGraph"));
    let kind = dto.parse_kind().unwrap();
    assert_eq!(kind, application::LayoutKind::GitGraph);
}

#[test]
fn create_layout_request_with_plugin_kind() {
    let project_id = Uuid::from_u128(13).to_string();
    let raw = json!({
        "projectId": project_id,
        "name": "Android Health",
        "kind": "plugin",
        "pluginOrigin": {
            "pluginId": "dev.idea.android-plugin",
            "layoutType": "idea-android.health"
        },
        "state": { "deviceId": "pixel-8" }
    });
    let dto: CreateLayoutRequestDto = serde_json::from_value(raw).unwrap();
    let kind = dto.parse_kind().unwrap();
    match kind {
        application::LayoutKind::Plugin {
            plugin_origin,
            state,
        } => {
            assert_eq!(plugin_origin.plugin_id.as_str(), "dev.idea.android-plugin");
            assert_eq!(plugin_origin.layout_type.as_str(), "idea-android.health");
            assert_eq!(state["deviceId"], "pixel-8");
        }
        _ => panic!("expected plugin layout kind"),
    }
}

#[test]
fn create_layout_request_plugin_kind_requires_origin() {
    let project_id = Uuid::from_u128(14).to_string();
    let raw = json!({ "projectId": project_id, "name": "Android Health", "kind": "plugin" });
    let dto: CreateLayoutRequestDto = serde_json::from_value(raw).unwrap();
    let err = dto.parse_kind().unwrap_err();
    assert_eq!(err.code, "INVALID");
}

#[test]
fn create_layout_request_unknown_kind_is_invalid() {
    let project_id = Uuid::from_u128(12).to_string();
    let raw = json!({ "projectId": project_id, "name": "X", "kind": "unknown" });
    let dto: CreateLayoutRequestDto = serde_json::from_value(raw).unwrap();
    let err = dto.parse_kind().unwrap_err();
    assert_eq!(err.code, "INVALID");
}

#[test]
fn rename_layout_request_deserialises_camelcase() {
    let raw = json!({
        "projectId": Uuid::nil().to_string(),
        "layoutId": Uuid::nil().to_string(),
        "name": "Renamed"
    });
    let dto: RenameLayoutRequestDto = serde_json::from_value(raw).unwrap();
    assert_eq!(dto.name, "Renamed");
}

#[test]
fn delete_layout_request_deserialises_camelcase() {
    let project_id = Uuid::from_u128(1).to_string();
    let layout_id = Uuid::from_u128(2).to_string();
    let raw = json!({ "projectId": project_id, "layoutId": layout_id });
    let dto: DeleteLayoutRequestDto = serde_json::from_value(raw).unwrap();
    assert_eq!(dto.project_id, project_id);
    assert_eq!(dto.layout_id, layout_id);
}

#[test]
fn set_active_layout_request_deserialises_camelcase() {
    let raw = json!({
        "projectId": Uuid::nil().to_string(),
        "layoutId": Uuid::nil().to_string()
    });
    let _dto: SetActiveLayoutRequestDto = serde_json::from_value(raw).unwrap();
}

// ---------------------------------------------------------------------------
// parse_layout_id
// ---------------------------------------------------------------------------

#[test]
fn parse_layout_id_accepts_uuid() {
    let id = Uuid::from_u128(5);
    assert_eq!(
        parse_layout_id(&id.to_string()).unwrap(),
        LayoutId::from_uuid(id)
    );
}

#[test]
fn parse_layout_id_rejects_garbage() {
    let err = parse_layout_id("not-a-uuid").expect_err("garbage rejected");
    assert_eq!(err.code, "INVALID");
}

// ---------------------------------------------------------------------------
// setCellAgent operation deserialisation (#3)
// ---------------------------------------------------------------------------

#[test]
fn set_cell_agent_op_deserialises_with_agent() {
    let target = nid(1);
    let agent = aid(99);
    let raw = json!({
        "type": "setCellAgent",
        "target": target.to_string(),
        "agent": agent.to_string()
    });
    let dto: LayoutOperationDto = serde_json::from_value(raw).unwrap();
    let op = dto.into_operation().unwrap();
    match op {
        application::LayoutOperation::SetCellAgent {
            target: t,
            agent: a,
        } => {
            assert_eq!(t, target);
            assert_eq!(a, Some(agent));
        }
        _ => panic!("expected SetCellAgent"),
    }
}

#[test]
fn set_cell_agent_op_deserialises_with_null_agent() {
    let target = nid(1);
    let raw = json!({
        "type": "setCellAgent",
        "target": target.to_string(),
        "agent": null
    });
    let dto: LayoutOperationDto = serde_json::from_value(raw).unwrap();
    let op = dto.into_operation().unwrap();
    match op {
        application::LayoutOperation::SetCellAgent {
            target: t,
            agent: None,
        } => {
            assert_eq!(t, target);
        }
        _ => panic!("expected SetCellAgent with None agent"),
    }
}

#[test]
fn set_cell_agent_op_deserialises_with_absent_agent_defaults_to_none() {
    let target = nid(2);
    // omitting `agent` should default to None
    let raw = json!({
        "type": "setCellAgent",
        "target": target.to_string()
    });
    let dto: LayoutOperationDto = serde_json::from_value(raw).unwrap();
    let op = dto.into_operation().unwrap();
    match op {
        application::LayoutOperation::SetCellAgent { agent: None, .. } => {}
        _ => panic!("expected SetCellAgent with None agent"),
    }
}

// ---------------------------------------------------------------------------
// setPluginLayoutState operation deserialisation (#141)
// ---------------------------------------------------------------------------

#[test]
fn set_plugin_layout_state_op_deserialises_opaque_state() {
    let target = nid(3);
    let raw = json!({
        "type": "setPluginLayoutState",
        "target": target.to_string(),
        "state": { "deviceId": "pixel-8", "healthy": true }
    });
    let dto: LayoutOperationDto = serde_json::from_value(raw).unwrap();
    let op = dto.into_operation().unwrap();
    match op {
        application::LayoutOperation::SetPluginLayoutState { target: t, state } => {
            assert_eq!(t, target);
            assert_eq!(state["deviceId"], "pixel-8");
            assert_eq!(state["healthy"], true);
        }
        _ => panic!("expected SetPluginLayoutState"),
    }
}

// ---------------------------------------------------------------------------
// setCellConversation operation deserialisation (T4b)
// ---------------------------------------------------------------------------

#[test]
fn set_cell_conversation_op_deserialises_with_id() {
    let target = nid(1);
    let raw = json!({
        "type": "setCellConversation",
        "target": target.to_string(),
        "conversationId": "conv-42"
    });
    let dto: LayoutOperationDto = serde_json::from_value(raw).unwrap();
    let op = dto.into_operation().unwrap();
    match op {
        application::LayoutOperation::SetCellConversation {
            target: t,
            conversation_id: Some(id),
        } => {
            assert_eq!(t, target);
            assert_eq!(id, "conv-42");
        }
        _ => panic!("expected SetCellConversation with id"),
    }
}

#[test]
fn set_cell_conversation_op_deserialises_with_null_id() {
    let target = nid(2);
    let raw = json!({
        "type": "setCellConversation",
        "target": target.to_string(),
        "conversationId": null
    });
    let dto: LayoutOperationDto = serde_json::from_value(raw).unwrap();
    let op = dto.into_operation().unwrap();
    match op {
        application::LayoutOperation::SetCellConversation {
            target: t,
            conversation_id: None,
        } => assert_eq!(t, target),
        _ => panic!("expected SetCellConversation with None id"),
    }
}

#[test]
fn set_cell_conversation_op_deserialises_with_absent_id_defaults_to_none() {
    let target = nid(3);
    let raw = json!({
        "type": "setCellConversation",
        "target": target.to_string()
    });
    let dto: LayoutOperationDto = serde_json::from_value(raw).unwrap();
    let op = dto.into_operation().unwrap();
    match op {
        application::LayoutOperation::SetCellConversation {
            conversation_id: None,
            ..
        } => {}
        _ => panic!("expected SetCellConversation with None id"),
    }
}
