//! Pure layout logic: split / merge / resize / move_session, nominal and error
//! paths, grid validation, and immutability of the source tree (ARCHITECTURE §7).

mod helpers;

use domain::ids::AgentId;
use domain::{
    Direction, GridCell, GridContainer, LayoutError, LayoutNode, LayoutTree, LeafCell,
    SplitContainer, WeightedChild,
};
use helpers::{node, session};

fn agent_id(n: u128) -> AgentId {
    AgentId::from_uuid(uuid::Uuid::from_u128(n))
}

fn leaf(id: u128, sess: Option<u128>) -> LeafCell {
    LeafCell {
        id: node(id),
        session: sess.map(session),
        agent: None,
        conversation_id: None,
        engine_session_id: None,
        agent_was_running: false,
    }
}

/// A leaf pre-populated with the two resume fields, used by the
/// non-regression tests that assert these fields survive other operations.
fn leaf_with_resume(id: u128, sess: Option<u128>) -> LeafCell {
    LeafCell {
        id: node(id),
        session: sess.map(session),
        agent: None,
        conversation_id: Some("conv-1".to_string()),
        engine_session_id: None,
        agent_was_running: true,
    }
}

/// Walks the public tree to fetch the full [`LeafCell`] for `id`.
fn leaf_for(tree: &LayoutTree, id: domain::NodeId) -> Option<LeafCell> {
    fn walk(n: &LayoutNode, id: domain::NodeId) -> Option<LeafCell> {
        match n {
            LayoutNode::Leaf(l) if l.id == id => Some(l.clone()),
            LayoutNode::Leaf(_) => None,
            LayoutNode::CustomPluginLayout(_) => None,
            LayoutNode::Split(s) => s.children.iter().find_map(|c| walk(&c.node, id)),
            LayoutNode::Grid(g) => g.cells.iter().find_map(|c| walk(&c.node, id)),
        }
    }
    walk(&tree.root, id)
}

fn single(id: u128, sess: Option<u128>) -> LayoutTree {
    LayoutTree::single(leaf(id, sess))
}

// ---------------------------------------------------------------------------
// split
// ---------------------------------------------------------------------------

#[test]
fn split_nominal_produces_two_children() {
    let tree = single(1, Some(100));
    let out = tree
        .split(node(1), Direction::Row, leaf(2, None), node(9))
        .unwrap();
    match out.root {
        LayoutNode::Split(s) => {
            assert_eq!(s.id, node(9));
            assert_eq!(s.direction, Direction::Row);
            assert_eq!(s.children.len(), 2);
            assert!(s.children.iter().all(|c| c.weight > 0.0));
        }
        _ => panic!("expected a split at the root"),
    }
}

#[test]
fn split_is_immutable_source_unchanged() {
    let tree = single(1, Some(100));
    let before = tree.clone();
    let _ = tree
        .split(node(1), Direction::Column, leaf(2, None), node(9))
        .unwrap();
    assert_eq!(tree, before, "source tree must not be mutated");
}

#[test]
fn split_missing_target_is_node_not_found() {
    let tree = single(1, None);
    let err = tree
        .split(node(404), Direction::Row, leaf(2, None), node(9))
        .unwrap_err();
    assert_eq!(err, LayoutError::NodeNotFound(node(404)));
}

#[test]
fn split_into_a_session_already_present_elsewhere_is_duplicate() {
    // root leaf 1 holds session 100; we split it adding a NEW leaf that reuses
    // the same session id → duplicate must be rejected by validation.
    let tree = single(1, Some(100));
    let err = tree
        .split(node(1), Direction::Row, leaf(2, Some(100)), node(9))
        .unwrap_err();
    assert_eq!(err, LayoutError::DuplicateSession(session(100)));
}

// ---------------------------------------------------------------------------
// merge
// ---------------------------------------------------------------------------

#[test]
fn merge_keeps_selected_child() {
    let tree = single(1, Some(100))
        .split(node(1), Direction::Row, leaf(2, Some(200)), node(9))
        .unwrap();
    // keep index 1 (the new leaf with session 200).
    let merged = tree.merge(node(9), 1).unwrap();
    assert_eq!(merged.root, LayoutNode::Leaf(leaf(2, Some(200))));
}

#[test]
fn merge_is_immutable() {
    let tree = single(1, Some(100))
        .split(node(1), Direction::Row, leaf(2, None), node(9))
        .unwrap();
    let before = tree.clone();
    let _ = tree.merge(node(9), 0).unwrap();
    assert_eq!(tree, before);
}

#[test]
fn merge_unknown_container_is_node_not_found() {
    let tree = single(1, None)
        .split(node(1), Direction::Row, leaf(2, None), node(9))
        .unwrap();
    let err = tree.merge(node(404), 0).unwrap_err();
    assert_eq!(err, LayoutError::NodeNotFound(node(404)));
}

#[test]
fn merge_index_out_of_range_is_cross_container() {
    let tree = single(1, None)
        .split(node(1), Direction::Row, leaf(2, None), node(9))
        .unwrap();
    let err = tree.merge(node(9), 5).unwrap_err();
    assert_eq!(err, LayoutError::CrossContainer);
}

