"""Tests for the write pipeline — extract, entity_resolution, reconcile, upsert, and end-to-end.

All LLM/Embedding calls are mocked via Protocol-compliant fakes.
"""

import json
from unittest.mock import AsyncMock, MagicMock

from conftest import FakeEmbeddingProvider, FakeLLMProvider, run_async


def _make_extraction_response(
    entities: list[dict] | None = None,
    facts: list[dict] | None = None,
    relations: list[dict] | None = None,
) -> str:
    return json.dumps(
        {
            "entities": entities or [],
            "facts": facts or [],
            "relations": relations or [],
        }
    )


# ===========================================================================
# Test: extract
# ===========================================================================


class TestExtract:
    def test_extract_parses_entities_and_facts(self):
        from arandu.write.extract import extract_from_message

        response = _make_extraction_response(
            entities=[
                {"name": "user", "type": "person"},
                {"name": "São Paulo", "type": "place"},
            ],
            facts=[
                {"subject": "user", "fact": "Lives in São Paulo", "confidence": 0.95},
            ],
            relations=[],
        )
        llm = FakeLLMProvider([response])

        result = run_async(extract_from_message("Eu moro em São Paulo", llm=llm))

        assert len(result.entities) == 2
        assert len(result.facts) == 1
        assert result.facts[0].subject == "user"
        assert result.facts[0].confidence == 0.95

    def test_extract_returns_empty_on_failure(self):
        from arandu.write.extract import extract_from_message

        llm = FakeLLMProvider(["not valid json {{{"])

        result = run_async(extract_from_message("hello", llm=llm))

        assert len(result.entities) == 0
        assert len(result.facts) == 0

    def test_extract_handles_markdown_fences(self):
        from arandu.write.extract import extract_from_message

        response = (
            "```json\n"
            + _make_extraction_response(
                entities=[{"name": "user", "type": "person"}],
                facts=[{"subject": "user", "fact": "Works at Google", "confidence": 0.9}],
            )
            + "\n```"
        )
        llm = FakeLLMProvider([response])

        result = run_async(extract_from_message("I work at Google", llm=llm))

        assert len(result.facts) == 1


# ===========================================================================
# Test: entity_resolution
# ===========================================================================


class TestEntityResolution:
    def test_self_reference_resolves_to_user_self(self):
        from arandu.constants import ExtractedEntity
        from arandu.write.entity_resolution import _resolve_single

        entity = ExtractedEntity(name="eu", type="person")
        llm = FakeLLMProvider()

        # _resolve_single needs a session but self-reference returns early
        # We use a MagicMock for session since it won't be accessed
        result = run_async(
            _resolve_single(
                session=MagicMock(),
                user_id="user_123",
                entity=entity,
                existing_entities=[],
                alias_cache={},
                llm=llm,
            )
        )

        assert result.entity_key == "user:self"
        assert result.resolution_method == "self"

    def test_exact_match_by_alias(self):
        from arandu.write.entity_resolution import _exact_match

        # Create a mock entity
        mock_entity = MagicMock()
        mock_entity.canonical_key = "person:kevin"
        mock_entity.display_name = "Kevin"
        mock_entity.entity_type = "person"

        alias_cache = {"kevin": "person:kevin"}

        result = _exact_match("kevin", [mock_entity], alias_cache)
        assert result is not None
        assert result.canonical_key == "person:kevin"

    def test_fuzzy_match_returns_similar(self):
        from conftest import run_async

        from arandu.write.entity_resolution import _fuzzy_match

        mock_entity = MagicMock()
        mock_entity.canonical_key = "person:kevin_silva"
        mock_entity.display_name = "Kevin Silva"
        mock_entity.entity_type = "person"
        mock_entity.embedding_vec = None

        result = run_async(_fuzzy_match("Kevin", [mock_entity]))
        assert len(result) >= 1
        entity, score = result[0]
        assert entity.canonical_key == "person:kevin_silva"
        assert score > 0.5

    def test_slugify(self):
        from arandu.write.entity_resolution import _slugify

        assert _slugify("Kevin Silva") == "kevin_silva"
        assert _slugify("São Paulo") == "sao_paulo"
        assert _slugify("") == "unknown"

    def test_hint_parsing(self):
        from arandu.write.entity_resolution import _parse_subject_hint

        name, hint = _parse_subject_hint("Carol (namorada de Rafael)")
        assert name == "Carol"
        assert hint == "namorada de Rafael"

        name, hint = _parse_subject_hint("Kevin")
        assert name == "Kevin"
        assert hint is None


# ===========================================================================
# Test: reconcile
# ===========================================================================


