//! L1 tests for the D4 structured-chat DTO contract (ARCHITECTURE §17.7):
//! - `ReplyChunk` tagged camelCase round-trip (`kind` + payload),
//! - `ReattachChatDto` camelCase wire shape (typed scrollback),
//! - the **derived** `cellKind` on `TerminalSessionDto` (`chat` ⇔
//!   `structured: Some(..)`, `pty` otherwise),
//! - non-regression: every `TerminalSessionDto` construction path now serialises a
//!   `cellKind` and PTY paths keep `"pty"`.

use app_tauri_lib::dto::{CellKind, ReattachChatDto, ReplyChunk, TerminalSessionDto};
use application::{LaunchAgentOutput, StructuredSessionDescriptor};
use domain::project::ProjectPath;
use domain::SessionId;
use domain::{AgentId, NodeId, PtySize, SessionKind, SessionStatus, TerminalSession};
use serde_json::json;
use uuid::Uuid;

// ---------------------------------------------------------------------------
// ReplyChunk — tagged camelCase, exact wire shape + round-trip (zone 5/6)
// ---------------------------------------------------------------------------

#[test]
fn reply_chunk_text_delta_serialises_exact_camel_case() {
    let v = serde_json::to_value(ReplyChunk::TextDelta {
        text: "hello".into(),
    })
    .unwrap();
    assert_eq!(v, json!({ "kind": "textDelta", "text": "hello" }));
}

#[test]
fn reply_chunk_tool_activity_serialises_exact_camel_case() {
    let v = serde_json::to_value(ReplyChunk::ToolActivity {
        label: "reads file".into(),
    })
    .unwrap();
    assert_eq!(v, json!({ "kind": "toolActivity", "label": "reads file" }));
}

#[test]
fn reply_chunk_final_serialises_exact_camel_case() {
    let v = serde_json::to_value(ReplyChunk::Final {
        content: "done".into(),
    })
    .unwrap();
    assert_eq!(v, json!({ "kind": "final", "content": "done" }));
}

#[test]
fn reply_chunk_error_serialises_exact_camel_case() {
    let v = serde_json::to_value(ReplyChunk::Error {
        message: "Réponse vide du modèle.".into(),
    })
    .unwrap();
    assert_eq!(
        v,
        json!({ "kind": "error", "message": "Réponse vide du modèle." })
    );
}

#[test]
fn reply_chunk_round_trips_through_json_for_every_variant() {
    for chunk in [
        ReplyChunk::TextDelta { text: "x".into() },
        ReplyChunk::ToolActivity {
            label: "runs".into(),
        },
        ReplyChunk::Final {
            content: "y".into(),
        },
        ReplyChunk::Error {
            message: "empty".into(),
        },
    ] {
        let v = serde_json::to_value(&chunk).unwrap();
        let back: ReplyChunk = serde_json::from_value(v).unwrap();
        assert_eq!(back, chunk, "round-trip preserves the variant + payload");
    }
}

#[test]
fn reply_chunk_deserialises_from_camel_case_wire_payload() {
    // The shape the frontend (or a mock gateway) emits.
    let back: ReplyChunk =
        serde_json::from_value(json!({ "kind": "textDelta", "text": "hi" })).unwrap();
    assert_eq!(back, ReplyChunk::TextDelta { text: "hi".into() });
}

#[test]
fn reply_chunk_rejects_snake_case_tag() {
    // Guard: a snake_case `text_delta` is NOT a valid wire tag (contract is camelCase).
    let r: Result<ReplyChunk, _> =
        serde_json::from_value(json!({ "kind": "text_delta", "text": "x" }));
    assert!(r.is_err(), "snake_case kind must not deserialise");
}

// ---------------------------------------------------------------------------
// ReattachChatDto — typed scrollback, camelCase (zone 5)
// ---------------------------------------------------------------------------

#[test]
fn reattach_chat_dto_serialises_camel_case_with_typed_scrollback() {
    let dto = ReattachChatDto {
        session_id: "sess-1".into(),
        scrollback: vec![
            ReplyChunk::TextDelta { text: "Hi".into() },
            ReplyChunk::Final {
                content: "Hi".into(),
            },
        ],
    };
    let v = serde_json::to_value(&dto).unwrap();
    assert_eq!(
        v,
        json!({
            "sessionId": "sess-1",
            "scrollback": [
                { "kind": "textDelta", "text": "Hi" },
                { "kind": "final", "content": "Hi" },
            ],
        })
    );
    assert!(v.get("session_id").is_none(), "no snake_case leak");
}

