#![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]

//! Integration tests for the Cursor SQLite chat-store harness.

use chrono::{DateTime, Utc};
use serde_json::json;
use txcript::{
    Block, Codec, Common, Cursor, CursorBlob, Message, Meta, Role, TextCodec, Tool, ToolOutput,
    Transcript,
};
#[cfg(feature = "opencode")]
use txcript::{CursorStore, Store};

fn ts(s: &str) -> DateTime<Utc> {
    s.parse().unwrap()
}

fn sample_common() -> Transcript<Common> {
    let meta = Meta {
        id: "sess-1".into(),
        timestamp: ts("2026-01-02T03:04:05.000Z"),
        cwd: Some("/repo".into()),
        git_branch: None,
        title: Some("Cursor demo".into()),
        cli_version: None,
        model: Some("composer-2.5-fast".into()),
    };
    Transcript::new(
        meta,
        vec![
            Message {
                role: Role::User,
                content: vec![Block::Text {
                    text: "read the file".into(),
                }],
                timestamp: ts("2026-01-02T03:04:06.000Z"),
                model: None,
                stop_reason: None,
                usage: None,
            },
            Message {
                role: Role::Assistant,
                content: vec![
                    Block::Thinking {
                        text: String::new(),
                        signature: None,
                        encrypted: Some("opaque-reasoning".into()),
                    },
                    Block::Text {
                        text: "I'll inspect it.".into(),
                    },
                    Block::ToolUse {
                        id: "tool-1".into(),
                        tool: Tool::Read {
                            file_path: "/repo/README.md".into(),
                            offset: None,
                            limit: None,
                        },
                    },
                ],
                timestamp: ts("2026-01-02T03:04:07.000Z"),
                model: Some("composer-2.5-fast".into()),
                stop_reason: None,
                usage: None,
            },
            Message {
                role: Role::User,
                content: vec![Block::ToolResult {
                    tool_use_id: "tool-1".into(),
                    content: ToolOutput::Text("contents".into()),
                    is_error: false,
                }],
                timestamp: ts("2026-01-02T03:04:08.000Z"),
                model: None,
                stop_reason: None,
                usage: None,
            },
            Message {
                role: Role::Assistant,
                content: vec![Block::Text {
                    text: "README loaded.".into(),
                }],
                timestamp: ts("2026-01-02T03:04:09.000Z"),
                model: Some("composer-2.5-fast".into()),
                stop_reason: None,
                usage: None,
            },
        ],
    )
}

#[test]
#[cfg(feature = "opencode")]
fn save_uses_cursor_chat_store_path() {
    let dir = tempfile::tempdir().unwrap();
    let common = sample_common();
    let native = Cursor::from_common(&common).unwrap();
    let saved = CursorStore::new(dir.path()).save(&native).unwrap();

    assert_eq!(saved.id, "sess-1");
    assert_eq!(
        saved.reference,
        dir.path()
            .join("6530f9eb448d96e7552a3c3a29b6cd2b")
            .join("sess-1")
            .join("store.db")
    );
    assert!(saved.reference.is_file());
    assert!(
        saved
            .reference
            .parent()
            .unwrap()
            .join("meta.json")
            .is_file()
    );
    assert!(
        saved
            .reference
            .parent()
            .unwrap()
            .join("prompt_history.json")
            .is_file()
    );
}

#[test]
#[cfg(feature = "opencode")]
fn discover_and_load_extract_cursor_metadata() {
    let dir = tempfile::tempdir().unwrap();
    let common = sample_common();
    let native = Cursor::from_common(&common).unwrap();
    CursorStore::new(dir.path()).save(&native).unwrap();

    let found = CursorStore::new(dir.path()).discover().unwrap();
    assert_eq!(found.len(), 1);
    let meta = &found[0].meta;
    assert_eq!(meta.id, "sess-1");
    assert_eq!(meta.cwd.as_deref(), Some("/repo"));
    assert_eq!(meta.title.as_deref(), Some("Cursor demo"));
    assert_eq!(meta.model.as_deref(), Some("composer-2.5-fast"));

    let loaded = CursorStore::new(dir.path())
        .load(&found[0].reference)
        .unwrap();
    let round = Cursor::to_common(&loaded).unwrap();
    assert_eq!(round.body.len(), 4);
}

