//! End-to-end cost-accounting smoke tests for `project_with_price_table`.

use klieo_core::ids::RunId;
use klieo_core::memory::Episode;
use klieo_runlog::projector::{project_with_llm_io, project_with_price_table};
use klieo_runlog::types::LlmIo;
use klieo_runlog::PriceTable;

#[test]
fn end_to_end_two_llm_calls_yields_summed_cost() {
    let eps = vec![
        Episode::Started {
            agent: "writer".into(),
        },
        Episode::llm_call(1500, 10),
        Episode::llm_call(800, 5),
        Episode::Completed,
    ];
    let io = vec![
        LlmIo {
            prompt: "hi".into(),
            completion: "hello".into(),
            model: Some("claude-sonnet-4-x".into()),
            provider: Some("anthropic".into()),
            prompt_tokens: Some(1000),
            completion_tokens: Some(500),
        },
        LlmIo {
            prompt: "hello".into(),
            completion: "world".into(),
            model: Some("gpt-4o".into()),
            provider: Some("openai".into()),
            prompt_tokens: Some(500),
            completion_tokens: Some(300),
        },
    ];
    let prices = PriceTable::with_default_rates();
    let log = project_with_price_table(RunId::new(), "writer", &eps, &io, &prices);
    let cost = log.cost_estimate.expect("cost should be populated");

    // Hand-compute:
    // - Anthropic Sonnet @ $0.003/1k prompt, $0.015/1k completion:
    //   prompt = 1000/1000 * 0.003 = 0.003
    //   completion = 500/1000 * 0.015 = 0.0075
    // - OpenAI gpt-4o @ $0.0025/1k prompt, $0.01/1k completion:
    //   prompt = 500/1000 * 0.0025 = 0.00125
    //   completion = 300/1000 * 0.01 = 0.003
    // total prompt = 0.003 + 0.00125 = 0.00425
    // total completion = 0.0075 + 0.003 = 0.0105
    // grand total = 0.01475
    assert!((cost.prompt_usd - 0.00425).abs() <= 1e-9);
    assert!((cost.completion_usd - 0.0105).abs() <= 1e-9);
    assert!((cost.total_usd - 0.01475).abs() <= 1e-9);
}

#[test]
fn family_alias_lookup_works_through_projector() {
    let eps = vec![
        Episode::Started {
            agent: "writer".into(),
        },
        Episode::llm_call(100, 1),
        Episode::Completed,
    ];
    let io = vec![LlmIo {
        prompt: "hi".into(),
        completion: "ok".into(),
        // Versioned ID — should resolve to claude-opus-4-x.
        model: Some("claude-opus-4-7".into()),
        provider: Some("anthropic".into()),
        prompt_tokens: Some(1000),
        completion_tokens: Some(1000),
    }];
    let prices = PriceTable::with_default_rates();
    let log = project_with_price_table(RunId::new(), "writer", &eps, &io, &prices);
    let cost = log.cost_estimate.expect("family-alias should yield cost");
    // Opus: prompt $0.015/1k, completion $0.075/1k → 0.015 + 0.075 = 0.09 total
    assert!((cost.prompt_usd - 0.015).abs() <= 1e-9);
    assert!((cost.completion_usd - 0.075).abs() <= 1e-9);
    assert!((cost.total_usd - 0.09).abs() <= 1e-9);
}

#[test]
fn partial_info_only_priced_steps_contribute() {
    let eps = vec![Episode::llm_call(1000, 1), Episode::llm_call(500, 1)];
    let io = vec![
        // Full info — should contribute.
        LlmIo {
            prompt: "a".into(),
            completion: "b".into(),
            model: Some("gpt-4o".into()),
            provider: Some("openai".into()),
            prompt_tokens: Some(1000),
            completion_tokens: Some(500),
        },
        // Missing prompt_tokens — should be skipped silently.
        LlmIo {
            prompt: "c".into(),
            completion: "d".into(),
            model: Some("gpt-4o".into()),
            provider: Some("openai".into()),
            prompt_tokens: None,
            completion_tokens: Some(500),
        },
    ];
    let prices = PriceTable::with_default_rates();
    let log = project_with_price_table(RunId::new(), "writer", &eps, &io, &prices);
    let cost = log
        .cost_estimate
        .expect("partial info should still yield cost");
    // Only first step contributes:
    //   prompt = 1000/1000 * 0.0025 = 0.0025
    //   completion = 500/1000 * 0.01 = 0.005
    //   total = 0.0075
    assert!((cost.prompt_usd - 0.0025).abs() <= 1e-9);
    assert!((cost.completion_usd - 0.005).abs() <= 1e-9);
    assert!((cost.total_usd - 0.0075).abs() <= 1e-9);
}

#[test]
fn all_missing_provider_yields_no_cost_estimate() {
    let eps = vec![Episode::llm_call(100, 1)];
    let io = vec![LlmIo {
        prompt: "a".into(),
        completion: "b".into(),
        model: Some("gpt-4o".into()),
        // provider missing
        provider: None,
        prompt_tokens: Some(1000),
        completion_tokens: Some(500),
    }];
    let prices = PriceTable::with_default_rates();
    let log = project_with_price_table(RunId::new(), "writer", &eps, &io, &prices);
    assert!(log.cost_estimate.is_none());
}

#[test]
fn project_with_llm_io_leaves_cost_estimate_none() {
    // Even with full info on LlmIo, project_with_llm_io passes an empty
    // PriceTable internally and so should not set cost_estimate.
    let eps = vec![Episode::llm_call(100, 1)];
    let io = vec![LlmIo {
        prompt: "a".into(),
        completion: "b".into(),
        model: Some("gpt-4o".into()),
        provider: Some("openai".into()),
        prompt_tokens: Some(1000),
        completion_tokens: Some(500),
    }];
    let log = project_with_llm_io(RunId::new(), "writer", &eps, &io);
    assert!(log.cost_estimate.is_none());
}

#[test]
fn ollama_zero_rate_still_sets_cost_estimate() {
    // Zero-rate lookup is still a successful lookup → cost_estimate Some
    // with all-zero fields.
    let eps = vec![Episode::llm_call(100, 1)];
    let io = vec![LlmIo {
        prompt: "a".into(),
        completion: "b".into(),
        model: Some("*".into()),
        provider: Some("ollama".into()),
        prompt_tokens: Some(1000),
        completion_tokens: Some(500),
    }];
    let prices = PriceTable::with_default_rates();
    let log = project_with_price_table(RunId::new(), "writer", &eps, &io, &prices);
    let cost = log.cost_estimate.expect("zero-rate hit still sets cost");
    assert_eq!(cost.prompt_usd, 0.0);
    assert_eq!(cost.completion_usd, 0.0);
    assert_eq!(cost.total_usd, 0.0);
}

#[test]
fn unknown_model_with_full_info_skips_cost() {
    let eps = vec![Episode::llm_call(100, 1)];
    let io = vec![LlmIo {
        prompt: "a".into(),
        completion: "b".into(),
        model: Some("imaginary-model".into()),
        provider: Some("imaginary-provider".into()),
        prompt_tokens: Some(1000),
        completion_tokens: Some(500),
    }];
    let prices = PriceTable::with_default_rates();
    let log = project_with_price_table(RunId::new(), "writer", &eps, &io, &prices);
    assert!(log.cost_estimate.is_none());
}
