#![cfg(target_os = "windows")]

use std::ffi::OsStr;
use std::io::{BufRead, BufReader, ErrorKind, Write};
use std::os::windows::ffi::OsStrExt;
use std::os::windows::io::FromRawHandle;
use std::sync::mpsc;
use std::thread;

use tsafe_core::agent::{
    format_agent_sock, ping_agent, request_password_from_agent, send_lock, write_agent_sock,
    AgentRequest, AgentResponse, ENV_AGENT_SOCK,
};

const PIPE_ACCESS_DUPLEX: u32 = 0x0000_0003;
const PIPE_TYPE_MESSAGE: u32 = 0x0000_0004;
const PIPE_READMODE_MESSAGE: u32 = 0x0000_0002;
const PIPE_WAIT: u32 = 0x0000_0000;

extern "system" {
    fn CreateNamedPipeW(
        lpName: *const u16,
        dwOpenMode: u32,
        dwPipeMode: u32,
        nMaxInstances: u32,
        nOutBufferSize: u32,
        nInBufferSize: u32,
        nDefaultTimeOut: u32,
        lpSecurityAttributes: *mut std::ffi::c_void,
    ) -> *mut std::ffi::c_void;
    fn ConnectNamedPipe(
        hNamedPipe: *mut std::ffi::c_void,
        lpOverlapped: *mut std::ffi::c_void,
    ) -> i32;
}

fn unique_pipe_name() -> String {
    let nanos = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap()
        .as_nanos();
    format!(
        r"\\.\pipe\tsafe-core-agent-test-{}-{nanos}",
        std::process::id()
    )
}

fn spawn_single_request_server(
    pipe_name: String,
    response: AgentResponse,
) -> mpsc::Receiver<AgentRequest> {
    let (tx, rx) = mpsc::channel();
    let (ready_tx, ready_rx) = mpsc::channel();
    thread::spawn(move || {
        let wide: Vec<u16> = OsStr::new(&pipe_name)
            .encode_wide()
            .chain(std::iter::once(0))
            .collect();
        let handle = unsafe {
            CreateNamedPipeW(
                wide.as_ptr(),
                PIPE_ACCESS_DUPLEX,
                PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
                1,
                4096,
                4096,
                0,
                std::ptr::null_mut(),
            )
        };
        assert_ne!(
            handle as isize, -1,
            "CreateNamedPipeW failed for {pipe_name}"
        );
        ready_tx.send(()).unwrap();

        let connected = unsafe { ConnectNamedPipe(handle, std::ptr::null_mut()) };
        assert_ne!(connected, 0, "ConnectNamedPipe failed for {pipe_name}");

        let mut file = unsafe { std::fs::File::from_raw_handle(handle as _) };
        let mut line = String::new();
        BufReader::new(&mut file)
            .read_line(&mut line)
            .expect("server should read one JSON line");
        let request: AgentRequest =
            serde_json::from_str(line.trim()).expect("request should deserialize");
        tx.send(request).unwrap();

        let response_json = serde_json::to_string(&response).unwrap();
        match writeln!(file, "{response_json}") {
            Ok(()) => {}
            Err(error) if error.kind() == ErrorKind::BrokenPipe => {}
            Err(error) => panic!("server should write one JSON response: {error}"),
        }
    });
    ready_rx.recv().unwrap();
    rx
}

#[test]
fn ping_agent_reports_running_named_pipe_session() {
    let pipe_name = unique_pipe_name();
    let rx = spawn_single_request_server(pipe_name.clone(), AgentResponse::Ok);

    let sock = format_agent_sock(&pipe_name, "deadbeef");
    assert!(ping_agent(&sock).unwrap());

    let request = rx.recv().unwrap();
    assert!(matches!(request, AgentRequest::Ping));
}

#[test]
fn request_password_from_agent_reuses_persisted_windows_pipe_state() {
    let temp = tempfile::tempdir().unwrap();
    let vault_dir = temp.path().join("vaults");
    let pipe_name = unique_pipe_name();
    let rx = spawn_single_request_server(
        pipe_name.clone(),
        AgentResponse::Password {
            password: "secret-from-agent".to_string(),
        },
    );
    let sock = format_agent_sock(&pipe_name, "cafebabe");

    temp_env::with_var(
        "TSAFE_VAULT_DIR",
        Some(vault_dir.as_os_str().to_str().unwrap()),
        || {
            temp_env::with_var(ENV_AGENT_SOCK, None::<&str>, || {
                write_agent_sock(&sock);

                let password = request_password_from_agent("work").unwrap();
                assert_eq!(password.as_deref(), Some("secret-from-agent"));
            });
        },
    );

    let request = rx.recv().unwrap();
    match request {
        AgentRequest::OpenVault {
            profile,
            session_token,
            requesting_pid,
        } => {
            assert_eq!(profile, "work");
            assert_eq!(session_token, "cafebabe");
            assert_eq!(requesting_pid, std::process::id());
        }
        other => panic!("expected OpenVault request, got {other:?}"),
    }
}

#[test]
fn send_lock_uses_persisted_windows_pipe_state() {
    let temp = tempfile::tempdir().unwrap();
    let vault_dir = temp.path().join("vaults");
    let pipe_name = unique_pipe_name();
    let rx = spawn_single_request_server(pipe_name.clone(), AgentResponse::Ok);
    let sock = format_agent_sock(&pipe_name, "locktoken");

    temp_env::with_var(
        "TSAFE_VAULT_DIR",
        Some(vault_dir.as_os_str().to_str().unwrap()),
        || {
            temp_env::with_var(ENV_AGENT_SOCK, None::<&str>, || {
                write_agent_sock(&sock);
                send_lock().unwrap();
            });
        },
    );

    let request = rx.recv().unwrap();
    match request {
        AgentRequest::Lock { session_token } => {
            assert_eq!(session_token, "locktoken");
        }
        other => panic!("expected Lock request, got {other:?}"),
    }
}