#[test]
fn to_common_preserves_cursor_tool_calls_results_and_reasoning() {
    let common = sample_common();
    let native = Cursor::from_common(&common).unwrap();
    let round = Cursor::to_common(&native).unwrap();

    assert_eq!(round.body[0].content, common.body[0].content);
    assert!(matches!(
        &round.body[1].content[0],
        Block::Thinking {
            encrypted: Some(data),
            ..
        } if data == "opaque-reasoning"
    ));
    assert!(matches!(
        &round.body[1].content[2],
        Block::ToolUse {
            id,
            tool: Tool::Read { file_path, .. },
        } if id == "tool-1" && file_path == "/repo/README.md"
    ));
    assert!(matches!(
        &round.body[2].content[0],
        Block::ToolResult {
            tool_use_id,
            content: ToolOutput::Text(text),
            is_error: false,
        } if tool_use_id == "tool-1" && text == "contents"
    ));
}

#[test]
fn text_codec_round_trips_native_export() {
    let native = Cursor::from_common(&sample_common()).unwrap();
    let text = Cursor::to_text(&native).unwrap();
    let round = Cursor::from_text(&text).unwrap();
    assert_eq!(round.body, native.body);
    assert_eq!(
        Cursor::to_common(&round).unwrap().body,
        sample_common().body
    );
}

#[test]
fn from_common_writes_cursor_resume_state_turns() {
    let native = Cursor::from_common(&sample_common()).unwrap();
    let root = latest_root_blob(&native.body);
    let turn_refs = len_fields(&root.data, 8);

    assert_eq!(turn_refs.len(), 1);
    assert!(len_fields(&root.data, 1).is_empty());

    let turn_id = hex_encode_test(&turn_refs[0]);
    let turn_blob = native
        .body
        .blobs
        .iter()
        .find(|blob| blob.id == turn_id)
        .expect("turn structure blob exists");
    let agent_turns = len_fields(&turn_blob.data, 1);
    assert_eq!(agent_turns.len(), 1);

    let user_refs = len_fields(&agent_turns[0], 1);
    let step_refs = len_fields(&agent_turns[0], 2);
    assert_eq!(user_refs.len(), 1);
    assert!(step_refs.len() >= 3);

    let user_id = hex_encode_test(&user_refs[0]);
    assert!(
        native.body.blobs.iter().any(|blob| blob.id == user_id),
        "user message blob exists"
    );
    let mut read_tool_call = None;
    for step_ref in step_refs {
        let step_id = hex_encode_test(&step_ref);
        let step_blob = native
            .body
            .blobs
            .iter()
            .find(|blob| blob.id == step_id)
            .unwrap_or_else(|| panic!("step blob {step_id} exists"));
        if let Some(tool_call) = len_fields(&step_blob.data, 2).into_iter().next() {
            assert_eq!(string_fields(&tool_call, 57), vec!["tool-1".to_string()]);
            read_tool_call = len_fields(&tool_call, 8).into_iter().next();
        }
    }

    let read_tool_call = read_tool_call.expect("read tool call step exists");
    let read_args = len_fields(&read_tool_call, 1);
    let read_result = len_fields(&read_tool_call, 2);
    assert_eq!(read_args.len(), 1);
    assert_eq!(read_result.len(), 1);
    assert_eq!(
        string_fields(&read_args[0], 1),
        vec!["/repo/README.md".to_string()]
    );

    let read_success = len_fields(&read_result[0], 1);
    assert_eq!(read_success.len(), 1);
    assert_eq!(
        string_fields(&read_success[0], 1),
        vec!["contents".to_string()]
    );
    assert_eq!(
        string_fields(&read_success[0], 7),
        vec!["/repo/README.md".to_string()]
    );
}

