use iroh::{Endpoint, endpoint::ConnectionError};
use n0_tracing_test::traced_test;
use tracing::Instrument;
use url::Url;

use crate::{ALPN_H3, Client, H3Request, QuicRequest, SessionError};

#[tokio::test]
#[traced_test]
async fn h3_smoke() -> n0_error::Result<()> {
    let client = Endpoint::bind()
        .instrument(tracing::error_span!("client-ep"))
        .await
        .unwrap();
    let client_id = client.id();
    let client = Client::new(client);

    let server = Endpoint::builder()
        .alpns(vec![ALPN_H3.as_bytes().to_vec()])
        .bind()
        .instrument(tracing::error_span!("server-ep"))
        .await
        .unwrap();
    let server_id = server.id();
    let server_addr = server.addr();

    let url: Url = format!("https://{}/foo", server_id).parse().unwrap();

    let client_task = tokio::task::spawn({
        let url = url.clone();
        async move {
            let session = client.connect_h3(server_addr, url.clone()).await.inspect_err(|err| println!("{err:#?}")).unwrap();
            assert_eq!(session.remote_id(), server_id);
            assert_eq!(session.url(), Some(&url));

            let mut stream = session.open_uni().await.unwrap();
            stream.write_all(b"hi").await.unwrap();
            stream.finish().unwrap();
            let reason = session.closed().await;
            assert!(
                matches!(reason, SessionError::ConnectionError(ConnectionError::ApplicationClosed(frame)) if web_transport_proto::error_from_http3(frame.error_code.into_inner()) == Some(23))
            );

            drop(session);
            client.close().await;
        }.instrument(tracing::error_span!("client"))
    });

    let server_task = tokio::task::spawn(
        async move {
            let conn = server.accept().await.unwrap().await.unwrap();
            assert_eq!(conn.alpn(), ALPN_H3.as_bytes());
            let request = H3Request::accept(conn)
                .await
                .inspect_err(|err| tracing::error!("accept failed: {err:?}"))
                .unwrap();
            assert_eq!(request.url(), &url);
            assert_eq!(request.conn().remote_id(), client_id);
            let session = request.ok().await.unwrap();
            assert_eq!(session.url(), Some(&url));
            assert_eq!(session.conn().remote_id(), client_id);
            let mut stream = session.accept_uni().await.unwrap();
            let buf = stream.read_to_end(2).await.unwrap();
            assert_eq!(buf, b"hi");
            session.close(23, b"bye");
            server.close().await;
        }
        .instrument(tracing::error_span!("server")),
    );

    client_task.await.unwrap();
    server_task.await.unwrap();

    Ok(())
}

#[tokio::test]
#[traced_test]
async fn quic_smoke() -> n0_error::Result<()> {
    const ALPN: &str = "moql";

    let client = Endpoint::bind().await.unwrap();
    let client_id = client.id();
    let client = Client::new(client);

    let server = Endpoint::builder()
        .alpns(vec![ALPN.as_bytes().to_vec()])
        .bind()
        .await
        .unwrap();
    let server_id = server.id();
    let server_addr = server.addr();

    let client_task = tokio::task::spawn({
        async move {
            let session = client
                .connect_quic(server_addr, ALPN.as_bytes())
                .await
                .unwrap();
            println!("session established");
            assert_eq!(session.remote_id(), server_id);
            assert_eq!(session.url(), None);
            let reason = session.closed().await;
            assert!(
                matches!(reason, SessionError::ConnectionError(ConnectionError::ApplicationClosed(frame)) if frame.error_code.into_inner() == 23)
            )
        }.instrument(tracing::error_span!("client"))
    });

    let server_task = tokio::task::spawn({
        async move {
            let conn = server.accept().await.unwrap().await.unwrap();
            assert_eq!(conn.alpn(), ALPN.as_bytes());
            let request = QuicRequest::accept(conn);
            assert_eq!(request.conn().remote_id(), client_id);
            let session = request.ok();
            assert_eq!(session.url(), None);
            assert_eq!(session.conn().remote_id(), client_id);
            session.close(23, b"bye");
        }
        .instrument(tracing::error_span!("server"))
    });

    client_task.await.unwrap();
    server_task.await.unwrap();

    Ok(())
}