// ---------------------------------------------------------------------------
// resize
// ---------------------------------------------------------------------------

#[test]
fn resize_nominal_updates_weights() {
    let tree = single(1, None)
        .split(node(1), Direction::Row, leaf(2, None), node(9))
        .unwrap();
    let out = tree.resize(node(9), &[2.0, 3.0]).unwrap();
    match out.root {
        LayoutNode::Split(s) => {
            assert_eq!(s.children[0].weight, 2.0);
            assert_eq!(s.children[1].weight, 3.0);
        }
        _ => panic!("expected split"),
    }
}

#[test]
fn resize_is_immutable() {
    let tree = single(1, None)
        .split(node(1), Direction::Row, leaf(2, None), node(9))
        .unwrap();
    let before = tree.clone();
    let _ = tree.resize(node(9), &[2.0, 3.0]).unwrap();
    assert_eq!(tree, before);
}

#[test]
fn resize_nonpositive_weight_rejected() {
    let tree = single(1, None)
        .split(node(1), Direction::Row, leaf(2, None), node(9))
        .unwrap();
    let err = tree.resize(node(9), &[0.0, 1.0]).unwrap_err();
    assert_eq!(err, LayoutError::NonPositiveWeight { weight: 0.0 });

    let err = tree.resize(node(9), &[-1.0, 1.0]).unwrap_err();
    assert_eq!(err, LayoutError::NonPositiveWeight { weight: -1.0 });
}

#[test]
fn resize_wrong_arity_is_cross_container() {
    let tree = single(1, None)
        .split(node(1), Direction::Row, leaf(2, None), node(9))
        .unwrap();
    let err = tree.resize(node(9), &[1.0, 2.0, 3.0]).unwrap_err();
    assert_eq!(err, LayoutError::CrossContainer);
}

#[test]
fn resize_unknown_container_is_node_not_found() {
    let tree = single(1, None)
        .split(node(1), Direction::Row, leaf(2, None), node(9))
        .unwrap();
    let err = tree.resize(node(404), &[1.0, 2.0]).unwrap_err();
    assert_eq!(err, LayoutError::NodeNotFound(node(404)));
}

// ---------------------------------------------------------------------------
// move_session
// ---------------------------------------------------------------------------

fn two_leaves(from_sess: Option<u128>, to_sess: Option<u128>) -> LayoutTree {
    LayoutTree::new(LayoutNode::Split(SplitContainer {
        id: node(9),
        direction: Direction::Row,
        children: vec![
            WeightedChild {
                node: LayoutNode::Leaf(leaf(1, from_sess)),
                weight: 1.0,
            },
            WeightedChild {
                node: LayoutNode::Leaf(leaf(2, to_sess)),
                weight: 1.0,
            },
        ],
    }))
}

/// Looks up the session held by the leaf `id` in a tree (test-only helper that
/// walks the public structure, since the domain's lookup is private).
fn session_for(tree: &LayoutTree, id: domain::NodeId) -> Option<domain::SessionId> {
    fn walk(n: &LayoutNode, id: domain::NodeId) -> Option<Option<domain::SessionId>> {
        match n {
            LayoutNode::Leaf(l) if l.id == id => Some(l.session),
            LayoutNode::Leaf(_) => None,
            LayoutNode::CustomPluginLayout(_) => None,
            LayoutNode::Split(s) => s.children.iter().find_map(|c| walk(&c.node, id)),
            LayoutNode::Grid(g) => g.cells.iter().find_map(|c| walk(&c.node, id)),
        }
    }
    walk(&tree.root, id).flatten()
}

#[test]
fn move_session_nominal() {
    let tree = two_leaves(Some(100), None);
    let out = tree.move_session(node(1), node(2)).unwrap();
    assert_eq!(session_for(&out, node(1)), None);
    assert_eq!(session_for(&out, node(2)), Some(session(100)));
}

#[test]
fn move_session_is_immutable() {
    let tree = two_leaves(Some(100), None);
    let before = tree.clone();
    let _ = tree.move_session(node(1), node(2)).unwrap();
    assert_eq!(tree, before);
}

#[test]
fn move_session_from_empty_rejected() {
    let tree = two_leaves(None, None);
    let err = tree.move_session(node(1), node(2)).unwrap_err();
    assert_eq!(err, LayoutError::CrossContainer);
}

#[test]
fn move_session_to_occupied_rejected() {
    let tree = two_leaves(Some(100), Some(200));
    let err = tree.move_session(node(1), node(2)).unwrap_err();
    assert_eq!(err, LayoutError::CrossContainer);
}

#[test]
fn move_session_missing_leaf_is_node_not_found() {
    let tree = two_leaves(Some(100), None);
    assert_eq!(
        tree.move_session(node(404), node(2)).unwrap_err(),
        LayoutError::NodeNotFound(node(404))
    );
    assert_eq!(
        tree.move_session(node(1), node(404)).unwrap_err(),
        LayoutError::NodeNotFound(node(404))
    );
}

