//! Projection smoke tests: `Vec<Episode>` → `RunLog`.

use klieo_core::ids::RunId;
use klieo_core::memory::{Episode, ToolResult};
use klieo_runlog::projector::{project, project_with_llm_io};
use klieo_runlog::types::{LlmIo, RunStatus, StepKind};

#[test]
fn empty_episodes_yield_running_run() {
    let log = project(RunId::new(), "writer", &[]);
    assert_eq!(log.status, RunStatus::Running);
    assert!(log.steps.is_empty());
    assert!(log.finished_at.is_none());
}

#[test]
fn started_then_completed_yields_completed_status() {
    let eps = vec![
        Episode::Started {
            agent: "writer".into(),
        },
        Episode::Completed,
    ];
    let log = project(RunId::new(), "writer", &eps);
    assert_eq!(log.status, RunStatus::Completed);
    assert!(log.finished_at.is_some());
    assert!(log.steps.is_empty());
}

#[test]
fn llm_call_episode_yields_one_llm_step() {
    let eps = vec![
        Episode::Started {
            agent: "writer".into(),
        },
        Episode::llm_call(42, 17),
        Episode::Completed,
    ];
    let log = project(RunId::new(), "writer", &eps);
    assert_eq!(log.steps.len(), 1);
    assert_eq!(log.steps[0].kind, StepKind::LlmCall);
    assert_eq!(log.steps[0].latency.as_millis(), 17);
    // T3 leaves input/output as Null until klieo-core enriches Episode::LlmCall.
    assert!(log.steps[0].input.is_null());
    assert!(log.steps[0].output.is_null());
    // Aggregate tokens accumulate; we don't know the prompt/completion split yet,
    // so all 42 land in completion_tokens until enrichment lands.
    assert_eq!(log.tokens.completion_tokens, 42);
}

#[test]
fn llm_call_episode_with_split_tokens_routes_prompt_and_completion() {
    let eps = vec![
        Episode::Started {
            agent: "writer".into(),
        },
        Episode::LlmCall {
            tokens: 60,
            latency_ms: 17,
            provider: Some("ollama".into()),
            model: Some("qwen2.5:14b".into()),
            prompt_tokens: Some(40),
            completion_tokens: Some(20),
        },
        Episode::Completed,
    ];
    let log = project(RunId::new(), "writer", &eps);
    assert_eq!(log.steps.len(), 1);
    assert_eq!(log.steps[0].kind, StepKind::LlmCall);
    // No LlmIo sidecar: name falls back to Episode::LlmCall::model.
    assert_eq!(log.steps[0].name.as_deref(), Some("qwen2.5:14b"));
    assert_eq!(log.tokens.prompt_tokens, 40);
    assert_eq!(log.tokens.completion_tokens, 20);
}

#[test]
fn tool_call_error_populates_step_error() {
    let eps = vec![
        Episode::Started {
            agent: "writer".into(),
        },
        Episode::ToolCall {
            name: "calc".into(),
            args: serde_json::json!({"x": 1}),
            result: ToolResult::Err {
                message: "boom".into(),
            },
        },
        Episode::Failed {
            error: "boom".into(),
        },
    ];
    let log = project(RunId::new(), "writer", &eps);
    assert_eq!(log.status, RunStatus::Failed);
    assert_eq!(log.steps.len(), 1);
    assert_eq!(log.steps[0].kind, StepKind::ToolCall);
    assert_eq!(log.steps[0].name.as_deref(), Some("calc"));
    assert!(log.steps[0].error.is_some());
}

#[test]
fn llm_io_sidecar_populates_input_output_and_name() {
    let eps = vec![
        Episode::Started {
            agent: "writer".into(),
        },
        Episode::llm_call(7, 1),
        Episode::Completed,
    ];
    let io = vec![LlmIo {
        prompt: "hello".into(),
        completion: "world".into(),
        model: Some("test-model".into()),
        ..LlmIo::default()
    }];
    let log = project_with_llm_io(RunId::new(), "writer", &eps, &io);
    assert_eq!(log.steps.len(), 1);
    let step = &log.steps[0];
    assert_eq!(step.kind, StepKind::LlmCall);
    assert_eq!(step.name.as_deref(), Some("test-model"));
    assert_eq!(
        step.input.get("prompt").and_then(|v| v.as_str()),
        Some("hello")
    );
    assert_eq!(step.output.as_str(), Some("world"));
}

#[test]
fn llm_io_sidecar_shorter_than_episodes_falls_back_to_null() {
    let eps = vec![Episode::llm_call(1, 1), Episode::llm_call(1, 1)];
    let io = vec![LlmIo {
        prompt: "only-first".into(),
        completion: "ok".into(),
        model: None,
        ..LlmIo::default()
    }];
    let log = project_with_llm_io(RunId::new(), "writer", &eps, &io);
    assert_eq!(log.steps.len(), 2);
    assert_eq!(
        log.steps[0].input.get("prompt").and_then(|v| v.as_str()),
        Some("only-first")
    );
    assert!(log.steps[1].input.is_null());
    assert!(log.steps[1].output.is_null());
}

#[tokio::test]
async fn projected_llm_io_round_trips_through_replay() {
    use klieo_core::test_utils::{noop_bus, FakeLlmClient, FakeLlmStep, FakeToolInvoker};
    use klieo_core::tool::ToolCtx;
    use klieo_runlog::replay::replay;
    use std::sync::Arc;

    let eps = vec![
        Episode::Started {
            agent: "writer".into(),
        },
        Episode::llm_call(3, 2),
        Episode::Completed,
    ];
    let io = vec![LlmIo {
        prompt: "hello".into(),
        completion: "world".into(),
        model: Some("test-model".into()),
        ..LlmIo::default()
    }];
    let log = project_with_llm_io(RunId::new(), "writer", &eps, &io);

    let llm = Arc::new(
        FakeLlmClient::new("test-model").with_steps(vec![FakeLlmStep::Text("world".into())]),
    );
    let tools = Arc::new(FakeToolInvoker::new());
    let (pubsub, _, kv, jobs) = noop_bus();
    let ctx = ToolCtx::new(pubsub, kv, jobs);

    let out = replay(&log, llm, tools, ctx).await.unwrap();
    assert_eq!(out, "world");
}

#[test]
fn multiple_steps_preserve_order_and_idx() {
    let eps = vec![
        Episode::Started {
            agent: "writer".into(),
        },
        Episode::llm_call(10, 5),
        Episode::ToolCall {
            name: "t1".into(),
            args: serde_json::Value::Null,
            result: ToolResult::Ok {
                value: serde_json::json!("ok"),
            },
        },
        Episode::llm_call(20, 8),
        Episode::Completed,
    ];
    let log = project(RunId::new(), "writer", &eps);
    let kinds: Vec<_> = log.steps.iter().map(|s| s.kind).collect();
    assert_eq!(
        kinds,
        vec![StepKind::LlmCall, StepKind::ToolCall, StepKind::LlmCall]
    );
    let idxs: Vec<_> = log.steps.iter().map(|s| s.idx).collect();
    assert_eq!(idxs, vec![0, 1, 2]);
    assert_eq!(log.tokens.completion_tokens, 30);
}
