use std::net::TcpListener;
use std::path::Path;
use std::process::Stdio;
use std::time::Duration;

use serde_json::{Value, json};
use tokio::process::{Child, Command};

struct GatewayProcess(Child);

impl Drop for GatewayProcess {
    fn drop(&mut self) {
        let _ = self.0.start_kill();
    }
}

fn available_port() -> u16 {
    TcpListener::bind("127.0.0.1:0")
        .unwrap()
        .local_addr()
        .unwrap()
        .port()
}

fn config(workspace: &Path, model_id: &str) -> Value {
    json!({
        "providers": {
            "provider": {
                "type": "openai",
                "base_url": "https://example.invalid/v1",
                "api_key": "test"
            }
        },
        "models": { "model": { "model_id": model_id } },
        "agents": { "default": { "provider": "provider", "model": "model" } },
        "gateway": { "require_pairing": false },
        "workspace_dir": workspace
    })
}

async fn wait_for_health(client: &reqwest::Client, base: &str) {
    for _ in 0..100 {
        if client
            .get(format!("{base}/health"))
            .send()
            .await
            .is_ok_and(|response| response.status().is_success())
        {
            return;
        }
        tokio::time::sleep(Duration::from_millis(50)).await;
    }
    panic!("gateway did not become healthy");
}

#[tokio::test]
async fn gateway_reloads_valid_config_and_keeps_serving_after_invalid_config() {
    let temp = tempfile::tempdir().unwrap();
    let home = temp.path().join("home");
    let config_dir = home.join(".picobot");
    let workspace = temp.path().join("workspace");
    std::fs::create_dir_all(&config_dir).unwrap();
    std::fs::create_dir_all(&workspace).unwrap();
    let config_path = config_dir.join("config.json");
    std::fs::write(
        &config_path,
        serde_json::to_vec_pretty(&config(&workspace, "old-model")).unwrap(),
    )
    .unwrap();

    let port = available_port();
    let child = Command::new(env!("CARGO_BIN_EXE_picobot"))
        .args([
            "gateway",
            "--host",
            "127.0.0.1",
            "--port",
            &port.to_string(),
        ])
        .env("HOME", &home)
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .kill_on_drop(true)
        .spawn()
        .unwrap();
    let mut gateway = GatewayProcess(child);
    let client = reqwest::Client::new();
    let base = format!("http://127.0.0.1:{port}");
    wait_for_health(&client, &base).await;

    std::fs::write(
        &config_path,
        serde_json::to_vec_pretty(&config(&workspace, "new-model")).unwrap(),
    )
    .unwrap();
    let accepted: Value = client
        .post(format!("{base}/api/config/reload"))
        .send()
        .await
        .unwrap()
        .error_for_status()
        .unwrap()
        .json()
        .await
        .unwrap();
    assert_eq!(accepted["generation"], 2);

    let mut active = false;
    for _ in 0..100 {
        let response = client
            .get(format!("{base}/api/config/reload/status"))
            .send()
            .await;
        if let Ok(response) = response
            && let Ok(status) = response.json::<Value>().await
            && status["generation"] == 2
            && status["phase"] == "active"
        {
            active = true;
            break;
        }
        tokio::time::sleep(Duration::from_millis(50)).await;
    }
    assert!(active, "reloaded generation did not become active");

    std::fs::write(&config_path, b"{").unwrap();
    let invalid = client
        .post(format!("{base}/api/config/reload"))
        .send()
        .await
        .unwrap();
    assert_eq!(invalid.status(), reqwest::StatusCode::BAD_REQUEST);
    assert!(
        client
            .get(format!("{base}/health"))
            .send()
            .await
            .unwrap()
            .status()
            .is_success(),
        "invalid candidate stopped the active generation"
    );

    gateway.0.start_kill().unwrap();
    gateway.0.wait().await.unwrap();
}
