// SPDX-License-Identifier: AGPL-3.0-or-later
#[path = "mod.rs"]
mod stub;

use axum::{body::Body, http::Request};
use std::sync::{Mutex, OnceLock};
use tower::ServiceExt;

use kaizen::core::config::TelemetryConfig;
use kaizen::core::event::{Event, EventKind, EventSource, SessionRecord, SessionStatus};
use kaizen::store::Store;
use kaizen::sync::FlushExporters;

fn env_lock() -> &'static Mutex<()> {
    static L: OnceLock<Mutex<()>> = OnceLock::new();
    L.get_or_init(|| Mutex::new(()))
}

#[tokio::test]
async fn health_returns_200() {
    let (app, _store) = stub::router();
    let resp = app
        .oneshot(
            Request::builder()
                .uri("/v1/health")
                .body(Body::empty())
                .unwrap(),
        )
        .await
        .unwrap();
    assert_eq!(resp.status(), 200);
}

#[tokio::test]
async fn unique_keys_accepted_dup_rejected() {
    let (app, _store) = stub::router();

    // POST 10 unique keys → all 202.
    for i in 0..10u32 {
        let req = Request::builder()
            .method("POST")
            .uri("/v1/events")
            .header("X-Kaizen-Idempotency-Key", format!("key-{i}"))
            .body(Body::empty())
            .unwrap();
        let resp = app.clone().oneshot(req).await.unwrap();
        assert_eq!(resp.status(), 202, "key-{i} should be accepted");
    }

    // Replay key-5 → 409.
    let dup = Request::builder()
        .method("POST")
        .uri("/v1/events")
        .header("X-Kaizen-Idempotency-Key", "key-5")
        .body(Body::empty())
        .unwrap();
    let resp = app.clone().oneshot(dup).await.unwrap();
    assert_eq!(resp.status(), 409, "duplicate key-5 must be rejected");
}

#[tokio::test]
async fn sync_flush_sends_redacted_gzip_batch() {
    let (app, state) = stub::router();
    let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
    let addr = listener.local_addr().unwrap();
    tokio::spawn(async move {
        axum::serve(listener, app).await.unwrap();
    });
    tokio::time::sleep(std::time::Duration::from_millis(80)).await;

    let tmp = tempfile::TempDir::new().unwrap();
    let ws = tmp.path().to_path_buf();
    let kz_home = ws.join(".kzhome");
    let salt_hex = "00".repeat(32);
    let endpoint = format!("http://{}", addr);
    let (_data_dir, db, cfg) = {
        let _guard = env_lock().lock().unwrap();
        unsafe { std::env::set_var("KAIZEN_HOME", &kz_home) };
        let d = kaizen::core::paths::project_data_dir(&ws).unwrap();
        std::fs::write(
            d.join("config.toml"),
            format!(
                r#"[sync]
endpoint = "{endpoint}"
team_token = "test-token"
team_id = "team-1"
team_salt_hex = "{salt_hex}"
"#
            ),
        )
        .unwrap();
        let c = kaizen::core::config::load(&ws).unwrap();
        unsafe { std::env::remove_var("KAIZEN_HOME") };
        let db = d.join("kaizen.db");
        (d, db, c)
    };
    let salt = kaizen::core::config::try_team_salt(&cfg.sync).unwrap();
    let ctx = kaizen::sync::ingest_ctx(&cfg, ws.clone()).unwrap();

    let store = Store::open(&db).unwrap();
    let ws = ws.as_path();
    let session = SessionRecord {
        id: "sess-1".into(),
        agent: "cursor".into(),
        model: Some("claude".into()),
        workspace: ws.to_string_lossy().into(),
        started_at_ms: 1,
        ended_at_ms: None,
        status: SessionStatus::Running,
        trace_path: "".into(),
        start_commit: None,
        end_commit: None,
        branch: None,
        dirty_start: None,
        dirty_end: None,
        repo_binding_source: None,
        prompt_fingerprint: None,
        parent_session_id: None,
        agent_version: None,
        os: None,
        arch: None,
        repo_file_count: None,
        repo_total_loc: None,
    };
    store.upsert_session(&session).unwrap();

    let ev = Event {
        session_id: "sess-1".into(),
        seq: 0,
        ts_ms: 99,
        ts_exact: true,
        kind: EventKind::ToolCall,
        source: EventSource::Hook,
        tool: Some("bash".into()),
        tool_call_id: Some("call-1".into()),
        tokens_in: None,
        tokens_out: None,
        reasoning_tokens: None,
        cost_usd_e6: None,
        stop_reason: None,
        latency_ms: None,
        ttft_ms: None,
        retry_count: None,
        context_used_tokens: None,
        context_max_tokens: None,
        cache_creation_tokens: None,
        cache_read_tokens: None,
        system_prompt_tokens: None,
        payload: serde_json::json!({
            "path": "/Users/alice/proj/secret.txt",
            "token": stub::TEST_SECRET_MARKER,
        }),
    };
    store.append_event_with_sync(&ev, Some(&ctx)).unwrap();
    let mut result = ev.clone();
    result.seq = 1;
    result.ts_ms = 1_100;
    result.kind = EventKind::ToolResult;
    store.append_event_with_sync(&result, Some(&ctx)).unwrap();

    let db_path = db.clone();
    let ws_path = ws.to_path_buf();
    let sync_cfg = cfg.sync.clone();
    tokio::task::spawn_blocking(move || {
        let store = Store::open(&db_path).unwrap();
        let flush = FlushExporters {
            telemetry: &TelemetryConfig::default(),
            registry: None,
        };
        kaizen::sync::flush_outbox_once(&store, &ws_path, &sync_cfg, &salt, &flush).unwrap();
    })
    .await
    .unwrap();

    let bodies = state.captured_bodies.lock().unwrap();
    assert_eq!(bodies.len(), 2, "expected events + tool-span POST bodies");

    let parsed = bodies
        .iter()
        .map(|body| serde_json::from_str::<serde_json::Value>(body).unwrap())
        .collect::<Vec<_>>();

    let event_body = parsed
        .iter()
        .find(|body| body.get("events").is_some())
        .expect("events batch");
    let tool_body = parsed
        .iter()
        .find(|body| body.get("spans").is_some())
        .expect("tool spans batch");

    let events = event_body["events"].as_array().expect("events array");
    assert_eq!(events.len(), 2);
    let payload_str = events[0]["payload"].to_string();
    assert!(
        !payload_str.contains("/Users/"),
        "payload leaked path: {payload_str}"
    );
    assert!(
        !payload_str.contains("sk-super-secret"),
        "payload leaked secret: {payload_str}"
    );

    let spans = tool_body["spans"].as_array().expect("spans array");
    assert_eq!(spans.len(), 1);
    let span_str = spans[0].to_string();
    assert!(
        !span_str.contains("/Users/"),
        "span leaked path: {span_str}"
    );
    assert!(
        !span_str.contains("sk-super-secret"),
        "span leaked secret: {span_str}"
    );
}

