"""Tests for memory extractor — post-conversation LLM extraction pass."""

import json
from unittest.mock import AsyncMock

import pytest

from fliiq.runtime.memory.extractor import (
    _build_conversation_summary,
    _merge_entity_file,
    _parse_extraction,
    _slugify,
    extract_and_save_memories,
)
from fliiq.runtime.memory.manager import MemoryManager

# --- _slugify ---


def test_slugify_basic():
    assert _slugify("Norm Chan") == "norm-chan"


def test_slugify_special_chars():
    assert _slugify("Alice O'Brien") == "alice-obrien"


def test_slugify_empty():
    assert _slugify("") == "unknown"


def test_slugify_hyphens():
    assert _slugify("my-topic-name") == "my-topic-name"


# --- _build_conversation_summary ---


def test_build_summary_basic():
    messages = [
        {"role": "user", "content": "Hello, my name is Andy."},
        {"role": "assistant", "content": "Nice to meet you, Andy!"},
    ]
    summary = _build_conversation_summary(messages)
    assert "user: Hello, my name is Andy." in summary
    assert "assistant: Nice to meet you, Andy!" in summary


def test_build_summary_content_blocks():
    messages = [
        {
            "role": "assistant",
            "content": [
                {"type": "text", "text": "Here is the plan."},
                {"type": "tool_use", "id": "t1", "name": "shell"},
            ],
        }
    ]
    summary = _build_conversation_summary(messages)
    assert "Here is the plan." in summary


def test_build_summary_caps_length():
    long_message = "x" * 10000
    messages = [{"role": "user", "content": long_message}]
    summary = _build_conversation_summary(messages)
    assert len(summary) <= 8000


def test_build_summary_empty():
    summary = _build_conversation_summary([])
    assert summary == ""


# --- _parse_extraction ---


def test_parse_valid_json():
    data = {
        "people": [{"name": "norm", "display_name": "Norm", "facts": ["Co-founder"]}],
        "topics": [],
        "decisions": [],
    }
    result = _parse_extraction(json.dumps(data))
    assert len(result["people"]) == 1
    assert result["people"][0]["name"] == "norm"


def test_parse_markdown_fenced():
    raw = '```json\n{"people": [], "topics": [], "decisions": []}\n```'
    result = _parse_extraction(raw)
    assert result["people"] == []


def test_parse_invalid_json():
    result = _parse_extraction("not json at all")
    assert result == {"people": [], "topics": [], "decisions": []}


# --- _merge_entity_file ---


def test_merge_creates_new_file(tmp_path):
    mgr = MemoryManager(tmp_path / "memory")
    mgr.ensure_dirs()

    _merge_entity_file(mgr, "people", "norm-chan", "Norm Chan", ["Co-founder of Simplify Health"])

    content = mgr.load_entity("people", "norm-chan")
    assert "Norm Chan" in content
    assert "Co-founder of Simplify Health" in content
    assert "---" in content  # frontmatter


def test_merge_appends_to_existing(tmp_path):
    mgr = MemoryManager(tmp_path / "memory")
    mgr.ensure_dirs()

    _merge_entity_file(mgr, "people", "norm-chan", "Norm Chan", ["Co-founder"])
    _merge_entity_file(mgr, "people", "norm-chan", "Norm Chan", ["Lives in NYC"])

    content = mgr.load_entity("people", "norm-chan")
    assert "Co-founder" in content
    assert "Lives in NYC" in content


def test_merge_deduplicates_facts(tmp_path):
    mgr = MemoryManager(tmp_path / "memory")
    mgr.ensure_dirs()

    _merge_entity_file(mgr, "people", "norm-chan", "Norm Chan", ["Co-founder"])
    _merge_entity_file(mgr, "people", "norm-chan", "Norm Chan", ["Co-founder"])

    content = mgr.load_entity("people", "norm-chan")
    assert content.lower().count("co-founder") == 1


def test_merge_with_decisions(tmp_path):
    mgr = MemoryManager(tmp_path / "memory")
    mgr.ensure_dirs()

    _merge_entity_file(
        mgr,
        "topics",
        "architecture",
        "Architecture",
        ["Monolith-first approach"],
        [{"decision": "Use PostgreSQL", "rationale": "Team knows it well"}],
    )

    content = mgr.load_entity("topics", "architecture")
    assert "Use PostgreSQL" in content
    assert "Team knows it well" in content


def test_merge_skips_when_nothing_new(tmp_path):
    mgr = MemoryManager(tmp_path / "memory")
    mgr.ensure_dirs()

    _merge_entity_file(mgr, "people", "norm", "Norm", ["Fact one"])
    content_before = mgr.load_entity("people", "norm")

    _merge_entity_file(mgr, "people", "norm", "Norm", ["Fact one"])
    content_after = mgr.load_entity("people", "norm")

    assert content_before == content_after


