//! Wire codec, broadcast, and end-to-end convergence tests for the CRDT sync server.

use super::super::wire::SyncMessage;

#[test]
fn peer_receives_op_encoded_via_wire_codec() {
    use bft_json_crdt::json_crdt::BaseCrdt;
    use bft_json_crdt::keypair::make_keypair;
    use bft_json_crdt::op::ROOT_ID;
    use serde_json::json;

    use crate::crdt_state::PipelineDoc;
    use crate::crdt_wire;

    let kp = make_keypair();
    let mut crdt = BaseCrdt::<PipelineDoc>::new(&kp);
    let item: bft_json_crdt::json_crdt::JsonValue = json!({
        "story_id": "506_story_lifecycle_test",
        "stage": "1_backlog",
        "name": "Lifecycle Test",
        "agent": "",
        "retry_count": 0.0,
        "blocked": false,
        "depends_on": "",
        "claimed_by": "",
        "claimed_at": 0.0,
    })
    .into();
    let op = crdt.doc.items.insert(ROOT_ID, item).sign(&kp);

    // Simulate what the broadcast handler does: encode via wire codec.
    let bytes = crdt_wire::encode(&op);

    // The bytes must be a versioned JSON envelope, not a SyncMessage wrapper.
    let text = std::str::from_utf8(&bytes).expect("wire output is valid UTF-8");
    assert!(
        text.contains("\"v\":1"),
        "wire codec version tag must be present: {text}"
    );
    assert!(
        !text.contains("\"type\":\"op\""),
        "must not be wrapped in SyncMessage: {text}"
    );

    // The receiving peer can decode and apply the op.
    let decoded = crdt_wire::decode(&bytes).expect("decode must succeed");
    assert_eq!(op, decoded);
}

#[tokio::test]
async fn multiple_peers_all_receive_broadcast_op() {
    use bft_json_crdt::json_crdt::BaseCrdt;
    use bft_json_crdt::json_crdt::SignedOp;
    use bft_json_crdt::keypair::make_keypair;
    use bft_json_crdt::op::ROOT_ID;
    use serde_json::json;
    use tokio::sync::broadcast;

    use crate::crdt_state::PipelineDoc;
    use crate::crdt_wire;

    // Create a broadcast channel (analogous to SYNC_TX).
    let (tx, _) = broadcast::channel::<SignedOp>(16);
    let mut rx_peer1 = tx.subscribe();
    let mut rx_peer2 = tx.subscribe();

    let kp = make_keypair();
    let mut crdt = BaseCrdt::<PipelineDoc>::new(&kp);
    let item: bft_json_crdt::json_crdt::JsonValue = json!({
        "story_id": "506_story_multi_peer_test",
        "stage": "1_backlog",
        "name": "Multi-Peer Test",
        "agent": "",
        "retry_count": 0.0,
        "blocked": false,
        "depends_on": "",
        "claimed_by": "",
        "claimed_at": 0.0,
    })
    .into();
    let op = crdt.doc.items.insert(ROOT_ID, item).sign(&kp);

    // Broadcast one op.
    tx.send(op.clone()).expect("send must succeed");

    // Both peers receive the same op.
    let received1 = rx_peer1.recv().await.expect("peer 1 must receive");
    let received2 = rx_peer2.recv().await.expect("peer 2 must receive");
    assert_eq!(received1, op);
    assert_eq!(received2, op);

    // Both encode identically via wire codec.
    let bytes1 = crdt_wire::encode(&received1);
    let bytes2 = crdt_wire::encode(&received2);
    assert_eq!(bytes1, bytes2, "wire-encoded bytes must be identical");
}

#[test]
fn disconnected_peer_does_not_panic() {
    use bft_json_crdt::json_crdt::BaseCrdt;
    use bft_json_crdt::json_crdt::SignedOp;
    use bft_json_crdt::keypair::make_keypair;
    use bft_json_crdt::op::ROOT_ID;
    use serde_json::json;
    use tokio::sync::broadcast;

    use crate::crdt_state::PipelineDoc;

    let (tx, rx) = broadcast::channel::<SignedOp>(16);
    // Drop the receiver to simulate a peer that disconnected.
    drop(rx);

    let kp = make_keypair();
    let mut crdt = BaseCrdt::<PipelineDoc>::new(&kp);
    let item: bft_json_crdt::json_crdt::JsonValue = json!({
        "story_id": "506_story_disconnect_test",
        "stage": "1_backlog",
        "name": "Disconnect Test",
        "agent": "",
        "retry_count": 0.0,
        "blocked": false,
        "depends_on": "",
        "claimed_by": "",
        "claimed_at": 0.0,
    })
    .into();
    let op = crdt.doc.items.insert(ROOT_ID, item).sign(&kp);

    // Sending to a channel with no receivers returns an error; must not panic.
    let _ = tx.send(op);
}

