//! End-to-end tests: build a realistic Pi session fixture on disk,
//! read it through PiConvo, and derive a Toolpath Path.

use std::fs;
use std::path::Path;
use tempfile::TempDir;
use toolpath_convo::ConversationProvider;
use toolpath_pi::{DeriveConfig, PathResolver, PiConvo};

const PROJECT_CWD: &str = "/Users/alex/demoproject";

/// Write a fixture JSONL for a small, realistic Pi session.
fn write_fixture(sessions_dir: &Path) {
    let encoded = "--Users-alex-demoproject--";
    let proj = sessions_dir.join(encoded);
    fs::create_dir_all(&proj).unwrap();

    let lines = [
        r#"{"type":"session","version":3,"id":"demo-session-1","timestamp":"2026-04-16T10:00:00Z","cwd":"/Users/alex/demoproject"}"#,
        r#"{"type":"message","id":"m1","timestamp":"2026-04-16T10:00:01Z","message":{"role":"user","content":"Write hello.rs","timestamp":1744797601000}}"#,
        r#"{"type":"message","id":"m2","parentId":"m1","timestamp":"2026-04-16T10:00:02Z","message":{"role":"assistant","content":[{"type":"text","text":"I'll write the file."},{"type":"toolCall","id":"tc-1","name":"write","arguments":{"path":"hello.rs","content":"fn main(){}"}}],"api":"anthropic","provider":"anthropic","model":"claude-sonnet-4-5","usage":{"input":100,"output":50,"cacheRead":0,"cacheWrite":0,"totalTokens":150,"cost":{"input":0.0,"output":0.0,"cacheRead":0.0,"cacheWrite":0.0,"total":0.0}},"stopReason":"toolUse","timestamp":1744797602000}}"#,
        r#"{"type":"message","id":"m3","parentId":"m2","timestamp":"2026-04-16T10:00:03Z","message":{"role":"toolResult","toolCallId":"tc-1","toolName":"write","content":[{"type":"text","text":"file written"}],"isError":false,"timestamp":1744797603000}}"#,
        r#"{"type":"message","id":"m4","parentId":"m3","timestamp":"2026-04-16T10:00:04Z","message":{"role":"assistant","content":[{"type":"text","text":"Done."}],"api":"anthropic","provider":"anthropic","model":"claude-sonnet-4-5","usage":{"input":120,"output":10,"cacheRead":100,"cacheWrite":0,"totalTokens":130,"cost":{"input":0.0,"output":0.0,"cacheRead":0.0,"cacheWrite":0.0,"total":0.0}},"stopReason":"stop","timestamp":1744797604000}}"#,
    ];

    fs::write(
        proj.join("2026-04-16_demo-session-1.jsonl"),
        lines.join("\n"),
    )
    .unwrap();
}

fn manager_for(temp: &TempDir) -> PiConvo {
    let sessions_dir = temp.path().join(".pi/agent/sessions");
    fs::create_dir_all(&sessions_dir).unwrap();
    let resolver = PathResolver::new().with_sessions_dir(&sessions_dir);
    PiConvo::with_resolver(resolver)
}

#[test]
fn test_list_projects_finds_fixture() {
    let temp = TempDir::new().unwrap();
    let manager = manager_for(&temp);
    write_fixture(manager.resolver().sessions_dir());
    let projects = manager.list_projects().unwrap();
    assert_eq!(projects, vec![PROJECT_CWD.to_string()]);
}

#[test]
fn test_list_sessions_finds_fixture() {
    let temp = TempDir::new().unwrap();
    let manager = manager_for(&temp);
    write_fixture(manager.resolver().sessions_dir());
    let sessions = manager.list_sessions(PROJECT_CWD).unwrap();
    assert_eq!(sessions.len(), 1);
    assert_eq!(sessions[0].id, "demo-session-1");
}

#[test]
fn test_read_session_parses_fixture() {
    let temp = TempDir::new().unwrap();
    let manager = manager_for(&temp);
    write_fixture(manager.resolver().sessions_dir());
    let session = manager.read_session(PROJECT_CWD, "demo-session-1").unwrap();
    assert_eq!(session.header.id, "demo-session-1");
    // 1 header + 4 messages
    assert_eq!(session.entries.len(), 5);
}