# --- extract_and_save_memories (integration) ---


@pytest.mark.asyncio
async def test_extract_and_save_creates_files(tmp_path):
    """Full integration: mock LLM returns structured JSON, files get created."""
    extraction = {
        "people": [
            {
                "name": "norm-chan",
                "display_name": "Norm Chan",
                "facts": ["Co-founder of Simplify Health", "Based in NYC"],
            }
        ],
        "topics": [
            {
                "name": "fliiq-launch",
                "display_name": "Fliiq Launch",
                "facts": ["Targeting Q1 beta release"],
            }
        ],
        "decisions": [
            {
                "topic": "fliiq-launch",
                "decision": "Start with healthcare vertical",
                "rationale": "Highest institutional knowledge density",
            }
        ],
    }

    mock_llm = AsyncMock()
    mock_llm.generate.return_value = AsyncMock(content=json.dumps(extraction))

    messages = [
        {"role": "user", "content": "Let's plan the Fliiq launch with Norm."},
        {"role": "assistant", "content": "Norm Chan is your co-founder. He's based in NYC. Let's target Q1."},
    ]

    # Set up project root with .fliiq/memory/ structure
    fliiq_dir = tmp_path / ".fliiq" / "memory"
    fliiq_dir.mkdir(parents=True)

    result = await extract_and_save_memories(mock_llm, messages, tmp_path)

    assert len(result["people"]) == 1
    assert len(result["topics"]) == 1

    # Verify files were created
    mgr = MemoryManager.from_project_root(tmp_path)
    norm = mgr.load_entity("people", "norm-chan")
    assert norm is not None
    assert "Co-founder" in norm

    launch = mgr.load_entity("topics", "fliiq-launch")
    assert launch is not None
    assert "Q1 beta" in launch
    assert "healthcare vertical" in launch  # decision merged into topic


@pytest.mark.asyncio
async def test_extract_short_conversation_skipped(tmp_path):
    """Conversations under 50 chars don't trigger extraction."""
    mock_llm = AsyncMock()
    messages = [{"role": "user", "content": "hi"}]

    result = await extract_and_save_memories(mock_llm, messages, tmp_path)

    assert result == {"people": [], "topics": [], "decisions": []}
    mock_llm.generate.assert_not_called()


@pytest.mark.asyncio
async def test_extract_handles_llm_failure(tmp_path):
    """LLM errors don't crash — return empty result."""
    mock_llm = AsyncMock()
    mock_llm.generate.side_effect = RuntimeError("API error")

    messages = [
        {"role": "user", "content": "A long enough conversation to trigger extraction for testing purposes."},
    ]

    result = await extract_and_save_memories(mock_llm, messages, tmp_path)
    assert result == {"people": [], "topics": [], "decisions": []}


@pytest.mark.asyncio
async def test_extract_handles_bad_json(tmp_path):
    """Invalid LLM output doesn't crash — return empty result."""
    mock_llm = AsyncMock()
    mock_llm.generate.return_value = AsyncMock(content="not valid json")

    messages = [
        {"role": "user", "content": "A long enough conversation to trigger extraction for testing purposes."},
    ]

    fliiq_dir = tmp_path / ".fliiq" / "memory"
    fliiq_dir.mkdir(parents=True)

    result = await extract_and_save_memories(mock_llm, messages, tmp_path)
    assert result == {"people": [], "topics": [], "decisions": []}


@pytest.mark.asyncio
async def test_extract_unscoped_decisions_go_to_log(tmp_path):
    """Decisions not matching any person/topic slug go to decisions log."""
    extraction = {
        "people": [],
        "topics": [],
        "decisions": [
            {
                "topic": "general-policy",
                "decision": "Always use type hints",
                "rationale": "Team convention",
            }
        ],
    }

    mock_llm = AsyncMock()
    mock_llm.generate.return_value = AsyncMock(content=json.dumps(extraction))

    messages = [
        {"role": "user", "content": "We decided to always use type hints in our Python code going forward."},
    ]

    fliiq_dir = tmp_path / ".fliiq" / "memory"
    fliiq_dir.mkdir(parents=True)

    result = await extract_and_save_memories(mock_llm, messages, tmp_path)
    assert len(result["decisions"]) == 1

    mgr = MemoryManager.from_project_root(tmp_path)
    decisions_log = mgr.load_entity("topics", "decisions")
    assert decisions_log is not None
    assert "type hints" in decisions_log
