"""Tests for qc_trace.schemas.cursor.transform_vscdb."""

import pytest

from qc_trace.schemas.cursor.transform_vscdb import (
    _detect_mode,
    _extract_hashes,
    _ms_to_iso,
    _extract_model,
    _bubble_tokens,
    _decode_varint,
    transform_cursor_vscdb,
)
from qc_trace.utils.vscdb import VscdbSession
from qc_trace.schemas.unified import TokenUsage


def _make_session(
    composer_id: str = "test-id",
    composer_data: dict | None = None,
    bubble_entries: dict | None = None,
    agent_kv_entries: dict | None = None,
    db_path: str = "/test/state.vscdb",
) -> VscdbSession:
    return VscdbSession(
        composer_id=composer_id,
        composer_data=composer_data or {},
        bubble_entries=bubble_entries or {},
        agent_kv_entries=agent_kv_entries or {},
        db_path=db_path,
    )


class TestDetectMode:
    def test_inline_with_text(self):
        cd = {"conversation": [{"type": 1, "text": "hello", "bubbleId": "b1"}]}
        assert _detect_mode(cd) == "inline"

    def test_hashchain_with_state(self):
        cd = {"conversationState": "some_base64_data"}
        assert _detect_mode(cd) == "hashchain"

    def test_headers_only(self):
        cd = {"fullConversationHeadersOnly": [{"type": 1, "bubbleId": "b1"}]}
        assert _detect_mode(cd) == "headers_only"

    def test_empty_conversation_is_headers_only(self):
        cd = {"conversation": []}
        assert _detect_mode(cd) == "headers_only"

    def test_conversation_without_text_is_headers_only(self):
        cd = {"conversation": [{"type": 1, "bubbleId": "b1"}]}
        assert _detect_mode(cd) == "headers_only"

    def test_empty_dict(self):
        assert _detect_mode({}) == "headers_only"


class TestMsToIso:
    def test_converts_ms(self):
        result = _ms_to_iso(1708000000000)
        assert "2024-02-15" in result
        assert result.endswith("+00:00")

    def test_zero_returns_empty(self):
        assert _ms_to_iso(0) == ""

    def test_none_returns_empty(self):
        assert _ms_to_iso(None) == ""


class TestExtractModel:
    def test_extracts_model(self):
        cd = {"modelConfig": {"modelName": "claude-3.5-sonnet"}}
        assert _extract_model(cd) == "claude-3.5-sonnet"

    def test_no_model_config(self):
        assert _extract_model({}) is None

    def test_no_model_name(self):
        cd = {"modelConfig": {}}
        assert _extract_model(cd) is None


class TestBubbleTokens:
    def test_finds_tokens(self):
        session = _make_session(
            composer_id="cid",
            bubble_entries={
                "bubbleId:cid:b1": {
                    "tokenCount": {"inputTokens": 50, "outputTokens": 100}
                }
            },
        )
        tokens = _bubble_tokens(session, "cid", "b1")
        assert tokens.input == 50
        assert tokens.output == 100

    def test_missing_bubble(self):
        session = _make_session()
        tokens = _bubble_tokens(session, "cid", "missing")
        assert tokens.input == 0
        assert tokens.output == 0

    def test_empty_bubble_id(self):
        session = _make_session()
        tokens = _bubble_tokens(session, "cid", "")
        assert tokens.input == 0


class TestDecodeVarint:
    def test_single_byte(self):
        val, pos = _decode_varint(b"\x05", 0)
        assert val == 5
        assert pos == 1

    def test_multi_byte(self):
        val, pos = _decode_varint(b"\xac\x02", 0)
        assert val == 300
        assert pos == 2

    def test_with_offset(self):
        val, pos = _decode_varint(b"\x00\x05", 1)
        assert val == 5
        assert pos == 2


class TestExtractHashes:
    def test_empty_string(self):
        assert _extract_hashes("") == []

    def test_invalid_base64(self):
        assert _extract_hashes("!!!invalid!!!") == []

    def test_extracts_hashes_from_protobuf(self):
        # Build a minimal protobuf with a 32-byte length-delimited field
        # Field 1, wire type 2 (length-delimited): tag = (1 << 3) | 2 = 0x0a
        import base64
        hash_bytes = bytes(range(32))  # 32-byte "hash"
        proto = bytes([0x0a, 0x20]) + hash_bytes  # tag + length(32) + data
        encoded = base64.b64encode(proto).decode()

        result = _extract_hashes(encoded)
        assert len(result) == 1
        assert result[0] == hash_bytes.hex()

    def test_strips_tilde_prefix(self):
        import base64
        hash_bytes = bytes(range(32))
        proto = bytes([0x0a, 0x20]) + hash_bytes
        encoded = "~" + base64.b64encode(proto).decode()

        result = _extract_hashes(encoded)
        assert len(result) == 1