#[tokio::test]
async fn lagged_peer_gets_lagged_error() {
    use bft_json_crdt::json_crdt::BaseCrdt;
    use bft_json_crdt::json_crdt::SignedOp;
    use bft_json_crdt::keypair::make_keypair;
    use bft_json_crdt::op::ROOT_ID;
    use serde_json::json;
    use tokio::sync::broadcast;

    use crate::crdt_state::PipelineDoc;

    // Tiny capacity so we can trigger Lagged easily.
    let (tx, mut rx) = broadcast::channel::<SignedOp>(2);

    let kp = make_keypair();
    let mut crdt = BaseCrdt::<PipelineDoc>::new(&kp);
    let item: bft_json_crdt::json_crdt::JsonValue = json!({
        "story_id": "506_story_lag_test",
        "stage": "1_backlog",
        "name": "Lag Test",
        "agent": "",
        "retry_count": 0.0,
        "blocked": false,
        "depends_on": "",
        "claimed_by": "",
        "claimed_at": 0.0,
    })
    .into();
    let op1 = crdt.doc.items.insert(ROOT_ID, item).sign(&kp);
    crdt.apply(op1.clone());

    // Overflow the tiny buffer by sending more ops than the capacity.
    let op2 = crdt.doc.items[0]
        .stage
        .set("2_current".to_string())
        .sign(&kp);
    crdt.apply(op2.clone());
    let op3 = crdt.doc.items[0].stage.set("3_qa".to_string()).sign(&kp);
    crdt.apply(op3.clone());
    let op4 = crdt.doc.items[0].stage.set("4_merge".to_string()).sign(&kp);
    crdt.apply(op4.clone());

    // Send more ops than the channel capacity without consuming.
    let _ = tx.send(op1);
    let _ = tx.send(op2);
    let _ = tx.send(op3);
    let _ = tx.send(op4);

    // The slow peer should now see a Lagged error on next recv.
    // Consume until we hit Lagged or run out.
    let mut got_lagged = false;
    for _ in 0..10 {
        match rx.recv().await {
            Err(broadcast::error::RecvError::Lagged(_)) => {
                got_lagged = true;
                break;
            }
            Ok(_) => continue,
            Err(broadcast::error::RecvError::Closed) => break,
        }
    }
    assert!(
        got_lagged,
        "slow peer must receive a Lagged error when channel overflows"
    );
}