#[test]
fn test_to_view_produces_expected_turns() {
    let temp = TempDir::new().unwrap();
    let manager = manager_for(&temp);
    write_fixture(manager.resolver().sessions_dir());
    let session = manager.read_session(PROJECT_CWD, "demo-session-1").unwrap();
    let view = manager.to_view(&session);

    // Turn count: user + assistant + assistant = 3
    // (tool-result entries fold into the assistant's tool_uses[i].result;
    // they no longer surface as standalone turns.)
    assert_eq!(view.turns.len(), 3);
    assert_eq!(view.provider_id.as_deref(), Some("pi"));
    // files_changed should include "hello.rs"
    assert!(view.files_changed.iter().any(|f| f == "hello.rs"));
    // total_usage should aggregate both assistant turns: 100+120=220 input
    let total = view.total_usage.expect("expected total_usage");
    assert_eq!(total.input_tokens, Some(220));
}

#[test]
fn test_derive_path_from_fixture() {
    let temp = TempDir::new().unwrap();
    let manager = manager_for(&temp);
    write_fixture(manager.resolver().sessions_dir());
    let session = manager.read_session(PROJECT_CWD, "demo-session-1").unwrap();

    let path = toolpath_pi::derive_path(&session, &DeriveConfig::default());

    // Path has 3 steps (one per turn; tool-result entries fold into the
    // matching assistant turn rather than emitting standalone turns).
    assert_eq!(path.steps.len(), 3);
    // Path ID format.
    assert!(path.path.id.starts_with("path-pi-"));
    // Head matches the last turn's native id (which is the source entry id).
    assert_eq!(path.path.head, "m4");
    // Base URI derived from cwd.
    assert!(
        path.path
            .base
            .as_ref()
            .map(|b| b.uri.as_str())
            .unwrap_or("")
            .contains("/Users/alex/demoproject")
    );
    // files_changed makes it into meta.extra.
    let meta = path.meta.as_ref().expect("meta");
    let fc = meta.extra.get("files_changed").expect("files_changed");
    assert!(fc.to_string().contains("hello.rs"));
}

#[test]
fn test_derive_roundtrip_serde() {
    let temp = TempDir::new().unwrap();
    let manager = manager_for(&temp);
    write_fixture(manager.resolver().sessions_dir());
    let session = manager.read_session(PROJECT_CWD, "demo-session-1").unwrap();

    let path = toolpath_pi::derive_path(&session, &DeriveConfig::default());
    let doc = toolpath::v1::Graph::from_path(path);
    let json = doc.to_json_pretty().unwrap();
    let parsed = toolpath::v1::Graph::from_json(&json).unwrap();
    // Compare as structured JSON values — HashMap-based `extra`/`actors` have
    // non-deterministic key order when re-serialized, so a string compare is flaky.
    let a: serde_json::Value = serde_json::from_str(&json).unwrap();
    let b: serde_json::Value = serde_json::from_str(&parsed.to_json_pretty().unwrap()).unwrap();
    assert_eq!(a, b);
}

#[test]
fn test_conversation_provider_impl() {
    let temp = TempDir::new().unwrap();
    let manager = manager_for(&temp);
    write_fixture(manager.resolver().sessions_dir());

    let ids = manager.list_conversations(PROJECT_CWD).unwrap();
    assert_eq!(ids, vec!["demo-session-1".to_string()]);

    let view = manager
        .load_conversation(PROJECT_CWD, "demo-session-1")
        .unwrap();
    assert_eq!(view.id, "demo-session-1");

    let meta = manager
        .load_metadata(PROJECT_CWD, "demo-session-1")
        .unwrap();
    assert_eq!(meta.id, "demo-session-1");

    let all = manager.list_metadata(PROJECT_CWD).unwrap();
    assert_eq!(all.len(), 1);
}

#[test]
fn test_most_recent_session() {
    let temp = TempDir::new().unwrap();
    let manager = manager_for(&temp);
    write_fixture(manager.resolver().sessions_dir());
    let session = manager.most_recent_session(PROJECT_CWD).unwrap().unwrap();
    assert_eq!(session.header.id, "demo-session-1");
}

#[test]
fn test_most_recent_session_none_for_empty_project() {
    let temp = TempDir::new().unwrap();
    let manager = manager_for(&temp);
    let sessions_dir = manager.resolver().sessions_dir();
    fs::create_dir_all(sessions_dir.join("--empty--")).unwrap();
    let result = manager.most_recent_session("/empty").unwrap();
    assert!(result.is_none());
}