// ---------------------------------------------------------------------------
// validate(): grid invariants & duplicate sessions
// ---------------------------------------------------------------------------

fn grid(col_w: Vec<f32>, row_w: Vec<f32>, cells: Vec<GridCell>) -> LayoutTree {
    LayoutTree::new(LayoutNode::Grid(GridContainer {
        id: node(50),
        col_weights: col_w,
        row_weights: row_w,
        cells,
    }))
}

fn gcell(id: u128, row: u16, col: u16, rs: u16, cs: u16) -> GridCell {
    GridCell {
        node: LayoutNode::Leaf(leaf(id, None)),
        row,
        col,
        row_span: rs,
        col_span: cs,
    }
}

#[test]
fn grid_fully_covered_2x2_ok() {
    let cells = vec![
        gcell(1, 0, 0, 1, 1),
        gcell(2, 0, 1, 1, 1),
        gcell(3, 1, 0, 1, 1),
        gcell(4, 1, 1, 1, 1),
    ];
    let t = grid(vec![1.0, 1.0], vec![1.0, 1.0], cells);
    assert!(t.validate().is_ok());
}

#[test]
fn grid_merged_span_full_coverage_ok() {
    // one cell spanning the whole top row + two cells on the bottom row.
    let cells = vec![
        gcell(1, 0, 0, 1, 2), // top row merged
        gcell(2, 1, 0, 1, 1),
        gcell(3, 1, 1, 1, 1),
    ];
    let t = grid(vec![1.0, 1.0], vec![1.0, 1.0], cells);
    assert!(t.validate().is_ok());
}

#[test]
fn grid_overlap_rejected() {
    let cells = vec![
        gcell(1, 0, 0, 1, 2),
        gcell(2, 0, 1, 1, 1), // overlaps column 1 of the spanning cell
        gcell(3, 1, 0, 1, 1),
        gcell(4, 1, 1, 1, 1),
    ];
    let t = grid(vec![1.0, 1.0], vec![1.0, 1.0], cells);
    assert!(matches!(
        t.validate().unwrap_err(),
        LayoutError::OverlappingCells { .. }
    ));
}

#[test]
fn grid_uncovered_surface_rejected() {
    // 2x2 grid but only one cell → three cells uncovered.
    let t = grid(vec![1.0, 1.0], vec![1.0, 1.0], vec![gcell(1, 0, 0, 1, 1)]);
    assert!(matches!(
        t.validate().unwrap_err(),
        LayoutError::UncoveredCell { .. }
    ));
}

#[test]
fn grid_span_out_of_bounds_rejected() {
    let t = grid(vec![1.0], vec![1.0], vec![gcell(1, 0, 0, 2, 1)]);
    assert!(matches!(
        t.validate().unwrap_err(),
        LayoutError::SpanOutOfBounds { .. }
    ));
}

#[test]
fn grid_zero_span_rejected() {
    let t = grid(vec![1.0], vec![1.0], vec![gcell(1, 0, 0, 0, 1)]);
    assert_eq!(t.validate().unwrap_err(), LayoutError::InvalidSpan);
}

#[test]
fn grid_nonpositive_weight_rejected() {
    let t = grid(vec![0.0], vec![1.0], vec![gcell(1, 0, 0, 1, 1)]);
    assert_eq!(
        t.validate().unwrap_err(),
        LayoutError::NonPositiveWeight { weight: 0.0 }
    );
}

#[test]
fn duplicate_session_across_leaves_rejected() {
    let tree = two_leaves(Some(100), Some(100));
    assert_eq!(
        tree.validate().unwrap_err(),
        LayoutError::DuplicateSession(session(100))
    );
}

#[test]
fn empty_split_rejected() {
    let t = LayoutTree::new(LayoutNode::Split(SplitContainer {
        id: node(9),
        direction: Direction::Row,
        children: vec![],
    }));
    assert_eq!(t.validate().unwrap_err(), LayoutError::EmptySplit);
}

// ---------------------------------------------------------------------------
// set_session (L4: cell ↔ terminal binding bridge)
// ---------------------------------------------------------------------------

#[test]
fn set_session_attaches_to_leaf() {
    let tree = single(1, None);
    let out = tree.set_session(node(1), Some(session(100))).unwrap();
    match out.root {
        LayoutNode::Leaf(l) => {
            assert_eq!(l.id, node(1));
            assert_eq!(l.session, Some(session(100)));
        }
        _ => panic!("expected a leaf at the root"),
    }
}

#[test]
fn set_session_detaches_with_none() {
    let tree = single(1, Some(100));
    let out = tree.set_session(node(1), None).unwrap();
    match out.root {
        LayoutNode::Leaf(l) => assert_eq!(l.session, None),
        _ => panic!("expected a leaf at the root"),
    }
}

#[test]
fn set_session_is_immutable_source_unchanged() {
    let tree = single(1, None);
    let before = tree.clone();
    let _ = tree.set_session(node(1), Some(session(100))).unwrap();
    assert_eq!(tree, before, "source tree must not be mutated");
}