#[tokio::test]
async fn e2e_convergence_two_websocket_nodes() {
    use bft_json_crdt::json_crdt::{BaseCrdt, CrdtNode, JsonValue as JV, OpState};
    use bft_json_crdt::keypair::make_keypair;
    use bft_json_crdt::op::ROOT_ID;
    use futures::{SinkExt, StreamExt};
    use serde_json::json;
    use std::sync::{Arc, Mutex};
    use tokio::net::TcpListener;
    use tokio_tungstenite::tungstenite::Message as TMsg;
    use tokio_tungstenite::{accept_async, connect_async};

    use crate::crdt_state::PipelineDoc;

    // ── Node A: build local state ──────────────────────────────────────
    let kp_a = make_keypair();
    let mut crdt_a = BaseCrdt::<PipelineDoc>::new(&kp_a);

    let item: bft_json_crdt::json_crdt::JsonValue = json!({
        "story_id": "508_e2e_convergence",
        "stage": "2_current",
        "name": "E2E Convergence Test",
        "agent": "",
        "retry_count": 0.0,
        "blocked": false,
        "depends_on": "",
        "claimed_by": "",
        "claimed_at": 0.0,
    })
    .into();
    let op1 = crdt_a.doc.items.insert(ROOT_ID, item).sign(&kp_a);
    crdt_a.apply(op1.clone());

    // Serialise A's full state as a bulk message.
    let op1_json = serde_json::to_string(&op1).unwrap();
    let bulk_msg = SyncMessage::Bulk {
        ops: vec![op1_json],
    };
    let bulk_wire = serde_json::to_string(&bulk_msg).unwrap();

    // ── Start Node A's WebSocket server on a random port ───────────────
    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
    let addr = listener.local_addr().unwrap();

    let bulk_to_send = bulk_wire.clone();
    let received_by_a: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![]));
    let received_by_a_clone = received_by_a.clone();

    tokio::spawn(async move {
        let (tcp_stream, _) = listener.accept().await.unwrap();
        let ws_stream = accept_async(tcp_stream).await.unwrap();
        let (mut sink, mut stream) = ws_stream.split();

        // Send bulk state to the connecting peer.
        sink.send(TMsg::Text(bulk_to_send.into())).await.unwrap();

        // Also listen for ops sent by the peer.
        if let Some(Ok(TMsg::Text(txt))) = stream.next().await {
            received_by_a_clone.lock().unwrap().push(txt.to_string());
        }
    });

    // ── Node B: connect to Node A and exchange state ───────────────────
    let kp_b = make_keypair();
    let mut crdt_b = BaseCrdt::<PipelineDoc>::new(&kp_b);

    let url = format!("ws://{addr}");
    let (ws_b, _) = connect_async(&url).await.unwrap();
    let (mut sink_b, mut stream_b) = ws_b.split();

    // Node B receives bulk from A.
    if let Some(Ok(TMsg::Text(txt))) = stream_b.next().await {
        let msg: SyncMessage = serde_json::from_str(txt.as_str()).unwrap();
        match msg {
            SyncMessage::Bulk { ops } => {
                for op_str in &ops {
                    let signed: bft_json_crdt::json_crdt::SignedOp =
                        serde_json::from_str(op_str).unwrap();
                    let r = crdt_b.apply(signed);
                    assert!(r == OpState::Ok || r == OpState::AlreadySeen);
                }
            }
            _ => panic!("Expected Bulk from Node A"),
        }
    }

    // Node B also creates a new op and sends it to A.
    let item_b: bft_json_crdt::json_crdt::JsonValue = json!({
        "story_id": "508_e2e_convergence_b",
        "stage": "1_backlog",
        "name": "E2E Convergence B",
        "agent": "",
        "retry_count": 0.0,
        "blocked": false,
        "depends_on": "",
        "claimed_by": "",
        "claimed_at": 0.0,
    })
    .into();
    let op_b1 = crdt_b.doc.items.insert(ROOT_ID, item_b).sign(&kp_b);
    crdt_b.apply(op_b1.clone());

    let op_b1_json = serde_json::to_string(&op_b1).unwrap();
    let msg_to_a = SyncMessage::Op { op: op_b1_json };
    sink_b
        .send(TMsg::Text(serde_json::to_string(&msg_to_a).unwrap().into()))
        .await
        .unwrap();

    // Wait a moment for Node A to process.
    tokio::time::sleep(std::time::Duration::from_millis(50)).await;

    // ── Assert convergence ─────────────────────────────────────────────

    // Node B received Node A's item.
    assert_eq!(
        crdt_b.doc.items.view().len(),
        2,
        "Node B must see both items after sync"
    );
    let has_a_item = crdt_b
        .doc
        .items
        .view()
        .iter()
        .any(|item| item.story_id.view() == JV::String("508_e2e_convergence".to_string()));
    assert!(has_a_item, "Node B must have Node A's item");

    // Node A received Node B's op via the WebSocket.
    let a_received = received_by_a.lock().unwrap();
    assert!(
        !a_received.is_empty(),
        "Node A must have received an op from Node B"
    );
}

