//! Real TCP smoke test via reqwest.
//! Binds to 127.0.0.1:0, spawns the Axum server in a background thread, and
//! exercises the full REST API surface.

use mps::commands::serve::{build_router, AppState};
use mps::config::{Config, ServeConfig};
use reqwest::blocking::Client;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use tempfile::TempDir;

/// Spawn server on a random port; return (port, shutdown handle via thread::JoinHandle).
fn start_server(dir: &TempDir) -> (u16, std::thread::JoinHandle<()>) {
    let storage = dir.path().join("storage");
    std::fs::create_dir_all(&storage).unwrap();
    let config = Config {
        mps_dir: dir.path().to_path_buf(),
        storage_dir: storage.clone(),
        log_file: dir.path().join("mps.log"),
        git_remote: "origin".into(),
        git_branch: "master".into(),
        default_command: "list".into(),
        type_aliases: Default::default(),
        command_aliases: Default::default(),
        custom_tags: vec![],
        notify: Default::default(),
        serve: ServeConfig {
            port: 0,
            host: "127.0.0.1".into(),
            token: String::new(),
        },
    };
    let state = Arc::new(AppState {
        config,
        config_path: PathBuf::from("/tmp/smoke_config.yaml"),
    });

    let (port_tx, port_rx) = std::sync::mpsc::channel::<u16>();

    let handle = std::thread::spawn(move || {
        let rt = tokio::runtime::Runtime::new().unwrap();
        rt.block_on(async {
            let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
            let port = listener.local_addr().unwrap().port();
            port_tx.send(port).unwrap();
            axum::serve(listener, build_router(state)).await.unwrap();
        });
    });

    let port = port_rx.recv_timeout(Duration::from_secs(5)).unwrap();
    (port, handle)
}

fn wait_until_ready(base: &str) {
    let client = Client::new();
    for _ in 0..20 {
        if client
            .get(format!("{}/health", base))
            .timeout(Duration::from_millis(200))
            .send()
            .is_ok()
        {
            return;
        }
        std::thread::sleep(Duration::from_millis(100));
    }
    panic!("server did not become ready within 2s");
}

#[test]
fn test_smoke_full_workflow() {
    let dir = TempDir::new().unwrap();
    let (port, _handle) = start_server(&dir);
    let base = format!("http://127.0.0.1:{}", port);
    wait_until_ready(&base);

    let client = Client::new();

    // ── Health ────────────────────────────────────────────────────────────────
    let resp = client.get(format!("{}/health", base)).send().unwrap();
    assert_eq!(resp.status(), 200);
    let j: serde_json::Value = resp.json().unwrap();
    assert_eq!(j["status"], "ok");
    assert!(j["version"].as_str().unwrap().contains("1.7"));

    // ── Append task ───────────────────────────────────────────────────────────
    let resp = client
        .post(format!("{}/elements", base))
        .json(&serde_json::json!({
            "type": "task",
            "body": "smoke task",
            "tags": ["smoke"]
        }))
        .send()
        .unwrap();
    assert_eq!(resp.status(), 201);
    let post_j: serde_json::Value = resp.json().unwrap();
    let task_ref = post_j["ref"].as_str().unwrap().to_string();
    assert!(!task_ref.is_empty());

    // ── Append note ───────────────────────────────────────────────────────────
    let resp = client
        .post(format!("{}/elements", base))
        .json(&serde_json::json!({"type": "note", "body": "smoke note"}))
        .send()
        .unwrap();
    assert_eq!(resp.status(), 201);

    // ── List — both present ───────────────────────────────────────────────────
    let resp = client.get(format!("{}/elements", base)).send().unwrap();
    let list: serde_json::Value = resp.json().unwrap();
    assert_eq!(list.as_array().unwrap().len(), 2);

    // ── Search ────────────────────────────────────────────────────────────────
    let resp = client
        .get(format!("{}/search?q=smoke+task", base))
        .send()
        .unwrap();
    let search: serde_json::Value = resp.json().unwrap();
    assert_eq!(search.as_array().unwrap().len(), 1);
    assert_eq!(search[0]["type"], "task");

    // ── Patch — mark done ─────────────────────────────────────────────────────
    let resp = client
        .patch(format!("{}/elements/{}", base, task_ref))
        .json(&serde_json::json!({"status": "done"}))
        .send()
        .unwrap();
    assert_eq!(resp.status(), 200);
    let patch_j: serde_json::Value = resp.json().unwrap();
    assert_eq!(patch_j["updated"], true);

    // ── Stats ─────────────────────────────────────────────────────────────────
    let resp = client.get(format!("{}/stats", base)).send().unwrap();
    let stats: serde_json::Value = resp.json().unwrap();
    assert_eq!(stats["tasks"]["done"], 1);
    assert_eq!(stats["tasks"]["open"], 0);
    assert_eq!(stats["notes"], 1);

    // ── Delete task ───────────────────────────────────────────────────────────
    let resp = client
        .delete(format!("{}/elements/{}", base, task_ref))
        .send()
        .unwrap();
    assert_eq!(resp.status(), 200);

    // ── List — only note remains ──────────────────────────────────────────────
    let resp = client.get(format!("{}/elements", base)).send().unwrap();
    let after_delete: serde_json::Value = resp.json().unwrap();
    assert_eq!(after_delete.as_array().unwrap().len(), 1);
    assert_eq!(after_delete[0]["type"], "note");

    // ── Export JSON ───────────────────────────────────────────────────────────
    let resp = client
        .get(format!("{}/export?format=json", base))
        .send()
        .unwrap();
    assert_eq!(resp.status(), 200);
    let export: serde_json::Value = resp.json().unwrap();
    assert!(export.is_array());

    // ── Export CSV ────────────────────────────────────────────────────────────
    let resp = client
        .get(format!("{}/export?format=csv", base))
        .send()
        .unwrap();
    assert_eq!(resp.status(), 200);
    let csv_text = resp.text().unwrap();
    assert!(csv_text.contains("smoke note"));

    // ── Tags ──────────────────────────────────────────────────────────────────
    // Note has no tags (task was deleted); append a tagged note
    client
        .post(format!("{}/elements", base))
        .json(&serde_json::json!({
            "type": "note",
            "body": "tagged note",
            "tags": ["smoke"]
        }))
        .send()
        .unwrap();
    let resp = client.get(format!("{}/tags", base)).send().unwrap();
    let tags: serde_json::Value = resp.json().unwrap();
    assert!(tags["smoke"].as_u64().unwrap() >= 1);
}