#[test]
fn set_session_reaches_nested_leaf() {
    // Attach onto the second leaf of a split, leaving the first empty.
    let tree = two_leaves(None, None);
    let out = tree.set_session(node(2), Some(session(7))).unwrap();
    match out.root {
        LayoutNode::Split(s) => match &s.children[1].node {
            LayoutNode::Leaf(l) => assert_eq!(l.session, Some(session(7))),
            _ => panic!("expected a leaf child"),
        },
        _ => panic!("expected a split root"),
    }
}

#[test]
fn set_session_missing_leaf_is_node_not_found() {
    let tree = single(1, None);
    let err = tree.set_session(node(404), Some(session(100))).unwrap_err();
    assert_eq!(err, LayoutError::NodeNotFound(node(404)));
}

#[test]
fn set_session_duplicate_across_leaves_rejected() {
    // Leaf 1 already hosts session 100; attaching the same session to leaf 2
    // must fail validation rather than producing a duplicate.
    let tree = two_leaves(Some(100), None);
    let err = tree.set_session(node(2), Some(session(100))).unwrap_err();
    assert_eq!(err, LayoutError::DuplicateSession(session(100)));
}

#[test]
fn attach_session_attaches_background_session_to_leaf() {
    let tree = two_leaves(None, None);
    let out = tree.attach_session(node(2), session(100)).unwrap();
    assert_eq!(session_for(&out, node(1)), None);
    assert_eq!(session_for(&out, node(2)), Some(session(100)));
}

#[test]
fn attach_session_moves_visible_session_to_target_leaf() {
    let tree = two_leaves(Some(100), None);
    let out = tree.attach_session(node(2), session(100)).unwrap();
    assert_eq!(session_for(&out, node(1)), None);
    assert_eq!(session_for(&out, node(2)), Some(session(100)));
}

#[test]
fn attach_session_same_leaf_is_idempotent() {
    let tree = two_leaves(Some(100), None);
    let out = tree.attach_session(node(1), session(100)).unwrap();
    assert_eq!(out, tree);
}

#[test]
fn attach_session_rejects_target_occupied_by_other_session() {
    let tree = two_leaves(Some(100), Some(200));
    let err = tree.attach_session(node(2), session(100)).unwrap_err();
    assert_eq!(err, LayoutError::CrossContainer);
}

// ---------------------------------------------------------------------------
// set_cell_agent (#3: per-cell agent)
// ---------------------------------------------------------------------------

#[test]
fn set_cell_agent_attaches_agent_to_leaf() {
    let tree = single(1, None);
    let out = tree.set_cell_agent(node(1), Some(agent_id(42))).unwrap();
    match out.root {
        LayoutNode::Leaf(l) => {
            assert_eq!(l.id, node(1));
            assert_eq!(l.agent, Some(agent_id(42)));
        }
        _ => panic!("expected a leaf at root"),
    }
}

#[test]
fn set_cell_agent_detaches_with_none() {
    // First attach, then detach.
    let tree = single(1, None);
    let with_agent = tree.set_cell_agent(node(1), Some(agent_id(42))).unwrap();
    let out = with_agent.set_cell_agent(node(1), None).unwrap();
    match out.root {
        LayoutNode::Leaf(l) => assert_eq!(l.agent, None),
        _ => panic!("expected a leaf at root"),
    }
}

#[test]
fn set_cell_agent_is_immutable_source_unchanged() {
    let tree = single(1, None);
    let before = tree.clone();
    let _ = tree.set_cell_agent(node(1), Some(agent_id(99))).unwrap();
    assert_eq!(tree, before, "source tree must not be mutated");
}

#[test]
fn set_cell_agent_missing_leaf_is_node_not_found() {
    let tree = single(1, None);
    let err = tree
        .set_cell_agent(node(404), Some(agent_id(1)))
        .unwrap_err();
    assert_eq!(err, LayoutError::NodeNotFound(node(404)));
}

#[test]
fn set_cell_agent_preserves_session() {
    // Session must survive an agent attachment.
    let tree = single(1, Some(100));
    let out = tree.set_cell_agent(node(1), Some(agent_id(7))).unwrap();
    match out.root {
        LayoutNode::Leaf(l) => {
            assert_eq!(l.session, Some(session(100)));
            assert_eq!(l.agent, Some(agent_id(7)));
        }
        _ => panic!("expected leaf"),
    }
}

// ---------------------------------------------------------------------------
// set_cell_conversation (resume: persistent CLI conversation id)
// ---------------------------------------------------------------------------

#[test]
fn set_cell_conversation_attaches_to_leaf() {
    let tree = single(1, None);
    let out = tree
        .set_cell_conversation(node(1), Some("conv-xyz".to_string()))
        .unwrap();
    let l = leaf_for(&out, node(1)).expect("leaf");
    assert_eq!(l.conversation_id, Some("conv-xyz".to_string()));
}

