//! Replay smoke tests using `klieo-core` test doubles.
//!
//! Plan-vs-live API note: the plan's verbatim test used
//! `FakeLlmClient::new("final-answer")` (treating the constructor argument as
//! the response text) and `FakeToolInvoker::with_response("calc", json!(...))`.
//! Live API:
//! - `FakeLlmClient::new(name)` sets the client name; programmed responses
//!   are pushed via `.with_steps(vec![FakeLlmStep::Text("...".into())])`.
//! - `FakeToolInvoker::new().with_tool(name, desc, |args| Ok(value))`.
//! - `ToolInvoker::invoke` requires a `ToolCtx` (built from `noop_bus()`).
//!
//! These tests adjust the call site, never `klieo-core`.

use klieo_core::ids::RunId;
use klieo_core::test_utils::{noop_bus, FakeLlmClient, FakeLlmStep, FakeToolInvoker};
use klieo_core::tool::ToolCtx;
use klieo_runlog::replay::replay;
use klieo_runlog::types::{RunLog, RunStatus, Step, StepKind, Usage};
use std::sync::Arc;
use std::time::Duration;

fn fixture_run_log() -> RunLog {
    RunLog {
        run_id: RunId::new(),
        agent: "writer".into(),
        started_at: chrono::Utc::now(),
        finished_at: Some(chrono::Utc::now()),
        status: RunStatus::Completed,
        steps: vec![
            Step {
                idx: 0,
                kind: StepKind::ToolCall,
                name: Some("calc".into()),
                input: serde_json::json!({"x": 2}),
                output: serde_json::json!({"y": 4}),
                error: None,
                latency: Duration::ZERO,
                span_id: None,
            },
            Step {
                idx: 1,
                kind: StepKind::LlmCall,
                name: Some("test-model".into()),
                input: serde_json::json!({"prompt": "hello"}),
                output: serde_json::json!("final-answer"),
                error: None,
                latency: Duration::ZERO,
                span_id: None,
            },
        ],
        tokens: Usage::default(),
        cost_estimate: None,
    }
}

fn ctx() -> ToolCtx {
    let (pubsub, _, kv, jobs) = noop_bus();
    ToolCtx::new(pubsub, kv, jobs)
}

#[tokio::test]
async fn replay_returns_final_llm_text_when_doubles_match() {
    let log = fixture_run_log();
    let llm = Arc::new(
        FakeLlmClient::new("test-model").with_steps(vec![FakeLlmStep::Text("final-answer".into())]),
    );
    let tools = Arc::new(
        FakeToolInvoker::new().with_tool("calc", "calculator", |_args| {
            Ok(serde_json::json!({"y": 4}))
        }),
    );
    let out = replay(&log, llm, tools, ctx()).await.unwrap();
    assert_eq!(out, "final-answer");
}

#[tokio::test]
async fn replay_fails_on_tool_mismatch() {
    let log = fixture_run_log();
    let llm = Arc::new(
        FakeLlmClient::new("test-model").with_steps(vec![FakeLlmStep::Text("final-answer".into())]),
    );
    let tools = Arc::new(
        FakeToolInvoker::new().with_tool("calc", "calculator", |_args| {
            Ok(serde_json::json!({"y": 99}))
        }),
    );
    let err = replay(&log, llm, tools, ctx()).await.unwrap_err();
    assert!(matches!(err, klieo_runlog::error::RunLogError::Replay(_)));
    assert!(
        err.to_string().contains("mismatch"),
        "error message must mention mismatch; got: {err}"
    );
}
