use std::{
    fs::{self, OpenOptions},
    io::Write,
};

use cc_pulseline::{config::RenderConfig, PulseLineRunner};
use serde_json::json;
use tempfile::TempDir;

fn append_line(path: &std::path::Path, line: &str) {
    let mut file = OpenOptions::new()
        .create(true)
        .append(true)
        .open(path)
        .expect("transcript file should open");
    writeln!(file, "{line}").expect("line should append");
}

fn payload_json(
    workspace: &TempDir,
    transcript_path: &std::path::Path,
    session_id: &str,
) -> String {
    json!({
        "session_id": session_id,
        "cwd": workspace.path(),
        "workspace": {"current_dir": workspace.path()},
        "model": {"display_name": "Opus"},
        "output_style": {"name": "concise"},
        "version": "2.2.0",
        "transcript_path": transcript_path,
        "context_window": {
            "context_window_size": 200000,
            "used_percentage": 43,
            "current_usage": {
                "input_tokens": 10000,
                "output_tokens": 20000,
                "cache_creation_input_tokens": 30000,
                "cache_read_input_tokens": 40000
            }
        },
        "cost": {
            "total_cost_usd": 3.50,
            "total_duration_ms": 3600000
        }
    })
    .to_string()
}

fn incomplete_payload_json(
    workspace: &TempDir,
    transcript_path: &std::path::Path,
    session_id: &str,
) -> String {
    json!({
        "session_id": session_id,
        "cwd": workspace.path(),
        "workspace": {"current_dir": workspace.path()},
        "model": {"display_name": "Opus"},
        "output_style": {"name": "concise"},
        "version": "2.2.0",
        "transcript_path": transcript_path,
        "cost": {
            "total_cost_usd": 4.00,
            "total_duration_ms": 4000000
        }
    })
    .to_string()
}

// ── L3 persistence tests ────────────────────────────────────────────

#[test]
fn l3_values_persist_across_fresh_runners_via_cache() {
    let workspace = TempDir::new().expect("temp workspace");
    let transcript = workspace.path().join("cache-l3.jsonl");
    fs::write(&transcript, "").unwrap();

    let config = RenderConfig {
        transcript_poll_throttle_ms: 0,
        ..RenderConfig::default()
    };

    // Runner 1: complete payload with L3 data
    {
        let mut runner = PulseLineRunner::default();
        let lines = runner
            .run_from_str(
                &payload_json(&workspace, &transcript, "cache-l3-test"),
                config.clone(),
            )
            .expect("render should succeed");
        let joined = lines.join("\n");
        // Context segment now `<used>/<total>` — 200k × 43% = 86k.
        assert!(
            joined.contains(" 86.0k") && joined.contains("/200.0k"),
            "first invocation should show context: got {joined}"
        );
        assert!(
            !joined.contains("--/--"),
            "first invocation should have real context data: got {joined}"
        );
    }

    // Runner 2: partial payload (has cost, no context) → has_data() is true,
    // so we trust the payload as-is — CTX shows NA, cost shows $4.00.
    {
        let mut runner = PulseLineRunner::default();
        let lines = runner
            .run_from_str(
                &incomplete_payload_json(&workspace, &transcript, "cache-l3-test"),
                config,
            )
            .expect("render should succeed");
        let joined = lines.join("\n");
        assert!(
            joined.contains("--/--"),
            "partial payload should show context skeleton (no field-level merge): got {joined}"
        );
        assert!(
            joined.contains("$4.00"),
            "cost should use current payload value: got {joined}"
        );
    }
}