#[test]
fn set_cell_conversation_clears_with_none() {
    let tree = LayoutTree::single(leaf_with_resume(1, None));
    let out = tree.set_cell_conversation(node(1), None).unwrap();
    let l = leaf_for(&out, node(1)).expect("leaf");
    assert_eq!(l.conversation_id, None);
}

#[test]
fn set_cell_conversation_targets_correct_leaf() {
    // Two leaves; only leaf 2 should receive the conversation id.
    let tree = two_leaves(None, None);
    let out = tree
        .set_cell_conversation(node(2), Some("conv-2".to_string()))
        .unwrap();
    assert_eq!(
        leaf_for(&out, node(1)).unwrap().conversation_id,
        None,
        "untouched leaf must stay clear"
    );
    assert_eq!(
        leaf_for(&out, node(2)).unwrap().conversation_id,
        Some("conv-2".to_string())
    );
}

#[test]
fn set_cell_conversation_is_immutable_source_unchanged() {
    let tree = single(1, None);
    let before = tree.clone();
    let _ = tree
        .set_cell_conversation(node(1), Some("conv-1".to_string()))
        .unwrap();
    assert_eq!(tree, before, "source tree must not be mutated");
}

#[test]
fn set_cell_conversation_missing_leaf_is_node_not_found() {
    let tree = single(1, None);
    let err = tree
        .set_cell_conversation(node(404), Some("conv".to_string()))
        .unwrap_err();
    assert_eq!(err, LayoutError::NodeNotFound(node(404)));
}

#[test]
fn set_cell_conversation_preserves_session_and_agent_running() {
    // conversation id must coexist with session and agent_was_running.
    let tree = LayoutTree::single(leaf_with_resume(1, Some(100)));
    let out = tree
        .set_cell_conversation(node(1), Some("conv-new".to_string()))
        .unwrap();
    let l = leaf_for(&out, node(1)).expect("leaf");
    assert_eq!(l.session, Some(session(100)));
    assert_eq!(l.agent_was_running, true);
    assert_eq!(l.conversation_id, Some("conv-new".to_string()));
}

// ---------------------------------------------------------------------------
// set_agent_running (resume: agent process state at close)
// ---------------------------------------------------------------------------

#[test]
fn set_agent_running_sets_true_then_false() {
    let tree = single(1, None);
    let out = tree.set_agent_running(node(1), true).unwrap();
    assert_eq!(leaf_for(&out, node(1)).unwrap().agent_was_running, true);

    let out2 = out.set_agent_running(node(1), false).unwrap();
    assert_eq!(leaf_for(&out2, node(1)).unwrap().agent_was_running, false);
}

#[test]
fn set_agent_running_targets_correct_leaf() {
    let tree = two_leaves(None, None);
    let out = tree.set_agent_running(node(2), true).unwrap();
    assert_eq!(
        leaf_for(&out, node(1)).unwrap().agent_was_running,
        false,
        "untouched leaf must stay false"
    );
    assert_eq!(leaf_for(&out, node(2)).unwrap().agent_was_running, true);
}

// ---------------------------------------------------------------------------
// agent_leaves (close-time snapshot traversal)
// ---------------------------------------------------------------------------

#[test]
fn agent_leaves_empty_when_no_agent() {
    let tree = two_leaves(Some(1), None);
    assert!(tree.agent_leaves().is_empty());
}

#[test]
fn agent_leaves_collects_only_agent_bearing_leaves() {
    // Split: leaf 1 with agent 100, leaf 2 plain, leaf 3 with agent 101.
    let tree = LayoutTree::new(LayoutNode::Split(SplitContainer {
        id: node(99),
        direction: Direction::Row,
        children: vec![
            WeightedChild {
                node: LayoutNode::Leaf(LeafCell {
                    id: node(1),
                    session: None,
                    agent: Some(agent_id(100)),
                    conversation_id: None,
                    engine_session_id: None,
                    agent_was_running: false,
                }),
                weight: 1.0,
            },
            WeightedChild {
                node: LayoutNode::Leaf(leaf(2, None)),
                weight: 1.0,
            },
            WeightedChild {
                node: LayoutNode::Leaf(LeafCell {
                    id: node(3),
                    session: None,
                    agent: Some(agent_id(101)),
                    conversation_id: None,
                    engine_session_id: None,
                    agent_was_running: false,
                }),
                weight: 1.0,
            },
        ],
    }));

    let mut found = tree.agent_leaves();
    found.sort_by_key(|(n, _)| *n);
    assert_eq!(
        found,
        vec![(node(1), agent_id(100)), (node(3), agent_id(101))]
    );
}

#[test]
fn set_agent_running_is_immutable_source_unchanged() {
    let tree = single(1, None);
    let before = tree.clone();
    let _ = tree.set_agent_running(node(1), true).unwrap();
    assert_eq!(tree, before, "source tree must not be mutated");
}

#[test]
fn set_agent_running_missing_leaf_is_node_not_found() {
    let tree = single(1, None);
    let err = tree.set_agent_running(node(404), true).unwrap_err();
    assert_eq!(err, LayoutError::NodeNotFound(node(404)));
}