#[test]
fn reattach_chat_dto_empty_scrollback_is_empty_array() {
    let dto = ReattachChatDto {
        session_id: "s".into(),
        scrollback: vec![],
    };
    let v = serde_json::to_value(&dto).unwrap();
    assert_eq!(v["scrollback"], json!([]));
}

// ---------------------------------------------------------------------------
// CellKind enum wire shape
// ---------------------------------------------------------------------------

#[test]
fn cell_kind_serialises_lowercase_pty_and_chat() {
    assert_eq!(serde_json::to_value(CellKind::Pty).unwrap(), json!("pty"));
    assert_eq!(serde_json::to_value(CellKind::Chat).unwrap(), json!("chat"));
}

// ---------------------------------------------------------------------------
// cellKind derivation on TerminalSessionDto (zone 5)
// ---------------------------------------------------------------------------

fn agent_session(session_id: u128) -> (SessionId, TerminalSession) {
    let sid = SessionId::from_uuid(Uuid::from_u128(session_id));
    let node_id = NodeId::from_uuid(Uuid::from_u128(8));
    let agent_id = AgentId::from_uuid(Uuid::from_u128(9));
    let cwd = ProjectPath::new("/tmp/project".to_owned()).expect("valid path");
    let size = PtySize::new(24, 80).unwrap();
    let mut session =
        TerminalSession::starting(sid, node_id, cwd, SessionKind::Agent { agent_id }, size);
    session.status = SessionStatus::Running;
    (sid, session)
}

#[test]
fn launch_output_with_structured_descriptor_derives_chat_cell_kind() {
    let (sid, session) = agent_session(7);
    let descriptor = StructuredSessionDescriptor {
        session_id: sid,
        agent_id: AgentId::from_uuid(Uuid::from_u128(9)),
        node_id: NodeId::from_uuid(Uuid::from_u128(8)),
        conversation_id: None,
    };
    let out = LaunchAgentOutput {
        session,
        assigned_conversation_id: None,
        engine_session_id: None,
        structured: Some(descriptor),
        profile: None,
    };
    let dto = TerminalSessionDto::from(out);
    assert_eq!(dto.cell_kind, CellKind::Chat);

    let v = serde_json::to_value(&dto).unwrap();
    assert_eq!(v["cellKind"], "chat", "structured ⇒ chat on the wire");
}

#[test]
fn launch_output_without_structured_descriptor_derives_pty_cell_kind() {
    let (_sid, session) = agent_session(7);
    let out = LaunchAgentOutput {
        session,
        assigned_conversation_id: None,
        engine_session_id: None,
        structured: None,
        profile: None,
    };
    let dto = TerminalSessionDto::from(out);
    assert_eq!(dto.cell_kind, CellKind::Pty, "no descriptor ⇒ pty");

    let v = serde_json::to_value(&dto).unwrap();
    assert_eq!(v["cellKind"], "pty");
}

// ---------------------------------------------------------------------------
// Non-regression: cellKind is always present & "pty" on the historical paths
// ---------------------------------------------------------------------------

#[test]
fn terminal_session_dto_from_domain_session_is_always_pty() {
    // From<TerminalSession> (e.g. open_terminal / change_agent_profile relaunch).
    let (_sid, session) = agent_session(11);
    let dto = TerminalSessionDto::from(session);
    assert_eq!(dto.cell_kind, CellKind::Pty);
    let v = serde_json::to_value(&dto).unwrap();
    assert_eq!(
        v["cellKind"], "pty",
        "the new field is always present on the PTY path (non-breaking shape)"
    );
}

#[test]
fn pty_launch_output_serialises_cellkind_pty_without_breaking_existing_keys() {
    // Guard the exact historical key set + the new derived field for a PTY launch.
    let (sid, session) = agent_session(12);
    let out = LaunchAgentOutput {
        session,
        assigned_conversation_id: None,
        engine_session_id: None,
        structured: None,
        profile: None,
    };
    let v = serde_json::to_value(TerminalSessionDto::from(out)).unwrap();
    assert_eq!(v["sessionId"], sid.to_string());
    assert_eq!(v["cwd"], "/tmp/project");
    assert_eq!(v["rows"], 24);
    assert_eq!(v["cols"], 80);
    assert_eq!(v["cellKind"], "pty");
    // assignedConversationId omitted when None (skip_serializing_if) — unchanged.
    assert!(v.get("assignedConversationId").is_none());
    assert!(v.get("session_id").is_none(), "no snake_case leak");
}