#[tokio::test]
async fn sync_flush_workspace_facts_hits_route() {
    let (app, state) = stub::router();
    let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
    let addr = listener.local_addr().unwrap();
    tokio::spawn(async move {
        axum::serve(listener, app).await.unwrap();
    });
    tokio::time::sleep(std::time::Duration::from_millis(80)).await;

    let tmp = tempfile::TempDir::new().unwrap();
    let ws = tmp.path().to_path_buf();
    let kz_home = ws.join(".kzhome");
    let salt_hex = "00".repeat(32);
    let endpoint = format!("http://{}", addr);
    let (_data_dir, db, cfg) = {
        let _guard = env_lock().lock().unwrap();
        unsafe { std::env::set_var("KAIZEN_HOME", &kz_home) };
        let d = kaizen::core::paths::project_data_dir(&ws).unwrap();
        std::fs::write(
            d.join("config.toml"),
            format!(
                r#"[sync]
endpoint = "{endpoint}"
team_token = "test-token"
team_id = "team-1"
team_salt_hex = "{salt_hex}"
"#
            ),
        )
        .unwrap();
        let c = kaizen::core::config::load(&ws).unwrap();
        unsafe { std::env::remove_var("KAIZEN_HOME") };
        let db = d.join("kaizen.db");
        (d, db, c)
    };
    let salt = kaizen::core::config::try_team_salt(&cfg.sync).unwrap();
    let store = Store::open(&db).unwrap();
    let fact = serde_json::json!({
        "skill_slugs": ["a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2"],
        "rule_slugs": ["b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3"]
    });
    store
        .replace_outbox_rows("w:test", "workspace_facts", &[fact.to_string()])
        .unwrap();

    let db_path = db.clone();
    let ws_path = ws.to_path_buf();
    let sync_cfg = cfg.sync.clone();
    tokio::task::spawn_blocking(move || {
        let store = Store::open(&db_path).unwrap();
        let flush = FlushExporters {
            telemetry: &TelemetryConfig::default(),
            registry: None,
        };
        kaizen::sync::flush_outbox_once(&store, &ws_path, &sync_cfg, &salt, &flush).unwrap();
    })
    .await
    .unwrap();

    let bodies = state.captured_bodies.lock().unwrap();
    assert_eq!(bodies.len(), 1, "expected one POST to workspace-facts");
    let v: serde_json::Value = serde_json::from_str(&bodies[0]).unwrap();
    let facts = v
        .get("facts")
        .and_then(|f| f.as_array())
        .expect("WorkspaceFacts body has `facts` array");
    assert_eq!(facts.len(), 1);
    assert!(facts[0].get("skill_slugs").is_some());
}