#[test]
fn set_agent_running_preserves_session_and_conversation() {
    let tree = LayoutTree::single(leaf_with_resume(1, Some(100)));
    let out = tree.set_agent_running(node(1), false).unwrap();
    let l = leaf_for(&out, node(1)).expect("leaf");
    assert_eq!(l.session, Some(session(100)));
    assert_eq!(l.conversation_id, Some("conv-1".to_string()));
    assert_eq!(l.agent_was_running, false);
}

// ---------------------------------------------------------------------------
// NON-REGRESSION (piège n°1): the resume fields (conversation_id,
// agent_was_running) MUST survive every leaf-rebuilding operation.
// ---------------------------------------------------------------------------

#[test]
fn set_session_preserves_resume_fields() {
    // leaf starts with conversation_id = Some("conv-1") and agent_was_running = true.
    let tree = LayoutTree::single(leaf_with_resume(1, None));
    let out = tree.set_session(node(1), Some(session(100))).unwrap();
    let l = leaf_for(&out, node(1)).expect("leaf");
    assert_eq!(l.session, Some(session(100)));
    assert_eq!(
        l.conversation_id,
        Some("conv-1".to_string()),
        "set_session must NOT erase conversation_id"
    );
    assert_eq!(
        l.agent_was_running, true,
        "set_session must NOT erase agent_was_running"
    );
}

#[test]
fn set_cell_agent_preserves_resume_fields() {
    let tree = LayoutTree::single(leaf_with_resume(1, None));
    let out = tree.set_cell_agent(node(1), Some(agent_id(42))).unwrap();
    let l = leaf_for(&out, node(1)).expect("leaf");
    assert_eq!(l.agent, Some(agent_id(42)));
    assert_eq!(
        l.conversation_id,
        Some("conv-1".to_string()),
        "set_cell_agent must NOT erase conversation_id"
    );
    assert_eq!(
        l.agent_was_running, true,
        "set_cell_agent must NOT erase agent_was_running"
    );
}

#[test]
fn move_session_preserves_resume_fields_on_both_leaves() {
    // from-leaf carries a session + resume fields; to-leaf is empty but ALSO
    // carries resume fields. After the move, BOTH leaves must keep their own
    // conversation_id / agent_was_running (only the session moves).
    let tree = LayoutTree::new(LayoutNode::Split(SplitContainer {
        id: node(9),
        direction: Direction::Row,
        children: vec![
            WeightedChild {
                node: LayoutNode::Leaf(LeafCell {
                    id: node(1),
                    session: Some(session(100)),
                    agent: None,
                    conversation_id: Some("conv-from".to_string()),
                    engine_session_id: None,
                    agent_was_running: true,
                }),
                weight: 1.0,
            },
            WeightedChild {
                node: LayoutNode::Leaf(LeafCell {
                    id: node(2),
                    session: None,
                    agent: None,
                    conversation_id: Some("conv-to".to_string()),
                    engine_session_id: None,
                    agent_was_running: true,
                }),
                weight: 1.0,
            },
        ],
    }));
    let out = tree.move_session(node(1), node(2)).unwrap();

    let from = leaf_for(&out, node(1)).expect("from leaf");
    assert_eq!(from.session, None, "session left the from-leaf");
    assert_eq!(
        from.conversation_id,
        Some("conv-from".to_string()),
        "move_session must NOT erase from-leaf conversation_id"
    );
    assert_eq!(
        from.agent_was_running, true,
        "move_session must NOT erase from-leaf agent_was_running"
    );

    let to = leaf_for(&out, node(2)).expect("to leaf");
    assert_eq!(to.session, Some(session(100)), "session arrived at to-leaf");
    assert_eq!(
        to.conversation_id,
        Some("conv-to".to_string()),
        "move_session must NOT erase to-leaf conversation_id"
    );
    assert_eq!(
        to.agent_was_running, true,
        "move_session must NOT erase to-leaf agent_was_running"
    );
}

// ---------------------------------------------------------------------------
// reconcile_duplicate_agents (R0c: dé-doublonnage à l'ouverture)
// ---------------------------------------------------------------------------

/// Builds an agent-bearing leaf with explicit resume signals.
fn agent_leaf_full(id: u128, agent: u128, conv: Option<&str>, running: bool) -> LeafCell {
    LeafCell {
        id: node(id),
        session: None,
        agent: Some(agent_id(agent)),
        conversation_id: conv.map(str::to_string),
        engine_session_id: None,
        agent_was_running: running,
    }
}

/// Wraps an ordered list of leaves into a single Row split (pre-order = the
/// children order, same traversal as `agent_leaves`).
fn split_of(leaves: Vec<LeafCell>) -> LayoutTree {
    LayoutTree::new(LayoutNode::Split(SplitContainer {
        id: node(900),
        direction: Direction::Row,
        children: leaves
            .into_iter()
            .map(|l| WeightedChild {
                node: LayoutNode::Leaf(l),
                weight: 1.0,
            })
            .collect(),
    }))
}