#[tokio::test]
async fn e2e_partition_healing_websocket() {
    use bft_json_crdt::json_crdt::{BaseCrdt, CrdtNode, JsonValue as JV};
    use bft_json_crdt::keypair::make_keypair;
    use bft_json_crdt::op::ROOT_ID;
    use futures::{SinkExt, StreamExt};
    use serde_json::json;
    use tokio::net::TcpListener;
    use tokio_tungstenite::tungstenite::Message as TMsg;
    use tokio_tungstenite::{accept_async, connect_async};

    use crate::crdt_state::PipelineDoc;

    // ── Phase 1: Both nodes start with op_a1 (before partition) ───────
    let kp_a = make_keypair();
    let kp_b = make_keypair();
    let mut crdt_a = BaseCrdt::<PipelineDoc>::new(&kp_a);
    let mut crdt_b = BaseCrdt::<PipelineDoc>::new(&kp_b);

    let item_a: bft_json_crdt::json_crdt::JsonValue = json!({
        "story_id": "508_heal_a",
        "stage": "1_backlog",
        "name": "Heal A",
        "agent": "",
        "retry_count": 0.0,
        "blocked": false,
        "depends_on": "",
        "claimed_by": "",
        "claimed_at": 0.0,
    })
    .into();
    let op_a1 = crdt_a.doc.items.insert(ROOT_ID, item_a).sign(&kp_a);
    crdt_a.apply(op_a1.clone());
    // B also starts with op_a1 (shared state before partition).
    crdt_b.apply(op_a1.clone());

    // ── Phase 2: Partition — each side mutates independently ──────────
    // A advances its story stage.
    let op_a2 = crdt_a.doc.items[0]
        .stage
        .set("2_current".to_string())
        .sign(&kp_a);
    crdt_a.apply(op_a2.clone());

    // B inserts a new story that A doesn't know about yet.
    let item_b: bft_json_crdt::json_crdt::JsonValue = json!({
        "story_id": "508_heal_b",
        "stage": "1_backlog",
        "name": "Heal B",
        "agent": "",
        "retry_count": 0.0,
        "blocked": false,
        "depends_on": "",
        "claimed_by": "",
        "claimed_at": 0.0,
    })
    .into();
    let op_b1 = crdt_b.doc.items.insert(ROOT_ID, item_b).sign(&kp_b);
    crdt_b.apply(op_b1.clone());

    // Collect B's full state as bulk (what it will send on reconnect).
    let b_ops: Vec<String> = [&op_a1, &op_b1]
        .iter()
        .map(|op| serde_json::to_string(op).unwrap())
        .collect();
    let b_bulk_wire = serde_json::to_string(&SyncMessage::Bulk { ops: b_ops }).unwrap();

    // Collect A's full state as bulk (what it will send on reconnect).
    let a_ops: Vec<String> = [&op_a1, &op_a2]
        .iter()
        .map(|op| serde_json::to_string(op).unwrap())
        .collect();
    let a_bulk_wire = serde_json::to_string(&SyncMessage::Bulk { ops: a_ops }).unwrap();

    // ── Phase 3: Reconnect — use a real WebSocket to exchange bulk ────
    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
    let addr = listener.local_addr().unwrap();

    let a_bulk_to_send = a_bulk_wire.clone();
    let a_received_bulk: std::sync::Arc<std::sync::Mutex<Option<String>>> =
        std::sync::Arc::new(std::sync::Mutex::new(None));
    let a_received_clone = a_received_bulk.clone();

    tokio::spawn(async move {
        let (tcp, _) = listener.accept().await.unwrap();
        let ws = accept_async(tcp).await.unwrap();
        let (mut sink, mut stream) = ws.split();
        // A sends its bulk state.
        sink.send(TMsg::Text(a_bulk_to_send.into())).await.unwrap();
        // A receives B's bulk state.
        if let Some(Ok(TMsg::Text(txt))) = stream.next().await {
            *a_received_clone.lock().unwrap() = Some(txt.to_string());
        }
    });

    // B connects, exchanges bulk state.
    let (ws_b, _) = connect_async(format!("ws://{addr}")).await.unwrap();
    let (mut sink_b, mut stream_b) = ws_b.split();

    // B receives A's bulk and applies it.
    if let Some(Ok(TMsg::Text(txt))) = stream_b.next().await {
        let msg: SyncMessage = serde_json::from_str(txt.as_str()).unwrap();
        if let SyncMessage::Bulk { ops } = msg {
            for op_str in &ops {
                let signed: bft_json_crdt::json_crdt::SignedOp =
                    serde_json::from_str(op_str).unwrap();
                let _ = crdt_b.apply(signed);
            }
        }
    }

    // B sends its bulk state to A.
    sink_b.send(TMsg::Text(b_bulk_wire.into())).await.unwrap();

    tokio::time::sleep(std::time::Duration::from_millis(50)).await;

    // Apply A's received ops into crdt_a.
    if let Some(bulk_str) = a_received_bulk.lock().unwrap().take() {
        let msg: SyncMessage = serde_json::from_str(&bulk_str).unwrap();
        if let SyncMessage::Bulk { ops } = msg {
            for op_str in &ops {
                let signed: bft_json_crdt::json_crdt::SignedOp =
                    serde_json::from_str(op_str).unwrap();
                let _ = crdt_a.apply(signed);
            }
        }
    }

    // ── Assert convergence ─────────────────────────────────────────────

    // Both nodes must have 2 items.
    assert_eq!(
        crdt_a.doc.items.view().len(),
        2,
        "A must have 2 items after healing"
    );
    assert_eq!(
        crdt_b.doc.items.view().len(),
        2,
        "B must have 2 items after healing"
    );

    // A must see B's story.
    let b_story_on_a = crdt_a
        .doc
        .items
        .view()
        .iter()
        .any(|item| item.story_id.view() == JV::String("508_heal_b".to_string()));
    assert!(b_story_on_a, "A must have B's story after healing");

    // B must see A's stage advance.
    let a_story_on_b = crdt_b
        .doc
        .items
        .view()
        .iter()
        .any(|item| item.story_id.view() == JV::String("508_heal_a".to_string()));
    assert!(a_story_on_b, "B must have A's story after healing");

    // CRDT views must be byte-identical (convergence).
    let view_a = serde_json::to_string(&CrdtNode::view(&crdt_a.doc.items)).unwrap();
    let view_b = serde_json::to_string(&CrdtNode::view(&crdt_b.doc.items)).unwrap();
    assert_eq!(
        view_a, view_b,
        "Both nodes must converge to identical state"
    );
}
