#![cfg(all(target_os = "linux", target_arch = "aarch64"))]

use std::io::{Read, Write};
use std::net::{SocketAddr, TcpListener};
use std::ptr::NonNull;
use std::time::{Duration, Instant};

use dambi::{Bytes, SlotId};
use dope_core::driver::{Backend, DriverConfig};
use dope_core::profile::Tail;
use dope_runtime::runtime::Manifold;
use dope_session::Plain;
use dope_session::pool::Pool;
use dope_session::pool::client::connect_source::StaticUpstreams;
use dope_session::pool::client::{Client, TcpClientConfig};
use dope_session::protocol::client::ClientProtocol;
use dope_session::session::Egress;
use dope_transport::Tcp;
use dope_transport::config::{SocketToggle, StreamSocketConfig};
use dope_uring::{CurrentSlot, UringBackend, set_current};

const MAX_CONN: usize = 4;
const SLOT_BASE: usize = 0;
const TICK_BUDGET: Duration = Duration::from_secs(5);
const ECHO_LEN: usize = 11;

#[derive(Default)]
struct EchoState;

#[derive(Default)]
struct EchoProto {
    received: Vec<Vec<u8>>,
    connected: bool,
}

impl ClientProtocol for EchoProto {
    type Head = Bytes;
    type ConnState = EchoState;
    type CodecLayer = Plain;

    fn parse(&self, _state: &mut EchoState, buf: &Bytes) -> Option<(Bytes, usize)> {
        if buf.len() < ECHO_LEN {
            None
        } else {
            Some((buf.slice(0..ECHO_LEN), ECHO_LEN))
        }
    }

    fn on_connect(
        &mut self,
        _conn_id: SlotId,
        _conn_state: &mut EchoState,
        _out: Egress<'_, Plain>,
    ) {
        self.connected = true;
    }

    fn on_response(
        &mut self,
        _conn_id: SlotId,
        _conn_state: &mut EchoState,
        head: Bytes,
        _out: Egress<'_, Plain>,
    ) {
        self.received.push(head.as_ref().to_vec());
    }
}

struct EchoServer {
    addr: SocketAddr,
    join: Option<std::thread::JoinHandle<()>>,
}

impl EchoServer {
    fn spawn() -> Self {
        let listener = TcpListener::bind("127.0.0.1:0").expect("bind echo server");
        let addr = listener.local_addr().expect("local_addr");
        let join = std::thread::spawn(move || {
            let (mut stream, _) = match listener.accept() {
                Ok(v) => v,
                Err(_) => return,
            };
            stream.set_read_timeout(Some(Duration::from_secs(2))).ok();
            let mut buf = [0u8; 4096];
            match stream.read(&mut buf) {
                Ok(0) => {}
                Ok(n) => {
                    let _ = stream.write_all(&buf[..n]);
                }
                Err(_) => {}
            }
        });
        Self {
            addr,
            join: Some(join),
        }
    }
}

impl Drop for EchoServer {
    fn drop(&mut self) {
        if let Some(j) = self.join.take() {
            let _ = j.join();
        }
    }
}

#[test]
fn pool_client_echo_roundtrip() {
    let server = EchoServer::spawn();

    let driver_cfg = <UringBackend as Backend>::Config::for_tcp_profile(256, MAX_CONN, 4096);
    let mut driver = <UringBackend as Backend>::new_driver(driver_cfg).expect("driver");

    let proto = EchoProto::default();
    let upstreams = StaticUpstreams::<Tcp>::new(vec![server.addr], Duration::from_millis(50));
    let mut socket_opts = StreamSocketConfig::default();
    socket_opts.nodelay = SocketToggle::Enabled;
    let cfg = TcpClientConfig {
        max_conn: MAX_CONN,
        socket_opts,
        shutdown: None,
        cpu_id: None,
    };
    let mut pool: Pool<Client<EchoProto, Tcp, StaticUpstreams<Tcp>>, Plain, Tail, UringBackend> =
        Pool::build_raw(proto, upstreams, cfg, &mut driver, SLOT_BASE).expect("build_raw");

    let driver_ptr = NonNull::from(&mut driver);
    set_current(Some(CurrentSlot::new(driver_ptr, std::ptr::null())));

    let deadline = Instant::now() + TICK_BUDGET;
    let mut sent = false;
    loop {
        pool.drive(&mut driver);

        if !sent {
            let conn_opt = pool.live_conns().next();
            if let Some(conn_id) = conn_opt {
                if let Some(session) = pool.session_mut(conn_id) {
                    session.enqueue(Bytes::copy_from_slice(b"hello-pool!"));
                    let _ = pool.poke_outbound(&mut driver, conn_id);
                    sent = true;
                }
            }
        }

        let done = pool.direction_mut().protocol().received.len() >= 1;
        if done {
            break;
        }
        if Instant::now() >= deadline {
            let proto = pool.direction_mut().protocol();
            panic!(
                "echo response timeout: connected={}, received_count={}",
                proto.connected,
                proto.received.len()
            );
        }
        let _ = driver.park_for(Duration::from_millis(5));
    }

    let proto = pool.direction_mut().protocol();
    assert!(proto.connected, "on_connect should fire");
    assert_eq!(proto.received.len(), 1, "expected exactly one response");
    assert_eq!(proto.received[0], b"hello-pool!", "echo body mismatch");
}