#[test]
fn empty_l3_payload_falls_back_to_cached_values() {
    let workspace = TempDir::new().expect("temp workspace");
    let transcript = workspace.path().join("cache-l3-empty.jsonl");
    fs::write(&transcript, "").unwrap();

    let config = RenderConfig {
        transcript_poll_throttle_ms: 0,
        ..RenderConfig::default()
    };

    // Runner 1: complete payload with L3 data
    {
        let mut runner = PulseLineRunner::default();
        let lines = runner
            .run_from_str(
                &payload_json(&workspace, &transcript, "cache-l3-empty-test"),
                config.clone(),
            )
            .expect("render should succeed");
        let joined = lines.join("\n");
        assert!(
            joined.contains(" 86.0k") && joined.contains("/200.0k"),
            "first run should show context: got {joined}"
        );
        assert!(
            joined.contains("$3.50"),
            "first run should show cost: got {joined}"
        );
    }

    // Runner 2: truly empty L3 (no context, no cost) → has_data() is false,
    // so we fall back to the cached L3 from Runner 1.
    {
        let empty_l3_payload = json!({
            "session_id": "cache-l3-empty-test",
            "cwd": workspace.path(),
            "workspace": {"current_dir": workspace.path()},
            "model": {"display_name": "Opus"},
            "output_style": {"name": "concise"},
            "version": "2.2.0",
            "transcript_path": &transcript
        })
        .to_string();

        let mut runner = PulseLineRunner::default();
        let lines = runner
            .run_from_str(&empty_l3_payload, config)
            .expect("render should succeed");
        let joined = lines.join("\n");
        assert!(
            joined.contains(" 86.0k") && joined.contains("/200.0k"),
            "empty L3 should fall back to cached context: got {joined}"
        );
        assert!(
            joined.contains("$3.50"),
            "empty L3 should fall back to cached cost: got {joined}"
        );
    }
}

// ── Transcript offset persistence tests ─────────────────────────────

#[test]
fn transcript_offset_persists_across_fresh_runners() {
    let workspace = TempDir::new().expect("temp workspace");
    let transcript = workspace.path().join("cache-offset.jsonl");

    let config = RenderConfig {
        transcript_poll_throttle_ms: 0,
        ..RenderConfig::default()
    };

    // Runner 1: append a tool_use, render to establish offset
    append_line(
        &transcript,
        r#"{"type":"tool_use","tool_use_id":"tool-1","name":"Bash"}"#,
    );
    {
        let mut runner = PulseLineRunner::default();
        let lines = runner
            .run_from_str(
                &payload_json(&workspace, &transcript, "cache-offset-test"),
                config.clone(),
            )
            .expect("render should succeed");
        let joined = lines.join("\n");
        assert!(
            joined.contains("T:Bash"),
            "first run should show Bash tool: got {joined}"
        );
    }

    // Runner 2: append tool_result (offset should continue from cached position)
    append_line(
        &transcript,
        r#"{"type":"tool_result","tool_use_id":"tool-1"}"#,
    );
    {
        let mut runner = PulseLineRunner::default();
        let lines = runner
            .run_from_str(
                &payload_json(&workspace, &transcript, "cache-offset-test"),
                config,
            )
            .expect("render should succeed");
        let joined = lines.join("\n");
        // The second runner loaded the cache with recent_tools from runner 1,
        // then read only new lines (tool_result) which records the completion.
        // Tool persists in recent_tools for display, and completed count appears.
        assert!(
            joined.contains("T:Bash"),
            "second run should still show Bash in recent tools: got {joined}"
        );
        assert!(
            joined.contains("✓ Bash"),
            "completed tool should appear: got {joined}"
        );
    }
}

// ── Transcript file truncation test ─────────────────────────────────

