"""Tests for relation lifecycle: evidence linkage and cascade invalidation.

Covers:
- _match_relation_to_fact heuristic (match found, no match, multiple candidates)
- _cascade_invalidate_relations (UPDATE cascade, DELETE cascade, no-evidence skip)
- UpsertResult.created_facts accumulation
"""

from __future__ import annotations

import uuid
from datetime import UTC, datetime
from unittest.mock import AsyncMock, MagicMock

import pytest
from conftest import make_fact

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _make_resolved_entity(
    entity_key: str = "person:ana",
    display_name: str = "Ana",
    entity_type: str = "person",
) -> MagicMock:
    """Create a mock ResolvedEntity."""
    entity = MagicMock()
    entity.entity_key = entity_key
    entity.display_name = display_name
    entity.entity_type = entity_type
    entity.is_new = False
    entity.resolution_method = "exact"
    entity.fuzzy_score = None
    return entity


def _make_relation(source: str, target: str, rel_type: str = "works_at") -> MagicMock:
    """Create a mock ExtractedRelation."""
    rel = MagicMock()
    rel.source = source
    rel.target = target
    rel.rel_type = rel_type
    return rel


def _make_mock_relationship(
    evidence_fact_id: uuid.UUID | None = None,
    invalidated_at: datetime | None = None,
) -> MagicMock:
    """Create a mock MemoryEntityRelationship."""
    from arandu.models import MemoryEntityRelationship

    rel = MagicMock(spec=MemoryEntityRelationship)
    rel.evidence_fact_id = evidence_fact_id
    rel.invalidated_at = invalidated_at
    rel.valid_to = None
    return rel


# ===========================================================================
# Tests: _match_relation_to_fact
# ===========================================================================


class TestMatchRelationToFact:
    """Tests for the heuristic match between relations and created facts."""

    def test_match_found_when_both_names_in_fact_text(self):
        """Match succeeds when fact_text contains both source and target names."""
        from arandu.write.upsert import _match_relation_to_fact

        fact_id = uuid.uuid4()
        fact = make_fact(
            id=fact_id,
            entity_key="person:ana",
            fact_text="Ana works at Acme Corp since 2024",
            confidence=0.95,
        )

        source = _make_resolved_entity("person:ana", "Ana")
        target = _make_resolved_entity("organization:acme_corp", "Acme Corp")
        created_facts = {"person:ana": [fact]}

        result = _match_relation_to_fact(source, target, created_facts)
        assert result == fact_id

    def test_no_match_when_target_name_missing(self):
        """Match returns None when fact_text doesn't mention the target."""
        from arandu.write.upsert import _match_relation_to_fact

        fact = make_fact(
            entity_key="person:ana",
            fact_text="Ana likes coffee",
            confidence=0.90,
        )

        source = _make_resolved_entity("person:ana", "Ana")
        target = _make_resolved_entity("organization:acme_corp", "Acme Corp")
        created_facts = {"person:ana": [fact]}

        result = _match_relation_to_fact(source, target, created_facts)
        assert result is None

    def test_no_match_when_created_facts_empty(self):
        """Match returns None when no created facts are provided."""
        from arandu.write.upsert import _match_relation_to_fact

        source = _make_resolved_entity("person:ana", "Ana")
        target = _make_resolved_entity("organization:acme_corp", "Acme Corp")

        result = _match_relation_to_fact(source, target, {})
        assert result is None

    def test_multiple_candidates_picks_highest_confidence(self):
        """When multiple facts match, the one with highest confidence wins."""
        from arandu.write.upsert import _match_relation_to_fact

        low_id = uuid.uuid4()
        high_id = uuid.uuid4()

        low_fact = make_fact(
            id=low_id,
            entity_key="person:ana",
            fact_text="Ana works at Acme Corp part-time",
            confidence=0.60,
        )
        high_fact = make_fact(
            id=high_id,
            entity_key="person:ana",
            fact_text="Ana works at Acme Corp as lead engineer",
            confidence=0.95,
        )

        source = _make_resolved_entity("person:ana", "Ana")
        target = _make_resolved_entity("organization:acme_corp", "Acme Corp")
        created_facts = {"person:ana": [low_fact, high_fact]}

        result = _match_relation_to_fact(source, target, created_facts)
        assert result == high_id

    def test_match_from_target_entity_key(self):
        """Match works when the matching fact is under the target's entity_key."""
        from arandu.write.upsert import _match_relation_to_fact

        fact_id = uuid.uuid4()
        fact = make_fact(
            id=fact_id,
            entity_key="organization:acme_corp",
            fact_text="Acme Corp employs Ana as engineer",
            confidence=0.85,
        )

        source = _make_resolved_entity("person:ana", "Ana")
        target = _make_resolved_entity("organization:acme_corp", "Acme Corp")
        created_facts = {"organization:acme_corp": [fact]}

        result = _match_relation_to_fact(source, target, created_facts)
        assert result == fact_id

    def test_match_is_case_insensitive(self):
        """Match is case-insensitive for display names and fact_text."""
        from arandu.write.upsert import _match_relation_to_fact

        fact_id = uuid.uuid4()
        fact = make_fact(
            id=fact_id,
            entity_key="person:ana",
            fact_text="ana lives in curitiba with her family",
            confidence=0.80,
        )

        source = _make_resolved_entity("person:ana", "Ana")
        target = _make_resolved_entity("place:curitiba", "Curitiba")
        created_facts = {"person:ana": [fact]}

        result = _match_relation_to_fact(source, target, created_facts)
        assert result == fact_id