#[test]
fn from_common_writes_cursor_shell_tool_calls() {
    let mut common = sample_common();
    common.body = vec![
        Message {
            role: Role::User,
            content: vec![Block::Text {
                text: "check git".into(),
            }],
            timestamp: ts("2026-01-02T03:04:06.000Z"),
            model: None,
            stop_reason: None,
            usage: None,
        },
        Message {
            role: Role::Assistant,
            content: vec![Block::ToolUse {
                id: "shell-1".into(),
                tool: Tool::Bash {
                    command: "git status --short".into(),
                    workdir: Some("/repo".into()),
                    timeout_ms: None,
                    description: Some("Show status".into()),
                    run_in_background: false,
                },
            }],
            timestamp: ts("2026-01-02T03:04:07.000Z"),
            model: None,
            stop_reason: None,
            usage: None,
        },
        Message {
            role: Role::User,
            content: vec![Block::ToolResult {
                tool_use_id: "shell-1".into(),
                content: ToolOutput::Text(" M src/main.rs".into()),
                is_error: false,
            }],
            timestamp: ts("2026-01-02T03:04:08.000Z"),
            model: None,
            stop_reason: None,
            usage: None,
        },
    ];

    let native = Cursor::from_common(&common).unwrap();
    let tool_call = first_tool_call(&native.body).expect("shell tool call");
    let shell = len_fields(&tool_call, 1);
    assert_eq!(shell.len(), 1);
    assert_eq!(string_fields(&tool_call, 57), vec!["shell-1".to_string()]);

    let args = len_fields(&shell[0], 1);
    let result = len_fields(&shell[0], 2);
    assert_eq!(args.len(), 1);
    assert_eq!(result.len(), 1);
    assert_eq!(
        string_fields(&args[0], 1),
        vec!["git status --short".to_string()]
    );
    assert_eq!(string_fields(&args[0], 2), vec!["/repo".to_string()]);
    assert_eq!(string_fields(&args[0], 4), vec!["shell-1".to_string()]);

    let success = len_fields(&result[0], 1);
    assert_eq!(success.len(), 1);
    assert_eq!(
        string_fields(&success[0], 5),
        vec![" M src/main.rs".to_string()]
    );
}

#[test]
fn from_common_writes_cursor_edit_tool_calls_with_diff_payload() {
    let mut common = sample_common();
    common.body = vec![
        Message {
            role: Role::User,
            content: vec![Block::Text {
                text: "update readme".into(),
            }],
            timestamp: ts("2026-01-02T03:04:06.000Z"),
            model: None,
            stop_reason: None,
            usage: None,
        },
        Message {
            role: Role::Assistant,
            content: vec![Block::ToolUse {
                id: "edit-1".into(),
                tool: Tool::Edit {
                    file_path: "/repo/README.md".into(),
                    old_string: "old title\n".into(),
                    new_string: "new title\n".into(),
                    replace_all: false,
                },
            }],
            timestamp: ts("2026-01-02T03:04:07.000Z"),
            model: None,
            stop_reason: None,
            usage: None,
        },
        Message {
            role: Role::User,
            content: vec![Block::ToolResult {
                tool_use_id: "edit-1".into(),
                content: ToolOutput::Text(
                    "The file /repo/README.md has been updated successfully.".into(),
                ),
                is_error: false,
            }],
            timestamp: ts("2026-01-02T03:04:08.000Z"),
            model: None,
            stop_reason: None,
            usage: None,
        },
    ];

    let native = Cursor::from_common(&common).unwrap();
    let tool_call = first_tool_call(&native.body).expect("edit tool call");
    let edit = len_fields(&tool_call, 12);
    assert_eq!(edit.len(), 1);
    assert_eq!(string_fields(&tool_call, 57), vec!["edit-1".to_string()]);

    let args = len_fields(&edit[0], 1);
    let result = len_fields(&edit[0], 2);
    assert_eq!(args.len(), 1);
    assert_eq!(result.len(), 1);
    assert_eq!(
        string_fields(&args[0], 1),
        vec!["/repo/README.md".to_string()]
    );
    assert_eq!(string_fields(&args[0], 6), vec!["new title\n".to_string()]);

    let success = len_fields(&result[0], 1);
    assert_eq!(success.len(), 1);
    assert_eq!(
        string_fields(&success[0], 1),
        vec!["/repo/README.md".to_string()]
    );
    assert_eq!(varint_fields(&success[0], 3), vec![1]);
    assert_eq!(varint_fields(&success[0], 4), vec![1]);

    let diff = string_fields(&success[0], 5)
        .into_iter()
        .next()
        .expect("diff string");
    assert!(diff.contains("--- a//repo/README.md"));
    assert!(diff.contains("+++ b//repo/README.md"));
    assert!(diff.contains("-old title"));
    assert!(diff.contains("+new title"));
    assert!(!diff.contains("updated successfully"));
    assert_eq!(
        string_fields(&success[0], 6),
        vec!["old title\n".to_string()]
    );
    assert_eq!(
        string_fields(&success[0], 7),
        vec!["new title\n".to_string()]
    );
}