#[test]
fn transcript_truncation_resets_state() {
    let workspace = TempDir::new().expect("temp workspace");
    let transcript = workspace.path().join("cache-truncate.jsonl");

    let config = RenderConfig {
        transcript_poll_throttle_ms: 0,
        ..RenderConfig::default()
    };

    // Runner 1: write lots of events
    append_line(
        &transcript,
        r#"{"type":"tool_use","tool_use_id":"t1","name":"Read"}"#,
    );
    append_line(&transcript, r#"{"type":"tool_result","tool_use_id":"t1"}"#);
    append_line(
        &transcript,
        r#"{"type":"tool_use","tool_use_id":"t2","name":"Bash"}"#,
    );
    {
        let mut runner = PulseLineRunner::default();
        runner
            .run_from_str(
                &payload_json(&workspace, &transcript, "cache-trunc-test"),
                config.clone(),
            )
            .expect("render should succeed");
    }

    // Truncate transcript (simulates file replacement)
    fs::write(
        &transcript,
        r#"{"type":"tool_use","tool_use_id":"t3","name":"Glob"}"#,
    )
    .unwrap();

    // Runner 2: should detect truncation, reset offset, re-read from beginning
    {
        let mut runner = PulseLineRunner::default();
        let lines = runner
            .run_from_str(
                &payload_json(&workspace, &transcript, "cache-trunc-test"),
                config,
            )
            .expect("render should succeed");
        let joined = lines.join("\n");
        assert!(
            joined.contains("T:Glob"),
            "after truncation should see new tool: got {joined}"
        );
        // Old state (✓Read, T:Bash) should be gone since offset reset clears state
        assert!(
            !joined.contains("T:Bash"),
            "old tools should be cleared after truncation: got {joined}"
        );
    }
}

// ── Completed agent display tests ───────────────────────────────────

#[test]
fn completed_agents_persist_across_runners() {
    let workspace = TempDir::new().expect("temp workspace");
    let transcript = workspace.path().join("cache-agent.jsonl");

    let config = RenderConfig {
        transcript_poll_throttle_ms: 0,
        max_agent_lines: 3,
        ..RenderConfig::default()
    };

    // Runner 1: start agent, then complete it
    append_line(
        &transcript,
        r#"{"type":"progress","data":{"type":"agent_progress","agentId":"a1","status":"running","description":"Search code","agentType":"Explore"},"timestamp":"2026-01-18T10:58:40.000Z"}"#,
    );
    append_line(
        &transcript,
        r#"{"type":"progress","data":{"type":"agent_progress","agentId":"a1","status":"completed"},"timestamp":"2026-01-18T10:59:10.000Z"}"#,
    );
    {
        let mut runner = PulseLineRunner::default();
        let lines = runner
            .run_from_str(
                &payload_json(&workspace, &transcript, "cache-agent-test"),
                config.clone(),
            )
            .expect("render should succeed");
        let joined = lines.join("\n");
        assert!(
            joined.contains("[done]"),
            "completed agent should show [done]: got {joined}"
        );
    }

    // Runner 2: should see the completed agent from cache
    {
        let mut runner = PulseLineRunner::default();
        let lines = runner
            .run_from_str(
                &payload_json(&workspace, &transcript, "cache-agent-test"),
                config,
            )
            .expect("render should succeed");
        let joined = lines.join("\n");
        assert!(
            joined.contains("Explore") && joined.contains("[done]"),
            "cached completed agent should persist: got {joined}"
        );
    }
}

// ── Model tag tests ─────────────────────────────────────────────────

#[test]
fn model_tag_appears_for_task_with_model() {
    let workspace = TempDir::new().expect("temp workspace");
    let transcript = workspace.path().join("model-tag.jsonl");

    let config = RenderConfig {
        transcript_poll_throttle_ms: 0,
        ..RenderConfig::default()
    };

    // Agent tool_use with model field → goes to pending queue
    append_line(
        &transcript,
        r#"{"message":{"role":"assistant","content":[{"type":"tool_use","id":"task-m1","name":"Agent","input":{"description":"Search auth code","subagent_type":"Explore","model":"haiku"}}]},"timestamp":"2026-01-18T11:00:00.000Z"}"#,
    );
    // agent_progress → links to pending task, inherits model from Agent
    append_line(
        &transcript,
        r#"{"type":"progress","data":{"type":"agent_progress","agentId":"a-model-1","prompt":"Searching auth code in the codebase"},"timestamp":"2026-01-18T11:00:01.000Z"}"#,
    );

    let mut runner = PulseLineRunner::default();
    let lines = runner
        .run_from_str(
            &payload_json(&workspace, &transcript, "model-tag-test"),
            config,
        )
        .expect("render should succeed");
    let joined = lines.join("\n");
    assert!(
        joined.contains("A:Explore"),
        "agent should appear: got {joined}"
    );
    assert!(
        joined.contains("[haiku]"),
        "model tag should appear: got {joined}"
    );
}

// ── Running + completed priority test ───────────────────────────────

#[test]
fn running_agents_take_priority_over_completed() {
    let workspace = TempDir::new().expect("temp workspace");
    let transcript = workspace.path().join("priority.jsonl");

    let config = RenderConfig {
        transcript_poll_throttle_ms: 0,
        max_agent_lines: 2,
        ..RenderConfig::default()
    };

    // Start 3 agents, complete 1
    append_line(
        &transcript,
        r#"{"type":"progress","data":{"type":"agent_progress","agentId":"a1","status":"running","description":"First task","agentType":"Explore"},"timestamp":"2026-01-18T11:00:00.000Z"}"#,
    );
    append_line(
        &transcript,
        r#"{"type":"progress","data":{"type":"agent_progress","agentId":"a2","status":"running","description":"Second task","agentType":"Plan"},"timestamp":"2026-01-18T11:00:01.000Z"}"#,
    );
    // Complete a1
    append_line(
        &transcript,
        r#"{"type":"progress","data":{"type":"agent_progress","agentId":"a1","status":"completed"},"timestamp":"2026-01-18T11:00:30.000Z"}"#,
    );

    // Now: 1 active (a2), 1 completed (a1). With max 2, both should show.
    // But active comes first.
    let mut runner = PulseLineRunner::default();
    let lines = runner
        .run_from_str(
            &payload_json(&workspace, &transcript, "priority-test"),
            config.clone(),
        )
        .expect("render should succeed");
    let agent_lines: Vec<&String> = lines.iter().filter(|l| l.starts_with("A:")).collect();
    assert_eq!(
        agent_lines.len(),
        2,
        "should show 2 agents (1 running + 1 completed): got {agent_lines:?}"
    );

    // Start a 3rd agent → now 2 active (a2, a3) + 1 completed (a1), max 2 → only running
    append_line(
        &transcript,
        r#"{"type":"progress","data":{"type":"agent_progress","agentId":"a3","status":"running","description":"Third task","agentType":"Bash"},"timestamp":"2026-01-18T11:01:00.000Z"}"#,
    );
    let lines = runner
        .run_from_str(
            &payload_json(&workspace, &transcript, "priority-test"),
            config,
        )
        .expect("render should succeed");
    let agent_lines: Vec<&String> = lines.iter().filter(|l| l.starts_with("A:")).collect();
    assert_eq!(
        agent_lines.len(),
        2,
        "should cap at 2 agents: got {agent_lines:?}"
    );
    // Both should be running (no [done])
    assert!(
        agent_lines.iter().all(|l| !l.contains("[done]")),
        "running agents should take priority over completed: got {agent_lines:?}"
    );
}

// ── Completed agent elapsed uses fixed duration ─────────────────────

#[test]
fn completed_agent_elapsed_is_fixed() {
    let workspace = TempDir::new().expect("temp workspace");
    let transcript = workspace.path().join("elapsed.jsonl");

    let config = RenderConfig {
        transcript_poll_throttle_ms: 0,
        max_agent_lines: 2,
        ..RenderConfig::default()
    };

    // Agent runs for 45 seconds (started at T+0, completed at T+45s)
    append_line(
        &transcript,
        r#"{"type":"progress","data":{"type":"agent_progress","agentId":"a1","status":"running","description":"Quick task","agentType":"Explore"},"timestamp":"2026-01-18T11:00:00.000Z"}"#,
    );
    append_line(
        &transcript,
        r#"{"type":"progress","data":{"type":"agent_progress","agentId":"a1","status":"completed"},"timestamp":"2026-01-18T11:00:45.000Z"}"#,
    );

    let mut runner = PulseLineRunner::default();
    let lines = runner
        .run_from_str(
            &payload_json(&workspace, &transcript, "elapsed-test"),
            config,
        )
        .expect("render should succeed");
    let joined = lines.join("\n");
    // The elapsed should be based on started_at epoch, not now-started_at.
    // Since the agent started_at is an ISO timestamp from 2026, the elapsed
    // relative to the completion time would be ~45s.
    // The completed_at is set by SystemTime::now() in remove_agent, not from the event.
    // So we just check it shows [done] with some elapsed time.
    assert!(
        joined.contains("[done]"),
        "completed agent should show [done]: got {joined}"
    );
}
