mod protocol;

use std::collections::HashSet;
use std::time::Duration;

use protocol::frame_harness::TestServer;
use protocol::scenario_harness::ScenarioSession;
use spvirit_codec::epics_decode::PvaPacketCommand;
use spvirit_codec::spvd_decode::{DecodedValue, extract_nt_scalar_value};
use spvirit_codec::spvirit_encode::encode_header;
use tokio::net::UdpSocket;

const PV_REQUEST_EMPTY: [u8; 6] = [0xfd, 0x02, 0x00, 0x80, 0x00, 0x00];

fn encode_size(size: usize) -> Vec<u8> {
    if size == 0 {
        return vec![0];
    }
    if size < 254 {
        return vec![size as u8];
    }
    let mut out = vec![0xFE];
    out.extend_from_slice(&(size as u32).to_le_bytes());
    out
}

fn encode_string(value: &str) -> Vec<u8> {
    let mut out = encode_size(value.len());
    out.extend_from_slice(value.as_bytes());
    out
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn lifecycle_get_put_monitor_paths() {
    let server = match TestServer::spawn() {
        Ok(s) => s,
        Err(msg) => {
            eprintln!("Skipping test, cannot spawn server: {}", msg);
            return;
        }
    };

    let mut scenario = ScenarioSession::connect(&server)
        .await
        .expect("connect scenario");
    let initial = scenario
        .get_scalar_value("SIM:AO")
        .await
        .expect("get initial");
    let initial_value = extract_nt_scalar_value(&initial).expect("value field");
    match initial_value {
        DecodedValue::Float64(v) => assert!((*v - 2.50).abs() < 1e-6),
        other => panic!("unexpected initial value type: {:?}", other),
    }

    scenario
        .put_scalar_number("SIM:AO", 4.321)
        .await
        .expect("put");
    let after = scenario
        .get_scalar_value("SIM:AO")
        .await
        .expect("get after put");
    let after_value = extract_nt_scalar_value(&after).expect("value field");
    match after_value {
        DecodedValue::Float64(v) => assert!((*v - 4.321).abs() < 1e-6),
        other => panic!("unexpected updated value type: {:?}", other),
    }

    scenario
        .monitor_init_start_stop("SIM:AO")
        .await
        .expect("monitor lifecycle");
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn lifecycle_multiple_channels_and_ioids() {
    let server = match TestServer::spawn() {
        Ok(s) => s,
        Err(msg) => {
            eprintln!("Skipping test, cannot spawn server: {}", msg);
            return;
        }
    };

    let mut scenario = ScenarioSession::connect(&server)
        .await
        .expect("connect scenario");
    let sid_ai = scenario.ensure_channel("SIM:AI").await.expect("sid ai");
    let sid_ao = scenario.ensure_channel("SIM:AO").await.expect("sid ao");

    let ioid_ai = 4001u32;
    let ioid_ao = 4002u32;
    scenario
        .session
        .send_client_op(10, sid_ai, ioid_ai, 0x08, &PV_REQUEST_EMPTY)
        .await
        .expect("get init ai");
    scenario
        .session
        .send_client_op(10, sid_ao, ioid_ao, 0x08, &PV_REQUEST_EMPTY)
        .await
        .expect("get init ao");

    let mut init_seen = HashSet::new();
    while init_seen.len() < 2 {
        let cmd = scenario
            .session
            .read_until(Duration::from_secs(2), |cmd| {
                matches!(cmd, PvaPacketCommand::Op(op) if op.command == 10 && (op.subcmd & 0x08) != 0)
            })
            .await
            .expect("init response");
        if let PvaPacketCommand::Op(op) = cmd {
            init_seen.insert(op.ioid);
        }
    }
    assert!(init_seen.contains(&ioid_ai));
    assert!(init_seen.contains(&ioid_ao));

    scenario
        .session
        .send_client_op(10, sid_ai, ioid_ai, 0x00, &[])
        .await
        .expect("get data ai");
    scenario
        .session
        .send_client_op(10, sid_ao, ioid_ao, 0x00, &[])
        .await
        .expect("get data ao");

    let mut data_seen = HashSet::new();
    while data_seen.len() < 2 {
        let cmd = scenario
            .session
            .read_until(Duration::from_secs(2), |cmd| {
                matches!(cmd, PvaPacketCommand::Op(op) if op.command == 10 && op.subcmd == 0x00)
            })
            .await
            .expect("data response");
        if let PvaPacketCommand::Op(op) = cmd {
            data_seen.insert(op.ioid);
        }
    }
    assert!(data_seen.contains(&ioid_ai));
    assert!(data_seen.contains(&ioid_ao));
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn lifecycle_put_without_init_returns_error() {
    let server = match TestServer::spawn() {
        Ok(s) => s,
        Err(msg) => {
            eprintln!("Skipping test, cannot spawn server: {}", msg);
            return;
        }
    };

    let mut scenario = ScenarioSession::connect(&server)
        .await
        .expect("connect scenario");
    let sid = scenario.ensure_channel("SIM:AO").await.expect("sid ao");
    let ioid = 5010u32;

    scenario
        .session
        .send_client_op(11, sid, ioid, 0x00, &[0x00, 0x02])
        .await
        .expect("put data without init");
    let cmd = scenario
        .session
        .read_until(Duration::from_secs(2), |cmd| {
            matches!(cmd, PvaPacketCommand::Op(op) if op.command == 11 && op.ioid == ioid)
        })
        .await
        .expect("put error response");

    match cmd {
        PvaPacketCommand::Op(op) => {
            let status = op.status.expect("status");
            assert_ne!(status.code, 0xFF);
            let msg = status.message.unwrap_or_default();
            assert!(
                msg.contains("PUT without init"),
                "unexpected message: {}",
                msg
            );
        }
        other => panic!("unexpected command: {:?}", other),
    }
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn lifecycle_destroy_channel_invalidates_sid() {
    let server = match TestServer::spawn() {
        Ok(s) => s,
        Err(msg) => {
            eprintln!("Skipping test, cannot spawn server: {}", msg);
            return;
        }
    };

    let mut scenario = ScenarioSession::connect(&server)
        .await
        .expect("connect scenario");
    let sid = scenario.ensure_channel("SIM:AI").await.expect("sid ai");
    let cid = 9901u32;
    let ioid = 9902u32;

    let mut destroy_payload = Vec::new();
    destroy_payload.extend_from_slice(&sid.to_le_bytes());
    destroy_payload.extend_from_slice(&cid.to_le_bytes());
    scenario
        .session
        .send_client_application(8, &destroy_payload)
        .await
        .expect("destroy channel");

    scenario
        .session
        .send_client_op(10, sid, ioid, 0x08, &PV_REQUEST_EMPTY)
        .await
        .expect("get after destroy");
    let cmd = scenario
        .session
        .read_until(Duration::from_secs(2), |cmd| {
            matches!(cmd, PvaPacketCommand::Op(op) if op.command == 10 && op.ioid == ioid && (op.subcmd & 0x08) != 0)
        })
        .await
        .expect("op error");

    match cmd {
        PvaPacketCommand::Op(op) => {
            let status = op.status.expect("status");
            let msg = status.message.unwrap_or_default();
            assert!(msg.contains("Unknown SID"), "unexpected error msg: {}", msg);
        }
        other => panic!("unexpected command after destroy: {:?}", other),
    }
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn lifecycle_application_echo_roundtrip() {
    let server = match TestServer::spawn() {
        Ok(s) => s,
        Err(msg) => {
            eprintln!("Skipping test, cannot spawn server: {}", msg);
            return;
        }
    };

    let mut scenario = ScenarioSession::connect(&server)
        .await
        .expect("connect scenario");

    let payload = b"echo-smoke";
    scenario
        .session
        .send_client_application(2, payload)
        .await
        .expect("send application echo");

    let cmd = scenario
        .session
        .read_until(Duration::from_secs(2), |cmd| {
            matches!(cmd, PvaPacketCommand::Echo(_))
        })
        .await
        .expect("echo response");

    match cmd {
        PvaPacketCommand::Echo(bytes) => assert_eq!(bytes, payload),
        other => panic!("unexpected echo response: {:?}", other),
    }
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn lifecycle_rpc_list_mode_gated_behavior() {
    let server = match TestServer::spawn_with_args(&["--pvlist-mode", "list"]) {
        Ok(s) => s,
        Err(msg) => {
            eprintln!("Skipping test, cannot spawn server: {}", msg);
            return;
        }
    };

    let mut scenario = ScenarioSession::connect(&server)
        .await
        .expect("connect scenario");
    let sid = scenario.ensure_channel("server").await.expect("sid server");
    let ioid = 6101u32;

    scenario
        .session
        .send_client_op(20, sid, ioid, 0x08, &PV_REQUEST_EMPTY)
        .await
        .expect("rpc init");
    let init_cmd = scenario
        .session
        .read_until(Duration::from_secs(2), |cmd| {
            matches!(cmd, PvaPacketCommand::Op(op) if op.command == 20 && op.ioid == ioid && (op.subcmd & 0x08) != 0)
        })
        .await
        .expect("rpc init response");
    match init_cmd {
        PvaPacketCommand::Op(op) => {
            if let Some(status) = op.status {
                assert_eq!(
                    status.code, 0xFF,
                    "unexpected rpc init status: {:?}",
                    status
                );
            }
        }
        other => panic!("unexpected rpc init response: {:?}", other),
    }

    scenario
        .session
        .send_client_op(20, sid, ioid, 0x00, &[])
        .await
        .expect("rpc data");
    let data_cmd = scenario
        .session
        .read_until(Duration::from_secs(2), |cmd| {
            matches!(cmd, PvaPacketCommand::Op(op) if op.command == 20 && op.ioid == ioid && op.subcmd == 0x00)
        })
        .await
        .expect("rpc data response");
    match data_cmd {
        PvaPacketCommand::Op(op) => {
            assert!(
                !op.body.is_empty(),
                "rpc data response should contain payload body"
            );
        }
        other => panic!("unexpected rpc data response: {:?}", other),
    }

    let server = match TestServer::spawn_with_args(&["--pvlist-mode", "off"]) {
        Ok(s) => s,
        Err(msg) => {
            eprintln!("Skipping test, cannot spawn server: {}", msg);
            return;
        }
    };
    let mut scenario = ScenarioSession::connect(&server)
        .await
        .expect("connect scenario");
    let sid = scenario.ensure_channel("SIM:AI").await.expect("sid ai");
    let ioid = 6201u32;
    scenario
        .session
        .send_client_op(20, sid, ioid, 0x08, &PV_REQUEST_EMPTY)
        .await
        .expect("rpc init off mode");
    let denied_cmd = scenario
        .session
        .read_until(Duration::from_secs(2), |cmd| {
            matches!(cmd, PvaPacketCommand::Op(op) if op.command == 20 && op.ioid == ioid && (op.subcmd & 0x08) != 0)
        })
        .await
        .expect("rpc deny response");
    match denied_cmd {
        PvaPacketCommand::Op(op) => {
            let status = op.status.expect("expected error status");
            let msg = status.message.unwrap_or_default();
            assert!(
                msg.contains("Operation not supported"),
                "unexpected rpc deny message: {}",
                msg
            );
        }
        other => panic!("unexpected rpc denied response: {:?}", other),
    }
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn lifecycle_udp_search_returns_matching_cid() {
    let server = match TestServer::spawn() {
        Ok(s) => s,
        Err(msg) => {
            eprintln!("Skipping test, cannot spawn server: {}", msg);
            return;
        }
    };

    let socket = UdpSocket::bind("127.0.0.1:0").await.expect("bind udp");
    let local = socket.local_addr().expect("local addr");

    let seq = 0x12345678u32;
    let cid = 0x01020304u32;
    let mut payload = Vec::new();
    payload.extend_from_slice(&seq.to_le_bytes());
    payload.push(0x81);
    payload.extend_from_slice(&[0u8; 3]);
    payload.extend_from_slice(&[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFF, 0xFF, 127, 0, 0, 1]);
    payload.extend_from_slice(&local.port().to_le_bytes());
    payload.extend_from_slice(&encode_size(1));
    payload.extend_from_slice(&encode_string("tcp"));
    payload.extend_from_slice(&1u16.to_le_bytes());
    payload.extend_from_slice(&cid.to_le_bytes());
    payload.extend_from_slice(&encode_string("SIM:AI"));

    let mut frame = encode_header(false, false, false, 2, 3, payload.len() as u32);
    frame.extend_from_slice(&payload);
    socket
        .send_to(&frame, format!("127.0.0.1:{}", server.udp_port))
        .await
        .expect("send search");

    let mut buf = vec![0u8; 2048];
    let (len, _) = tokio::time::timeout(Duration::from_secs(2), socket.recv_from(&mut buf))
        .await
        .expect("search timeout")
        .expect("search recv");
    buf.truncate(len);

    let mut packet = spvirit_codec::epics_decode::PvaPacket::new(&buf);
    let decoded = packet.decode_payload().expect("search decode");
    match decoded {
        PvaPacketCommand::SearchResponse(resp) => {
            assert!(resp.found);
            assert!(resp.cids.contains(&cid));
            assert_eq!(resp.protocol, "tcp");
        }
        other => panic!("unexpected search response {:?}", other),
    }
}