class TestReconcile:
    def test_all_add_when_no_existing_facts(self):
        from arandu.constants import ExtractedFact
        from arandu.write.entity_resolution import ResolvedEntity
        from arandu.write.reconcile import _reconcile_subject_batch

        entity = ResolvedEntity(
            entity_type="person",
            entity_key="user:self",
            display_name="user",
            is_new=False,
            resolution_method="self",
        )
        facts = [
            ExtractedFact(subject="user", fact="Lives in São Paulo", confidence=0.95),
            ExtractedFact(subject="user", fact="Works at Google", confidence=0.9),
        ]

        # Mock session to return empty result for existing facts
        mock_session = AsyncMock()
        mock_result = MagicMock()
        mock_result.scalars.return_value.all.return_value = []
        mock_session.execute = AsyncMock(return_value=mock_result)

        llm = FakeLLMProvider()
        emb = FakeEmbeddingProvider()

        decisions = run_async(
            _reconcile_subject_batch(
                mock_session,
                "user_123",
                entity,
                facts,
                llm,
                emb,
            )
        )

        assert len(decisions) == 2
        assert all(d.action == "ADD" for d in decisions)

    def test_reconcile_groups_by_subject(self):
        from arandu.constants import ExtractedFact
        from arandu.write.entity_resolution import ResolvedEntity
        from arandu.write.reconcile import reconcile_facts

        entity_map = {
            "user": ResolvedEntity("person", "user:self", "user", False, "self"),
        }
        facts = [
            ExtractedFact(subject="user", fact="Lives in SP", confidence=0.9),
        ]

        mock_session = AsyncMock()
        mock_result = MagicMock()
        mock_result.scalars.return_value.all.return_value = []
        mock_session.execute = AsyncMock(return_value=mock_result)
        mock_session.begin_nested = MagicMock()
        mock_session.begin_nested.return_value.__aenter__ = AsyncMock()
        mock_session.begin_nested.return_value.__aexit__ = AsyncMock(return_value=False)

        llm = FakeLLMProvider()
        emb = FakeEmbeddingProvider()

        decisions = run_async(
            reconcile_facts(
                mock_session,
                "user_123",
                facts,
                entity_map,
                llm,
                emb,
            )
        )

        assert len(decisions) == 1
        assert decisions[0].action == "ADD"


# ===========================================================================
# Test: upsert
# ===========================================================================


class TestUpsert:
    def test_execute_upsert_add(self):
        from arandu.write.entity_resolution import ResolvedEntity
        from arandu.write.reconcile import ReconciliationDecision
        from arandu.write.upsert import execute_upsert

        entity_map = {
            "user": ResolvedEntity("person", "user:self", "user", False, "self"),
        }
        decisions = [
            ReconciliationDecision(
                fact_text="Lives in São Paulo",
                subject="user",
                action="ADD",
                confidence=0.95,
            ),
        ]

        mock_session = AsyncMock()
        mock_session.add = MagicMock()
        mock_session.flush = AsyncMock()
        mock_session.begin_nested = MagicMock()
        mock_session.begin_nested.return_value.__aenter__ = AsyncMock()
        mock_session.begin_nested.return_value.__aexit__ = AsyncMock(return_value=False)

        emb = FakeEmbeddingProvider()

        result = run_async(
            execute_upsert(
                mock_session,
                "user_123",
                "evt_1",
                decisions,
                entity_map,
                emb,
            )
        )

        assert result.added == 1
        assert result.updated == 0
        assert mock_session.add.called


# ===========================================================================
# Test: end-to-end pipeline
# ===========================================================================


class TestWritePipeline:
    def test_end_to_end_write(self):
        from arandu.config import MemoryConfig
        from arandu.write.pipeline import run_write_pipeline

        extraction_response = _make_extraction_response(
            entities=[
                {"name": "user", "type": "person"},
                {"name": "São Paulo", "type": "place"},
            ],
            facts=[
                {"subject": "user", "fact": "Lives in São Paulo", "confidence": 0.95},
            ],
            relations=[],
        )
        llm = FakeLLMProvider([extraction_response])
        emb = FakeEmbeddingProvider()
        config = MemoryConfig()

        # Mock session
        mock_session = AsyncMock()
        mock_session.add = MagicMock()
        mock_session.flush = AsyncMock()
        mock_session.begin_nested = MagicMock()
        mock_session.begin_nested.return_value.__aenter__ = AsyncMock()
        mock_session.begin_nested.return_value.__aexit__ = AsyncMock(return_value=False)

        # Mock execute for different queries (entity cache, alias cache, fact fetch)
        mock_result = MagicMock()
        mock_result.scalars.return_value.all.return_value = []
        mock_result.scalar_one_or_none.return_value = None
        mock_session.execute = AsyncMock(return_value=mock_result)

        result = run_async(
            run_write_pipeline(
                session=mock_session,
                user_id="user_123",
                message="Eu moro em São Paulo",
                llm=llm,
                embeddings=emb,
                config=config,
            )
        )

        assert result["event_id"]
        assert len(result["facts_added"]) >= 1
        assert result["duration_ms"] >= 0
        assert len(result["entities_resolved"]) >= 1

    def test_empty_message_returns_empty_result(self):
        from arandu.config import MemoryConfig
        from arandu.write.pipeline import run_write_pipeline

        llm = FakeLLMProvider()  # returns empty extraction
        emb = FakeEmbeddingProvider()
        config = MemoryConfig()

        mock_session = AsyncMock()
        mock_session.add = MagicMock()
        mock_session.flush = AsyncMock()

        result = run_async(
            run_write_pipeline(
                session=mock_session,
                user_id="user_123",
                message="oi",
                llm=llm,
                embeddings=emb,
                config=config,
            )
        )

        assert result["event_id"]
        assert len(result["facts_added"]) == 0
        assert result["facts_unchanged"] == 0
