"""
Tests for R-036 fix: message parts writing on ingest.

Verifies that ingest_message() correctly decomposes structured content
into hippo_message_parts rows via _build_message_parts().

Issue: R-036 #1
"""

from __future__ import annotations

import json
import pytest

from cortex.hippo.config import HippoConfig
from cortex.hippo.engine import HippoEngine, _build_message_parts, _estimate_tokens_for_content


# ── Unit tests for _build_message_parts() ─────────────────────────────────


def test_build_parts_plain_string():
    """Plain string content produces a single text part."""
    parts = _build_message_parts("user", "Hello, world!")
    assert len(parts) == 1
    assert parts[0]["part_type"] == "text"
    assert parts[0]["text_content"] == "Hello, world!"
    assert parts[0]["ordinal"] == 0


def test_build_parts_empty_string():
    """Empty string produces no parts."""
    parts = _build_message_parts("user", "")
    assert parts == []


def test_build_parts_none():
    """None content produces no parts."""
    parts = _build_message_parts("user", None)
    assert parts == []


def test_build_parts_empty_list():
    """Empty list produces no parts."""
    parts = _build_message_parts("user", [])
    assert parts == []


def test_build_parts_text_block():
    """Array with a single text block → one text part."""
    content = [{"type": "text", "text": "Hello"}]
    parts = _build_message_parts("user", content)
    assert len(parts) == 1
    assert parts[0]["part_type"] == "text"
    assert parts[0]["text_content"] == "Hello"
    assert parts[0]["ordinal"] == 0


def test_build_parts_tool_use():
    """tool_use block → tool_use part with correct fields."""
    content = [
        {
            "type": "tool_use",
            "id": "call_abc123",
            "name": "bash",
            "input": {"command": "ls -la"},
        }
    ]
    parts = _build_message_parts("assistant", content)
    assert len(parts) == 1
    p = parts[0]
    assert p["part_type"] == "tool_use"
    assert p["tool_call_id"] == "call_abc123"
    assert p["tool_name"] == "bash"
    assert p["tool_input"] == json.dumps({"command": "ls -la"})
    assert p["ordinal"] == 0


def test_build_parts_tool_use_variants():
    """All tool_use type variants map to tool_use part."""
    for block_type in ("tool_use", "toolUse", "toolCall", "functionCall", "function_call"):
        content = [{"type": block_type, "id": "x", "name": "fn", "input": {}}]
        parts = _build_message_parts("assistant", content)
        assert len(parts) == 1
        assert parts[0]["part_type"] == "tool_use", f"failed for type={block_type}"


def test_build_parts_tool_result():
    """tool_result block → tool_result part with tool_call_id and tool_output."""
    content = [
        {
            "type": "tool_result",
            "tool_use_id": "call_abc123",
            "content": "File listing output",
        }
    ]
    parts = _build_message_parts("tool", content)
    assert len(parts) == 1
    p = parts[0]
    assert p["part_type"] == "tool_result"
    assert p["tool_call_id"] == "call_abc123"
    assert p["tool_output"] == "File listing output"


def test_build_parts_tool_result_variants():
    """All tool_result type variants map to tool_result part."""
    for block_type in ("tool_result", "toolResult", "function_call_output"):
        content = [{"type": block_type, "tool_use_id": "x", "content": "out"}]
        parts = _build_message_parts("tool", content)
        assert len(parts) == 1
        assert parts[0]["part_type"] == "tool_result", f"failed for type={block_type}"


def test_build_parts_thinking():
    """thinking block → reasoning part_type (matching lossless-claw convention)."""
    content = [{"type": "thinking", "thinking": "Let me reason through this..."}]
    parts = _build_message_parts("assistant", content)
    assert len(parts) == 1
    p = parts[0]
    assert p["part_type"] == "reasoning"  # stored as "reasoning" per lossless-claw convention
    assert p["text_content"] == "Let me reason through this..."


def test_build_parts_reasoning_variant():
    """reasoning type also maps to reasoning part_type."""
    content = [{"type": "reasoning", "thinking": "Hmm..."}]
    parts = _build_message_parts("assistant", content)
    assert len(parts) == 1
    assert parts[0]["part_type"] == "reasoning"


def test_build_parts_multiple_blocks():
    """Multiple blocks produce correct ordinals."""
    content = [
        {"type": "text", "text": "Let me use a tool"},
        {"type": "tool_use", "id": "call_1", "name": "search", "input": {"q": "test"}},
    ]
    parts = _build_message_parts("assistant", content)
    assert len(parts) == 2
    assert parts[0]["ordinal"] == 0
    assert parts[0]["part_type"] == "text"
    assert parts[1]["ordinal"] == 1
    assert parts[1]["part_type"] == "tool_use"


def test_build_parts_unknown_block_type():
    """Unknown block types map to 'agent' part."""
    content = [{"type": "custom_block", "data": "something"}]
    parts = _build_message_parts("assistant", content)
    assert len(parts) == 1
    assert parts[0]["part_type"] == "agent"


# ── Token estimation tests ─────────────────────────────────────────────────


