//! Compaction policy smoke tests. Deterministic by default; LLM gated.

use klieo_core::ids::RunId;
use klieo_runlog::compaction::{CompactionPolicy, DeterministicCompaction};
use klieo_runlog::projector::project;
use klieo_runlog::types::{Step, StepKind};
use std::time::Duration;

fn step(idx: u32, kind: StepKind) -> Step {
    Step {
        idx,
        kind,
        name: None,
        input: serde_json::Value::Null,
        output: serde_json::Value::Null,
        error: None,
        latency: Duration::ZERO,
        span_id: None,
    }
}

#[tokio::test]
async fn deterministic_default_keeps_all_under_threshold() {
    let mut log = project(RunId::new(), "writer", &[]);
    for i in 0..10 {
        log.steps.push(step(i, StepKind::LlmCall));
    }
    DeterministicCompaction::default()
        .compact(&mut log)
        .await
        .unwrap();
    assert_eq!(log.steps.len(), 10);
}

#[tokio::test]
async fn deterministic_truncates_over_threshold_with_summary_step() {
    let mut log = project(RunId::new(), "writer", &[]);
    for i in 0..150 {
        log.steps.push(step(i, StepKind::LlmCall));
    }
    let policy = DeterministicCompaction {
        max_steps: 100,
        drop_kinds: vec![],
    };
    policy.compact(&mut log).await.unwrap();
    // Expect 1 summary step + 100 most-recent steps = 101.
    assert_eq!(log.steps.len(), 101);
    assert_eq!(log.steps[0].kind, StepKind::ToolCall);
    assert_eq!(log.steps[0].name.as_deref(), Some("compacted"));
    assert_eq!(log.steps[0].input["dropped_count"], 50);
}

#[tokio::test]
async fn deterministic_drops_kinds() {
    let mut log = project(RunId::new(), "writer", &[]);
    for i in 0..10 {
        log.steps.push(step(i, StepKind::LlmCall));
        log.steps.push(step(i, StepKind::ToolCall));
    }
    let policy = DeterministicCompaction {
        max_steps: 100,
        drop_kinds: vec![StepKind::ToolCall],
    };
    policy.compact(&mut log).await.unwrap();
    assert!(log.steps.iter().all(|s| s.kind == StepKind::LlmCall));
    assert_eq!(log.steps.len(), 10);
}

#[tokio::test]
async fn deterministic_reindexes_after_compaction() {
    let mut log = project(RunId::new(), "writer", &[]);
    for i in 0..150 {
        log.steps.push(step(i, StepKind::LlmCall));
    }
    let policy = DeterministicCompaction {
        max_steps: 100,
        drop_kinds: vec![],
    };
    policy.compact(&mut log).await.unwrap();
    let idxs: Vec<u32> = log.steps.iter().map(|s| s.idx).collect();
    assert_eq!(idxs.first().copied(), Some(0));
    for w in idxs.windows(2) {
        assert_eq!(w[1], w[0] + 1);
    }
}

#[cfg(feature = "compaction-llm")]
#[tokio::test]
async fn llm_compaction_replaces_older_with_summary() {
    use klieo_core::test_utils::{FakeLlmClient, FakeLlmStep};
    use klieo_runlog::compaction::LlmCompaction;
    use std::sync::Arc;

    // Live API: FakeLlmClient::new(name) sets the name; programmed responses
    // are pushed via .with_steps(...) — adjust per the plan's verbatim test
    // (which assumed FakeLlmClient::new returned the response text directly).
    let llm = Arc::new(
        FakeLlmClient::new("test-llm").with_steps(vec![FakeLlmStep::Text("summary-text".into())]),
    );
    let mut log = project(RunId::new(), "writer", &[]);
    for i in 0..150 {
        log.steps.push(step(i, StepKind::LlmCall));
    }
    let policy = LlmCompaction {
        llm,
        max_steps: 100,
    };
    policy.compact(&mut log).await.unwrap();
    assert_eq!(log.steps.len(), 101);
    assert_eq!(log.steps[0].name.as_deref(), Some("compacted"));
    assert!(log.steps[0]
        .output
        .as_str()
        .map(|s| s.contains("summary-text"))
        .unwrap_or(false));
}