/// 1. Two leaves of the SAME agent, both `agent_was_running = true`: after
/// reconciliation exactly ONE stays running; the other is neutralised
/// (running=false, conversation_id=None) but the leaf/agent SURVIVES.
#[test]
fn reconcile_basic_duplicate_keeps_single_host() {
    let tree = split_of(vec![
        agent_leaf_full(1, 7, None, true),
        agent_leaf_full(2, 7, None, true),
    ]);
    let out = tree.reconcile_duplicate_agents();

    // First in pre-order is the host (no resume signal differentiates them).
    let host = leaf_for(&out, node(1)).expect("leaf 1");
    let other = leaf_for(&out, node(2)).expect("leaf 2");

    assert_eq!(host.agent_was_running, true, "host stays running");
    assert_eq!(other.agent_was_running, false, "duplicate neutralised");
    assert_eq!(other.conversation_id, None);

    // The leaf and its agent binding still exist (cell not removed).
    assert_eq!(host.agent, Some(agent_id(7)));
    assert_eq!(other.agent, Some(agent_id(7)));

    // Exactly one running leaf for the agent.
    let running = [&host, &other]
        .iter()
        .filter(|l| l.agent_was_running)
        .count();
    assert_eq!(running, 1);
}

/// 2a. Host chosen by resume signal: the FIRST leaf carries no signal, the
/// SECOND carries one (conversation_id). The second must become the host; the
/// first (no signal) is left untouched (already neutral).
#[test]
fn reconcile_host_is_first_leaf_carrying_resume_signal() {
    let tree = split_of(vec![
        agent_leaf_full(1, 7, None, false), // no signal, first in order
        agent_leaf_full(2, 7, Some("conv-2"), false), // signal → becomes host
    ]);
    let out = tree.reconcile_duplicate_agents();

    let first = leaf_for(&out, node(1)).expect("leaf 1");
    let second = leaf_for(&out, node(2)).expect("leaf 2");

    assert_eq!(
        second.conversation_id,
        Some("conv-2".to_string()),
        "the signal-bearing leaf is kept as host"
    );
    // The first leaf has no signal to strip; stays neutral.
    assert_eq!(first.conversation_id, None);
    assert_eq!(first.agent_was_running, false);
}

/// 2b. When NO leaf carries a signal, the host is simply the first encountered.
/// Here both are inert; reconciliation must leave them unchanged (nothing to
/// strip), and not invent a running flag.
#[test]
fn reconcile_no_signal_host_is_first_and_tree_unchanged() {
    let tree = split_of(vec![
        agent_leaf_full(1, 7, None, false),
        agent_leaf_full(2, 7, None, false),
    ]);
    let out = tree.reconcile_duplicate_agents();
    assert_eq!(out, tree, "no resume signal anywhere → nothing to strip");
}

/// 2c. The signal-bearing second leaf wins even against a first leaf that has a
/// signal too? No — the FIRST signal-bearer wins. Verify the first running leaf
/// stays host and the second (also running) is neutralised.
#[test]
fn reconcile_first_signal_bearer_wins_over_later_signal() {
    let tree = split_of(vec![
        agent_leaf_full(1, 7, Some("conv-1"), true), // first signal → host
        agent_leaf_full(2, 7, Some("conv-2"), true),
    ]);
    let out = tree.reconcile_duplicate_agents();

    let host = leaf_for(&out, node(1)).expect("leaf 1");
    let other = leaf_for(&out, node(2)).expect("leaf 2");

    assert_eq!(host.conversation_id, Some("conv-1".to_string()));
    assert_eq!(host.agent_was_running, true);
    assert_eq!(other.conversation_id, None, "later duplicate stripped");
    assert_eq!(other.agent_was_running, false);
}

/// 3. Triplet (N=3) of the same agent: exactly one host, two neutralised.
#[test]
fn reconcile_triplet_one_host_two_neutralised() {
    let tree = split_of(vec![
        agent_leaf_full(1, 7, None, true),
        agent_leaf_full(2, 7, None, true),
        agent_leaf_full(3, 7, Some("c3"), true),
    ]);
    let out = tree.reconcile_duplicate_agents();

    let l1 = leaf_for(&out, node(1)).unwrap();
    let l2 = leaf_for(&out, node(2)).unwrap();
    let l3 = leaf_for(&out, node(3)).unwrap();

    // First signal-bearer in pre-order is leaf 1 (running=true is a signal).
    let running_count = [&l1, &l2, &l3]
        .iter()
        .filter(|l| l.agent_was_running)
        .count();
    assert_eq!(running_count, 1, "exactly one host remains running");
    assert_eq!(l1.agent_was_running, true, "leaf 1 is the host");
    assert_eq!(l2.agent_was_running, false);
    assert_eq!(l3.agent_was_running, false);
    assert_eq!(l3.conversation_id, None, "duplicate conv stripped");
    // All three cells survive.
    for l in [&l1, &l2, &l3] {
        assert_eq!(l.agent, Some(agent_id(7)));
    }
}