#[test]
fn test_smoke_auth_rejection() {
    let dir = TempDir::new().unwrap();
    let storage = dir.path().join("storage");
    std::fs::create_dir_all(&storage).unwrap();

    let config = Config {
        mps_dir: dir.path().to_path_buf(),
        storage_dir: storage,
        log_file: dir.path().join("mps.log"),
        git_remote: "origin".into(),
        git_branch: "master".into(),
        default_command: "list".into(),
        type_aliases: Default::default(),
        command_aliases: Default::default(),
        custom_tags: vec![],
        notify: Default::default(),
        serve: ServeConfig {
            port: 0,
            host: "127.0.0.1".into(),
            token: "supersecret".into(),
        },
    };
    let state = Arc::new(AppState {
        config,
        config_path: PathBuf::from("/tmp/smoke_auth_config.yaml"),
    });

    let (port_tx, port_rx) = std::sync::mpsc::channel::<u16>();
    std::thread::spawn(move || {
        let rt = tokio::runtime::Runtime::new().unwrap();
        rt.block_on(async {
            let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
            let port = listener.local_addr().unwrap().port();
            port_tx.send(port).unwrap();
            axum::serve(listener, build_router(state)).await.unwrap();
        });
    });
    let port = port_rx.recv_timeout(Duration::from_secs(5)).unwrap();
    let base = format!("http://127.0.0.1:{}", port);
    wait_until_ready_with_auth(&base, "supersecret");

    let client = Client::new();

    // No token — 401
    let resp = client.get(format!("{}/health", base)).send().unwrap();
    assert_eq!(resp.status(), 401);

    // Wrong token — 401
    let resp = client
        .get(format!("{}/health", base))
        .bearer_auth("wrong")
        .send()
        .unwrap();
    assert_eq!(resp.status(), 401);

    // Correct token — 200
    let resp = client
        .get(format!("{}/health", base))
        .bearer_auth("supersecret")
        .send()
        .unwrap();
    assert_eq!(resp.status(), 200);
}

fn wait_until_ready_with_auth(base: &str, token: &str) {
    let client = Client::new();
    for _ in 0..20 {
        if client
            .get(format!("{}/health", base))
            .bearer_auth(token)
            .timeout(Duration::from_millis(200))
            .send()
            .is_ok()
        {
            return;
        }
        std::thread::sleep(Duration::from_millis(100));
    }
    panic!("server did not become ready within 2s");
}

#[test]
fn test_smoke_error_handling() {
    let dir = TempDir::new().unwrap();
    let (port, _handle) = start_server(&dir);
    let base = format!("http://127.0.0.1:{}", port);
    wait_until_ready(&base);

    let client = Client::new();

    // Invalid type → 400
    let resp = client
        .post(format!("{}/elements", base))
        .json(&serde_json::json!({"type": "garbage", "body": "x"}))
        .send()
        .unwrap();
    assert_eq!(resp.status(), 400);
    let j: serde_json::Value = resp.json().unwrap();
    assert!(j["error"].as_str().is_some());

    // Missing type → 422
    let resp = client
        .post(format!("{}/elements", base))
        .json(&serde_json::json!({"body": "no type"}))
        .send()
        .unwrap();
    assert_eq!(resp.status(), 422);

    // Patch nonexistent ref → 404
    let resp = client
        .patch(format!("{}/elements/99991231.9999", base))
        .json(&serde_json::json!({"status": "done"}))
        .send()
        .unwrap();
    assert_eq!(resp.status(), 404);

    // Delete nonexistent ref → 404
    let resp = client
        .delete(format!("{}/elements/99991231.9999", base))
        .send()
        .unwrap();
    assert_eq!(resp.status(), 404);
}