#[test]
#[cfg(feature = "opencode")]
fn store_round_trip_preserves_non_json_blobs() {
    let src = tempfile::tempdir().unwrap();
    let dst = tempfile::tempdir().unwrap();
    let mut native = Cursor::from_common(&sample_common()).unwrap();
    native.body.blobs.insert(
        0,
        CursorBlob {
            id: "binary-internal-state".into(),
            data: vec![0, 159, 146, 150, 1, 2, 3],
        },
    );

    let saved = CursorStore::new(src.path()).save(&native).unwrap();
    let loaded = CursorStore::new(src.path()).load(&saved.reference).unwrap();
    assert_eq!(loaded.body.blobs[0].data, vec![0, 159, 146, 150, 1, 2, 3]);

    let copied = CursorStore::new(dst.path()).save(&loaded).unwrap();
    let reloaded = CursorStore::new(dst.path())
        .load(&copied.reference)
        .unwrap();
    assert_eq!(reloaded.body, loaded.body);
}

#[test]
fn parses_existing_cursor_message_shapes() {
    let body = txcript::harness::cursor::CursorDb {
        blobs: vec![
            CursorBlob {
                id: "system".into(),
                data: serde_json::to_vec(&json!({
                    "role": "system",
                    "content": "You are Cursor."
                }))
                .unwrap(),
            },
            CursorBlob {
                id: "context".into(),
                data: serde_json::to_vec(&json!({
                    "role": "user",
                    "content": "<user_info>\nWorkspace Path: /Users/me/repo\n</user_info>"
                }))
                .unwrap(),
            },
            CursorBlob {
                id: "user".into(),
                data: serde_json::to_vec(&json!({
                    "role": "user",
                    "content": [{"type": "text", "text": "<user_query>\nhello!\n</user_query>"}]
                }))
                .unwrap(),
            },
            CursorBlob {
                id: "assistant".into(),
                data: serde_json::to_vec(&json!({
                    "role": "assistant",
                    "content": [{
                        "type": "tool-call",
                        "toolCallId": "tool-1",
                        "toolName": "StrReplace",
                        "args": {
                            "path": "/repo/a.rs",
                            "old_string": "old",
                            "new_string": "new"
                        }
                    }],
                    "providerOptions": {"cursor": {"modelName": "composer-2.5"}}
                }))
                .unwrap(),
            },
        ],
        meta: Vec::new(),
        session_meta: Some(json!({
            "schemaVersion": 1,
            "createdAtMs": 1767337445000i64,
            "hasConversation": true,
            "title": "Hello Agent",
            "updatedAtMs": 1767337445000i64
        })),
    };
    let transcript = Transcript::<Cursor>::new(
        Meta {
            id: "sess".into(),
            timestamp: ts("2026-01-02T03:04:05.000Z"),
            cwd: Some("/Users/me/repo".into()),
            git_branch: None,
            title: None,
            cli_version: None,
            model: None,
        },
        body,
    );
    let common = Cursor::to_common(&transcript).unwrap();
    assert_eq!(common.body.len(), 2);
    assert!(matches!(
        &common.body[1].content[0],
        Block::ToolUse {
            tool: Tool::Edit { file_path, old_string, new_string, .. },
            ..
        } if file_path == "/repo/a.rs" && old_string == "old" && new_string == "new"
    ));
}

fn latest_root_blob(body: &txcript::harness::cursor::CursorDb) -> &CursorBlob {
    let meta = cursor_meta_json(body);
    let id = meta
        .get("latestRootBlobId")
        .and_then(serde_json::Value::as_str)
        .expect("latest root id");
    body.blobs
        .iter()
        .find(|blob| blob.id == id)
        .expect("latest root blob")
}