/// 4. Distinct agents are never affected: two leaves with DIFFERENT agents,
/// each running, must both stay running.
#[test]
fn reconcile_distinct_agents_unaffected() {
    let tree = split_of(vec![
        agent_leaf_full(1, 7, Some("c1"), true),
        agent_leaf_full(2, 8, Some("c2"), true),
    ]);
    let out = tree.reconcile_duplicate_agents();
    assert_eq!(out, tree, "different agents → no duplicates → unchanged");
}

/// 5a. Idempotence (fixed point): reconcile(reconcile(t)) == reconcile(t).
#[test]
fn reconcile_is_idempotent_fixed_point() {
    let tree = split_of(vec![
        agent_leaf_full(1, 7, Some("c1"), true),
        agent_leaf_full(2, 7, Some("c2"), true),
        agent_leaf_full(3, 7, None, true),
    ]);
    let once = tree.reconcile_duplicate_agents();
    let twice = once.reconcile_duplicate_agents();
    assert_eq!(twice, once, "second pass is a no-op (fixed point)");
}

/// 5b. A tree WITHOUT duplicates is returned unchanged (== t).
#[test]
fn reconcile_no_duplicate_returns_identical() {
    let tree = split_of(vec![
        agent_leaf_full(1, 7, Some("c1"), true),
        agent_leaf_full(2, 8, None, false),
        leaf(3, Some(100)), // plain non-agent leaf
    ]);
    let out = tree.reconcile_duplicate_agents();
    assert_eq!(out, tree, "no duplicate agent → identical tree");
}

/// Source immutability: reconciliation must not mutate the input tree.
#[test]
fn reconcile_is_immutable_source_unchanged() {
    let tree = split_of(vec![
        agent_leaf_full(1, 7, None, true),
        agent_leaf_full(2, 7, None, true),
    ]);
    let before = tree.clone();
    let _ = tree.reconcile_duplicate_agents();
    assert_eq!(tree, before, "source tree must not be mutated");
}

// ---------------------------------------------------------------------------
// serde: compat ascendante & combinaisons des nouveaux champs
// ---------------------------------------------------------------------------

#[test]
fn leaf_serde_all_four_combinations_roundtrip() {
    let combos = [
        (None, false),
        (Some("c".to_string()), false),
        (None, true),
        (Some("c".to_string()), true),
    ];
    for (conv, running) in combos {
        let cell = LeafCell {
            id: node(1),
            session: None,
            agent: None,
            conversation_id: conv.clone(),
            engine_session_id: None,
            agent_was_running: running,
        };
        let json = serde_json::to_string(&cell).unwrap();
        let back: LeafCell = serde_json::from_str(&json).unwrap();
        assert_eq!(back, cell, "roundtrip failed for {conv:?}/{running}");
    }
}

#[test]
fn leaf_serde_omits_defaults() {
    let cell = LeafCell {
        id: node(1),
        session: None,
        agent: None,
        conversation_id: None,
        engine_session_id: None,
        agent_was_running: false,
    };
    let json = serde_json::to_string(&cell).unwrap();
    assert!(
        !json.contains("conversationId"),
        "default conversation_id must be omitted; json was {json}"
    );
    assert!(
        !json.contains("agentWasRunning"),
        "default agent_was_running must be omitted; json was {json}"
    );
}

#[test]
fn leaf_serde_field_names_are_camel_case_when_present() {
    let cell = LeafCell {
        id: node(1),
        session: None,
        agent: None,
        conversation_id: Some("c".to_string()),
        engine_session_id: None,
        agent_was_running: true,
    };
    let json = serde_json::to_string(&cell).unwrap();
    assert!(json.contains("conversationId"), "json was {json}");
    assert!(json.contains("agentWasRunning"), "json was {json}");
}

#[test]
fn legacy_leaf_json_deserialises_to_defaults() {
    // A leaf persisted before these fields existed (only id present).
    let json = r#"{"id":"00000000-0000-0000-0000-000000000001"}"#;
    let cell: LeafCell = serde_json::from_str(json).unwrap();
    assert_eq!(cell.conversation_id, None);
    assert_eq!(cell.agent_was_running, false);
    assert_eq!(cell.session, None);
    assert_eq!(cell.agent, None);
}

#[test]
fn leaf_can_carry_conversation_without_session_and_inversely() {
    // conversation_id present, no session.
    let a = LeafCell {
        id: node(1),
        session: None,
        agent: None,
        conversation_id: Some("c".to_string()),
        engine_session_id: None,
        agent_was_running: false,
    };
    let a_back: LeafCell = serde_json::from_str(&serde_json::to_string(&a).unwrap()).unwrap();
    assert_eq!(a_back, a);

    // session present, no conversation_id.
    let b = LeafCell {
        id: node(2),
        session: Some(session(7)),
        agent: None,
        conversation_id: None,
        engine_session_id: None,
        agent_was_running: false,
    };
    let b_back: LeafCell = serde_json::from_str(&serde_json::to_string(&b).unwrap()).unwrap();
    assert_eq!(b_back, b);
}
