"""Tests for MemoryFactEntityLink — cross-entity fact linking."""

from __future__ import annotations

import uuid
from unittest.mock import AsyncMock, MagicMock

from conftest import run_async

from arandu.models import MemoryFact, MemoryFactEntityLink
from arandu.write.entity_resolution import ResolvedEntity
from arandu.write.upsert import _create_entity_links

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


def _make_fact(
    *,
    entity_key: str = "person:clara_rezende",
    fact_text: str = "Clara Rezende left Vertix.",
    user_id: str = "user_123",
) -> MemoryFact:
    """Create a minimal MemoryFact for testing."""
    fact = MagicMock(spec=MemoryFact)
    fact.id = uuid.uuid4()
    fact.entity_key = entity_key
    fact.fact_text = fact_text
    fact.user_id = user_id
    return fact


def _make_entity_map(*entities: tuple[str, str, str]) -> dict[str, ResolvedEntity]:
    """Create entity_map from (name_key, entity_key, display_name) tuples."""
    result: dict[str, ResolvedEntity] = {}
    for name_key, ek, display in entities:
        result[name_key] = ResolvedEntity(
            entity_type=ek.split(":")[0] if ":" in ek else "other",
            entity_key=ek,
            display_name=display,
            is_new=False,
            resolution_method="test",
        )
    return result


# ---------------------------------------------------------------------------
# Model tests
# ---------------------------------------------------------------------------


def test_link_model_fields():
    """MemoryFactEntityLink has all required fields."""
    link = MemoryFactEntityLink(
        fact_id=uuid.uuid4(),
        entity_key="person:clara",
        is_primary=True,
        user_id="user_123",
    )
    assert link.entity_key == "person:clara"
    assert link.is_primary is True
    assert link.user_id == "user_123"


# ---------------------------------------------------------------------------
# _create_entity_links tests
# ---------------------------------------------------------------------------


def test_creates_primary_link():
    """Creates a primary link for the fact's own entity_key."""
    session = AsyncMock()
    fact = _make_fact(entity_key="person:clara_rezende", fact_text="Clara Rezende is an engineer.")
    entity_map = _make_entity_map(
        ("clara rezende", "person:clara_rezende", "Clara Rezende"),
    )

    run_async(_create_entity_links(session, fact, entity_map))

    # Should have called session.add at least once (primary link)
    added_objects = [call.args[0] for call in session.add.call_args_list]
    primary_links = [o for o in added_objects if isinstance(o, MemoryFactEntityLink) and o.is_primary]
    assert len(primary_links) == 1
    assert primary_links[0].entity_key == "person:clara_rezende"


def test_creates_secondary_links():
    """Creates secondary links for entities mentioned in fact text."""
    session = AsyncMock()
    fact = _make_fact(
        entity_key="person:clara_rezende",
        fact_text="Clara Rezende left Vertix and joined Orion Tech.",
    )
    entity_map = _make_entity_map(
        ("clara rezende", "person:clara_rezende", "Clara Rezende"),
        ("vertix", "organization:vertix", "Vertix"),
        ("orion tech", "organization:orion_tech", "Orion Tech"),
    )

    run_async(_create_entity_links(session, fact, entity_map))

    added_objects = [call.args[0] for call in session.add.call_args_list]
    links = [o for o in added_objects if isinstance(o, MemoryFactEntityLink)]

    entity_keys = {lnk.entity_key for lnk in links}
    assert "person:clara_rezende" in entity_keys  # primary
    assert "organization:vertix" in entity_keys  # secondary
    assert "organization:orion_tech" in entity_keys  # secondary
    assert len(links) == 3


def test_skips_short_display_names():
    """Entity display names shorter than 3 chars are skipped (avoid false positives)."""
    session = AsyncMock()
    fact = _make_fact(
        entity_key="person:jo",
        fact_text="Jo works at AI Corp.",
    )
    entity_map = _make_entity_map(
        ("jo", "person:jo", "Jo"),  # 2 chars — should be skipped for secondary
        ("ai corp", "organization:ai_corp", "AI Corp"),
    )

    run_async(_create_entity_links(session, fact, entity_map))

    added_objects = [call.args[0] for call in session.add.call_args_list]
    links = [o for o in added_objects if isinstance(o, MemoryFactEntityLink)]

    # Primary link for person:jo (always created regardless of length)
    # Secondary for AI Corp (5 chars, matches)
    # "Jo" as secondary would be skipped (< 3 chars) but it's already primary
    entity_keys = {lnk.entity_key for lnk in links}
    assert "person:jo" in entity_keys
    assert "organization:ai_corp" in entity_keys


def test_no_duplicate_primary_and_secondary():
    """Primary entity_key is not duplicated as secondary link."""
    session = AsyncMock()
    fact = _make_fact(
        entity_key="person:clara_rezende",
        fact_text="Clara Rezende is an engineer at Vertix.",
    )
    entity_map = _make_entity_map(
        ("clara rezende", "person:clara_rezende", "Clara Rezende"),
        ("vertix", "organization:vertix", "Vertix"),
    )

    run_async(_create_entity_links(session, fact, entity_map))

    added_objects = [call.args[0] for call in session.add.call_args_list]
    links = [o for o in added_objects if isinstance(o, MemoryFactEntityLink)]

    clara_links = [lnk for lnk in links if lnk.entity_key == "person:clara_rezende"]
    assert len(clara_links) == 1  # only primary, no secondary duplicate


def test_failsafe_on_error():
    """If link creation fails, no exception propagates."""
    session = AsyncMock()
    session.flush.side_effect = Exception("DB error")

    fact = _make_fact()
    entity_map = _make_entity_map(
        ("clara rezende", "person:clara_rezende", "Clara Rezende"),
    )

    # Should not raise
    run_async(_create_entity_links(session, fact, entity_map))


def test_empty_entity_map():
    """Empty entity_map creates only primary link."""
    session = AsyncMock()
    fact = _make_fact(entity_key="person:clara_rezende")

    run_async(_create_entity_links(session, fact, {}))

    added_objects = [call.args[0] for call in session.add.call_args_list]
    links = [o for o in added_objects if isinstance(o, MemoryFactEntityLink)]
    assert len(links) == 1
    assert links[0].is_primary is True