fn cursor_meta_json(body: &txcript::harness::cursor::CursorDb) -> serde_json::Value {
    let raw = body
        .meta
        .iter()
        .find(|entry| entry.key == "0")
        .expect("cursor meta row")
        .value
        .as_str();
    let decoded = hex_decode_test(raw);
    serde_json::from_slice(&decoded).expect("cursor meta json")
}

fn first_tool_call(body: &txcript::harness::cursor::CursorDb) -> Option<Vec<u8>> {
    let root = latest_root_blob(body);
    for turn_ref in len_fields(&root.data, 8) {
        let turn_id = hex_encode_test(&turn_ref);
        let turn_blob = body.blobs.iter().find(|blob| blob.id == turn_id)?;
        for agent_turn in len_fields(&turn_blob.data, 1) {
            for step_ref in len_fields(&agent_turn, 2) {
                let step_id = hex_encode_test(&step_ref);
                let Some(step_blob) = body.blobs.iter().find(|blob| blob.id == step_id) else {
                    continue;
                };
                if let Some(tool_call) = len_fields(&step_blob.data, 2).into_iter().next() {
                    return Some(tool_call);
                }
            }
        }
    }
    None
}

fn string_fields(data: &[u8], wanted_field: u64) -> Vec<String> {
    len_fields(data, wanted_field)
        .into_iter()
        .map(|bytes| String::from_utf8(bytes).expect("utf-8 field"))
        .collect()
}

fn varint_fields(data: &[u8], wanted_field: u64) -> Vec<u64> {
    let mut out = Vec::new();
    let mut i = 0usize;
    while i < data.len() {
        let Some(key) = read_varint(data, &mut i) else {
            break;
        };
        let field = key >> 3;
        match key & 0x07 {
            0 => {
                let Some(value) = read_varint(data, &mut i) else {
                    break;
                };
                if field == wanted_field {
                    out.push(value);
                }
            }
            1 => i = i.saturating_add(8),
            2 => {
                let Some(len) = read_varint(data, &mut i).and_then(|v| usize::try_from(v).ok())
                else {
                    break;
                };
                let Some(end) = i.checked_add(len) else {
                    break;
                };
                if end > data.len() {
                    break;
                }
                i = end;
            }
            5 => i = i.saturating_add(4),
            _ => break,
        }
    }
    out
}

fn len_fields(data: &[u8], wanted_field: u64) -> Vec<Vec<u8>> {
    let mut out = Vec::new();
    let mut i = 0usize;
    while i < data.len() {
        let Some(key) = read_varint(data, &mut i) else {
            break;
        };
        let field = key >> 3;
        match key & 0x07 {
            0 => {
                let _ = read_varint(data, &mut i);
            }
            1 => i = i.saturating_add(8),
            2 => {
                let Some(len) = read_varint(data, &mut i).and_then(|v| usize::try_from(v).ok())
                else {
                    break;
                };
                let Some(end) = i.checked_add(len) else {
                    break;
                };
                if end > data.len() {
                    break;
                }
                let value = data[i..end].to_vec();
                if field == wanted_field {
                    out.push(value);
                }
                i = end;
            }
            5 => i = i.saturating_add(4),
            _ => break,
        }
    }
    out
}

fn read_varint(data: &[u8], i: &mut usize) -> Option<u64> {
    let mut value = 0u64;
    let mut shift = 0u32;
    while *i < data.len() && shift < 64 {
        let byte = data[*i];
        *i += 1;
        value |= ((byte & 0x7f) as u64) << shift;
        if byte & 0x80 == 0 {
            return Some(value);
        }
        shift += 7;
    }
    None
}

fn hex_decode_test(s: &str) -> Vec<u8> {
    assert_eq!(s.len() % 2, 0);
    (0..s.len())
        .step_by(2)
        .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
        .collect()
}

fn hex_encode_test(bytes: &[u8]) -> String {
    bytes.iter().map(|b| format!("{b:02x}")).collect::<String>()
}