class TestTransformInline:
    def test_user_and_assistant(self):
        session = _make_session(
            composer_id="s1",
            composer_data={
                "createdAt": 1708000000000,
                "conversation": [
                    {"type": 1, "bubbleId": "b1", "text": "Hello"},
                    {"type": 2, "bubbleId": "b2", "text": "Hi!", "timingInfo": {"clientStartTime": 1708000001000}},
                ],
                "modelConfig": {"modelName": "gpt-4o"},
            },
        )
        msgs = transform_cursor_vscdb(session)
        assert len(msgs) == 2
        assert msgs[0].msg_type == "user"
        assert msgs[0].content == "Hello"
        assert msgs[0].source == "cursor_vscdb"
        assert msgs[0].source_schema_version == 2
        assert msgs[1].msg_type == "assistant"
        assert msgs[1].content == "Hi!"
        assert msgs[1].model == "gpt-4o"
        assert "2024-02-15" in msgs[1].timestamp

    def test_tool_call_bubble(self):
        session = _make_session(
            composer_data={
                "createdAt": 1708000000000,
                "conversation": [
                    {"type": 2, "bubbleId": "b1", "text": "", "isCapabilityIteration": True, "capabilityType": "code_edit"},
                ],
            },
        )
        msgs = transform_cursor_vscdb(session)
        assert len(msgs) == 1
        assert msgs[0].msg_type == "tool_call"
        assert msgs[0].tool_call.name == "code_edit"

    def test_thinking_blocks(self):
        session = _make_session(
            composer_data={
                "createdAt": 1708000000000,
                "conversation": [
                    {
                        "type": 2, "bubbleId": "b1", "text": "Answer",
                        "allThinkingBlocks": [{"thinking": "Let me think..."}],
                    },
                ],
            },
        )
        msgs = transform_cursor_vscdb(session)
        assert msgs[0].thinking == "Let me think..."

    def test_tokens_from_bubble_entry(self):
        session = _make_session(
            composer_id="s1",
            composer_data={
                "createdAt": 1708000000000,
                "conversation": [
                    {"type": 2, "bubbleId": "b1", "text": "Response"},
                ],
            },
            bubble_entries={
                "bubbleId:s1:b1": {"tokenCount": {"inputTokens": 200, "outputTokens": 300}},
            },
        )
        msgs = transform_cursor_vscdb(session)
        assert msgs[0].tokens.input == 200
        assert msgs[0].tokens.output == 300


class TestTransformHashchain:
    def test_basic_hashchain(self):
        import base64
        import json as _json

        # Build protobuf with one hash
        hash_bytes = b"\x01" * 32
        proto = bytes([0x0a, 0x20]) + hash_bytes
        conv_state = base64.b64encode(proto).decode()

        # Build agent KV entry
        agent_kv_data = _json.dumps({"role": "user", "content": [{"type": "text", "text": "Hello from hashchain"}]}).encode()

        session = _make_session(
            composer_data={
                "createdAt": 1708000000000,
                "conversationState": conv_state,
                "modelConfig": {"modelName": "claude-3.5-sonnet"},
            },
            agent_kv_entries={
                f"agentKv:blob:{hash_bytes.hex()}": agent_kv_data,
            },
        )
        msgs = transform_cursor_vscdb(session)
        assert len(msgs) == 1
        assert msgs[0].msg_type == "user"
        assert msgs[0].content == "Hello from hashchain"

    def test_tool_result_with_error(self):
        import base64
        import json as _json

        hash_bytes = b"\x02" * 32
        proto = bytes([0x0a, 0x20]) + hash_bytes
        conv_state = base64.b64encode(proto).decode()

        agent_kv_data = _json.dumps({
            "role": "tool",
            "id": "call-123",
            "content": [{"type": "text", "text": "Error occurred", "is_error": True}],
        }).encode()

        session = _make_session(
            composer_data={
                "createdAt": 1708000000000,
                "conversationState": conv_state,
            },
            agent_kv_entries={
                f"agentKv:blob:{hash_bytes.hex()}": agent_kv_data,
            },
        )
        msgs = transform_cursor_vscdb(session)
        assert len(msgs) == 1
        assert msgs[0].msg_type == "tool_result"
        assert msgs[0].tool_result.status == "failure"
        assert msgs[0].tool_result.call_id == "call-123"


class TestTransformHeadersOnly:
    def test_basic_headers(self):
        session = _make_session(
            composer_id="ho-1",
            composer_data={
                "createdAt": 1708000000000,
                "fullConversationHeadersOnly": [
                    {"type": 1, "bubbleId": "b1"},
                    {"type": 2, "bubbleId": "b2"},
                ],
                "modelConfig": {"modelName": "gpt-4o"},
            },
            bubble_entries={
                "bubbleId:ho-1:b1": {"_v": 3, "tokenCount": {"inputTokens": 50, "outputTokens": 0}, "createdAt": 1708000001000},
                "bubbleId:ho-1:b2": {"_v": 3, "tokenCount": {"inputTokens": 50, "outputTokens": 150}, "createdAt": 1708000002000},
            },
        )
        msgs = transform_cursor_vscdb(session)
        assert len(msgs) == 2
        assert msgs[0].msg_type == "user"
        assert msgs[0].tokens.input == 50
        assert msgs[1].msg_type == "assistant"
        assert msgs[1].tokens.output == 150
        assert msgs[1].model == "gpt-4o"
        # Should use bubbleId.createdAt for timestamp
        assert "2024-02-15" in msgs[1].timestamp

    def test_tool_call_header(self):
        session = _make_session(
            composer_data={
                "createdAt": 1708000000000,
                "fullConversationHeadersOnly": [
                    {"type": 2, "bubbleId": "b1", "isCapabilityIteration": True, "capabilityType": "terminal"},
                ],
            },
        )
        msgs = transform_cursor_vscdb(session)
        assert len(msgs) == 1
        assert msgs[0].msg_type == "tool_call"
        assert msgs[0].tool_call.name == "terminal"


class TestTransformEmpty:
    def test_empty_session(self):
        session = _make_session(composer_data={})
        msgs = transform_cursor_vscdb(session)
        assert msgs == []