def test_estimate_tokens_plain_string():
    """Plain string: token estimate = len // 4."""
    text = "Hello world"
    result = _estimate_tokens_for_content(text)
    assert result == max(1, len(text) // 4)


def test_estimate_tokens_structured_content():
    """Structured content: JSON-serialized for realistic estimate."""
    content = [
        {"type": "tool_use", "id": "x", "name": "bash", "input": {"command": "ls " * 100}},
    ]
    # JSON serialize is longer than just the text content
    plain_text_estimate = max(1, len("") // 4)  # text_content would be empty
    structured_estimate = _estimate_tokens_for_content(content)
    assert structured_estimate > plain_text_estimate


# ── Integration tests: ingest_message() writes parts to DB ────────────────


@pytest.mark.asyncio
async def test_ingest_plain_text_writes_one_part(engine: HippoEngine):
    """Ingest plain text → one text part in DB."""
    result = await engine.ingest_message("user", "Hello, Hippo!")
    parts = await engine.conversation_store.get_message_parts(result.message_id)
    assert len(parts) == 1
    assert parts[0]["part_type"] == "text"
    assert parts[0]["text_content"] == "Hello, Hippo!"


@pytest.mark.asyncio
async def test_ingest_tool_use_writes_tool_use_part(engine: HippoEngine):
    """Ingest tool_use content block → tool_use part with correct fields."""
    content = [
        {"type": "text", "text": "Let me run that"},
        {
            "type": "tool_use",
            "id": "call_test_123",
            "name": "bash",
            "input": {"command": "echo hello"},
        },
    ]
    result = await engine.ingest_message("assistant", content)
    parts = await engine.conversation_store.get_message_parts(result.message_id)

    assert len(parts) == 2

    text_part = parts[0]
    assert text_part["part_type"] == "text"
    assert text_part["text_content"] == "Let me run that"

    tool_part = parts[1]
    assert tool_part["part_type"] == "tool_use"
    assert tool_part["tool_call_id"] == "call_test_123"
    assert tool_part["tool_name"] == "bash"
    assert json.loads(tool_part["tool_input"]) == {"command": "echo hello"}


@pytest.mark.asyncio
async def test_ingest_tool_result_writes_tool_result_part(engine: HippoEngine):
    """Ingest tool_result content block → tool_result part with tool_call_id."""
    content = [
        {
            "type": "tool_result",
            "tool_use_id": "call_test_123",
            "content": "hello\n",
        }
    ]
    result = await engine.ingest_message("tool", content)
    parts = await engine.conversation_store.get_message_parts(result.message_id)

    assert len(parts) == 1
    p = parts[0]
    assert p["part_type"] == "tool_result"
    assert p["tool_call_id"] == "call_test_123"
    assert p["tool_output"] == "hello\n"


@pytest.mark.asyncio
async def test_ingest_thinking_writes_thinking_part(engine: HippoEngine):
    """Ingest thinking block → thinking part in DB."""
    content = [
        {"type": "thinking", "thinking": "I need to think carefully here."},
        {"type": "text", "text": "Here is my answer."},
    ]
    result = await engine.ingest_message("assistant", content)
    parts = await engine.conversation_store.get_message_parts(result.message_id)

    assert len(parts) == 2
    thinking_part = next(p for p in parts if p["part_type"] == "reasoning")
    assert thinking_part["text_content"] == "I need to think carefully here."


@pytest.mark.asyncio
async def test_ingest_preserves_plain_text_in_content_column(engine: HippoEngine):
    """Plain text in messages.content column is preserved (not overwritten)."""
    result = await engine.ingest_message("user", "Test message")
    msg = await engine.conversation_store.get_message_by_id(result.message_id)
    assert msg is not None
    assert msg.content == "Test message"


@pytest.mark.asyncio
async def test_ingest_structured_content_extracts_text_to_content_column(engine: HippoEngine):
    """Structured content: only text blocks stored in messages.content."""
    content = [
        {"type": "text", "text": "visible text"},
        {"type": "tool_use", "id": "x", "name": "fn", "input": {}},
    ]
    result = await engine.ingest_message("assistant", content)
    msg = await engine.conversation_store.get_message_by_id(result.message_id)
    assert msg is not None
    # Only text blocks in content column; tool_use excluded
    assert msg.content == "visible text"


@pytest.mark.asyncio
async def test_ingest_token_count_uses_full_content_shape(engine: HippoEngine):
    """Token count reflects full content (not just text extraction)."""
    # Large tool input that would be lost with text-only extraction
    large_input = {"data": "x" * 1000}
    content = [
        {"type": "tool_use", "id": "x", "name": "fn", "input": large_input},
    ]
    result = await engine.ingest_message("assistant", content)
    # JSON-serialized content >> empty string token count
    assert result.token_count > 50  # should be ~250+ tokens for large input


@pytest.mark.asyncio
async def test_assemble_after_ingest_with_tool_use_returns_content_blocks(engine: HippoEngine):
    """Assemble after ingesting tool_use → reconstructed message has content blocks."""
    tool_call_id = "call_abc"
    content = [
        {"type": "text", "text": "Using a tool"},
        {
            "type": "tool_use",
            "id": tool_call_id,
            "name": "my_tool",
            "input": {"arg": "value"},
        },
    ]
    tool_result_content = [
        {
            "type": "tool_result",
            "tool_use_id": tool_call_id,
            "content": "tool output here",
        }
    ]
    await engine.ingest_message("user", "user prompt")
    await engine.ingest_message("assistant", content)
    await engine.ingest_message("tool", tool_result_content)

    assembled = await engine.get_context()
    # Find the assistant message
    assistant_msgs = [m for m in assembled.messages if m.get("role") == "assistant"]
    assert len(assistant_msgs) >= 1

    assistant_msg = assistant_msgs[0]
    # Content should be a list of blocks, not a plain string
    assert isinstance(assistant_msg["content"], list)
    block_types = [b["type"] for b in assistant_msg["content"]]
    assert "text" in block_types
    assert "tool_use" in block_types
