use std::collections::HashSet;
use std::path::PathBuf;

use moraine_ingest_core::normalize::normalize_record;
use serde_json::Value;

fn fixture_path(name: &str) -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("..")
        .join("..")
        .join("fixtures")
        .join("kimi-cli")
        .join(name)
}

fn normalize_lines(name: &str) -> Vec<moraine_ingest_core::model::NormalizedRecord> {
    let path = fixture_path(name);
    let body = std::fs::read_to_string(&path).expect("read fixture");
    body.lines()
        .enumerate()
        .filter_map(|(idx, line)| {
            if line.trim().is_empty() {
                return None;
            }
            let record: Value = serde_json::from_str(line).expect("valid fixture line");
            Some(
                normalize_record(
                    &record,
                    "ci-kimi",
                    "kimi-cli",
                    path.to_str().unwrap(),
                    1,
                    1,
                    idx as u64 + 1,
                    idx as u64,
                    "",
                    "",
                    "",
                )
                .expect("kimi fixture normalizes"),
            )
        })
        .collect()
}

#[test]
fn kimi_wire_fixture_maps_messages_tools_and_tokens() {
    let rows = normalize_lines("wire.jsonl");
    assert!(rows.iter().all(|row| row.error_rows.is_empty()));

    // The leading `{"type":"metadata",...}` header is a per-file protocol
    // marker, not a session event — normalize_record returns an empty
    // NormalizedRecord for it so no raw/event rows are emitted.
    assert!(rows[0].raw_row.is_null());
    assert!(rows[0].event_rows.is_empty());

    assert_eq!(
        rows[1].raw_row.get("harness").and_then(Value::as_str),
        Some("kimi-cli")
    );
    assert_eq!(
        rows[1]
            .raw_row
            .get("inference_provider")
            .and_then(Value::as_str),
        Some("moonshot")
    );
    assert!(rows[1].session_hint.starts_with("kimi-cli:kimi-cli"));

    let user = &rows[1].event_rows[0];
    assert_eq!(
        user.get("event_kind").and_then(Value::as_str),
        Some("message")
    );
    assert_eq!(user.get("actor_kind").and_then(Value::as_str), Some("user"));
    assert_eq!(
        user.get("record_ts").and_then(Value::as_str),
        Some("2026-04-12T00:32:24.549974Z")
    );

    let reasoning = &rows[3].event_rows[0];
    assert_eq!(
        reasoning.get("event_kind").and_then(Value::as_str),
        Some("reasoning")
    );
    assert_eq!(
        reasoning.get("payload_type").and_then(Value::as_str),
        Some("reasoning")
    );
    assert_eq!(
        reasoning.get("content_types"),
        Some(&serde_json::json!(["reasoning"]))
    );
    assert_eq!(
        reasoning.get("has_reasoning").and_then(Value::as_u64),
        Some(1)
    );

    let tool_call = &rows[5].event_rows[0];
    assert_eq!(
        tool_call.get("event_kind").and_then(Value::as_str),
        Some("tool_call")
    );
    assert_eq!(
        tool_call.get("tool_name").and_then(Value::as_str),
        Some("ReadFile")
    );
    assert_eq!(rows[5].tool_rows.len(), 1);

    let tool_result = &rows[6].event_rows[0];
    assert_eq!(
        tool_result.get("event_kind").and_then(Value::as_str),
        Some("tool_result")
    );
    assert_eq!(rows[6].tool_rows.len(), 1);

    let usage = &rows[7].event_rows[0];
    assert_eq!(
        usage.get("payload_type").and_then(Value::as_str),
        Some("token_count")
    );
    // input_tokens = input_other + input_cache_read + input_cache_creation
    // (10 + 2 + 1). cache reads/writes are a subset of total input, not
    // additional on top — see issue #272.
    assert_eq!(usage.get("input_tokens").and_then(Value::as_u64), Some(13));
    assert_eq!(usage.get("output_tokens").and_then(Value::as_u64), Some(5));
    assert_eq!(
        usage.get("cache_read_tokens").and_then(Value::as_u64),
        Some(2)
    );
    assert_eq!(
        usage.get("cache_write_tokens").and_then(Value::as_u64),
        Some(1)
    );

    // Every emitted Kimi event row should carry the harness-slug placeholder
    // model ("kimi-cli") so they are visible in tokens-by-model analytics —
    // the wire schema does not expose the underlying model name.
    for record in &rows {
        for event in &record.event_rows {
            assert_eq!(
                event.get("model").and_then(Value::as_str),
                Some("kimi-cli"),
                "every kimi event should carry a placeholder model"
            );
        }
    }
}

#[test]
fn kimi_subagent_event_emits_raw_row_but_no_normalized_event() {
    // SubagentEvent envelopes on the parent wire duplicate events that are
    // already captured via the sub-agent's own wire.jsonl. They must not
    // produce a normalized event row (which would double-count every
    // sub-agent event as `progress` on the parent, see #271), but the raw
    // row is preserved so the byte-level trace remains intact.
    let rows = normalize_lines("wire.jsonl");
    let subagent_row = rows
        .iter()
        .find(|row| {
            row.raw_row
                .get("top_type")
                .and_then(Value::as_str)
                .map(|k| k == "SubagentEvent")
                .unwrap_or(false)
        })
        .expect("fixture contains a SubagentEvent record");

    assert!(!subagent_row.raw_row.is_null());
    assert!(subagent_row.event_rows.is_empty());
    assert!(subagent_row.tool_rows.is_empty());
    assert!(subagent_row.error_rows.is_empty());
}

#[test]
fn kimi_events_do_not_reuse_raw_record_uid() {
    let rows = normalize_lines("wire.jsonl");
    let mut event_uids = HashSet::new();

    for row in &rows {
        // Skipped records (e.g. the metadata header) have no raw_row; ignore
        // them here — they also have no event_rows to check.
        let Some(raw_uid) = row.raw_row.get("event_uid").and_then(Value::as_str) else {
            assert!(row.event_rows.is_empty());
            continue;
        };
        for event in &row.event_rows {
            let event_uid = event
                .get("event_uid")
                .and_then(Value::as_str)
                .expect("event uid");
            assert_ne!(raw_uid, event_uid);
            assert!(event_uids.insert(event_uid.to_string()));
        }
    }
}