# ===========================================================================
# Tests: _cascade_invalidate_relations
# ===========================================================================


class TestCascadeInvalidateRelations:
    """Tests for cascade invalidation of relations when a fact is invalidated."""

    @pytest.mark.asyncio
    async def test_cascade_invalidates_matching_relations(self):
        """Relations with matching evidence_fact_id are invalidated."""
        from arandu.write.upsert import _cascade_invalidate_relations

        fact_id = uuid.uuid4()
        now = datetime.now(UTC)

        rel1 = _make_mock_relationship(evidence_fact_id=fact_id)
        rel2 = _make_mock_relationship(evidence_fact_id=fact_id)

        mock_result = MagicMock()
        mock_result.scalars.return_value.all.return_value = [rel1, rel2]

        session = AsyncMock()
        session.execute.return_value = mock_result

        count = await _cascade_invalidate_relations(session, fact_id, now)

        assert count == 2
        assert rel1.valid_to == now
        assert rel1.invalidated_at == now
        assert rel2.valid_to == now
        assert rel2.invalidated_at == now

    @pytest.mark.asyncio
    async def test_cascade_returns_zero_when_no_matching_relations(self):
        """Returns 0 when no relations reference the given fact."""
        from arandu.write.upsert import _cascade_invalidate_relations

        fact_id = uuid.uuid4()
        now = datetime.now(UTC)

        mock_result = MagicMock()
        mock_result.scalars.return_value.all.return_value = []

        session = AsyncMock()
        session.execute.return_value = mock_result

        count = await _cascade_invalidate_relations(session, fact_id, now)
        assert count == 0

    @pytest.mark.asyncio
    async def test_cascade_does_not_touch_already_invalidated_relations(self):
        """Already invalidated relations are not returned by the query filter."""
        from arandu.write.upsert import _cascade_invalidate_relations

        fact_id = uuid.uuid4()
        now = datetime.now(UTC)

        # The query filters invalidated_at.is_(None), so already-invalidated
        # relations won't be in the result set. We simulate this by returning
        # an empty list.
        mock_result = MagicMock()
        mock_result.scalars.return_value.all.return_value = []

        session = AsyncMock()
        session.execute.return_value = mock_result

        count = await _cascade_invalidate_relations(session, fact_id, now)
        assert count == 0


# ===========================================================================
# Tests: UpsertResult.created_facts
# ===========================================================================


class TestUpsertResultCreatedFacts:
    """Tests for the created_facts field on UpsertResult."""

    def test_created_facts_defaults_to_empty_dict(self):
        """UpsertResult.created_facts is an empty dict by default."""
        from arandu.write.upsert import UpsertResult

        result = UpsertResult()
        assert result.created_facts == {}

    def test_created_facts_accumulates_by_entity_key(self):
        """created_facts groups facts by entity_key."""
        from arandu.write.upsert import UpsertResult

        result = UpsertResult()
        fact1 = make_fact(entity_key="person:ana")
        fact2 = make_fact(entity_key="person:ana")
        fact3 = make_fact(entity_key="organization:acme")

        result.created_facts.setdefault("person:ana", []).append(fact1)
        result.created_facts.setdefault("person:ana", []).append(fact2)
        result.created_facts.setdefault("organization:acme", []).append(fact3)

        assert len(result.created_facts["person:ana"]) == 2
        assert len(result.created_facts["organization:acme"]) == 1
