"""Tests for spreading activation — hop 1-2 context expansion."""

from __future__ import annotations

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

from conftest import make_fact, run_async

from arandu.config import MemoryConfig
from arandu.read._helpers import is_fact_active, is_key_allowed
from arandu.read.spreading_activation import (
    SpreadingActivationResult,
    _compute_avg_seed_score,
    _entity_from_fact,
    _parse_uuids,
    _score_fact,
)

# ---------------------------------------------------------------------------
# Helper function tests
# ---------------------------------------------------------------------------


def test_entity_from_fact_with_colon():
    """Fact with colon in entity_key returns it directly."""
    fact = make_fact(entity_key="person:kevin", entity_type="person")
    assert _entity_from_fact(fact) == "person:kevin"


def test_entity_from_fact_without_colon():
    """Fact without colon gets entity_type prefix."""
    fact = make_fact(entity_key="kevin", entity_type="person")
    assert _entity_from_fact(fact) == "person:kevin"


def test_parse_uuids_valid():
    """Valid UUID strings should be parsed."""
    uid = uuid.uuid4()
    result = _parse_uuids([str(uid)])
    assert len(result) == 1
    assert result[0] == uid


def test_parse_uuids_invalid():
    """Invalid UUID strings should be skipped."""
    result = _parse_uuids(["not-a-uuid", "also-invalid"])
    assert result == []


def test_parse_uuids_mixed():
    """Mix of valid and invalid UUIDs."""
    uid = uuid.uuid4()
    result = _parse_uuids([str(uid), "bad", str(uuid.uuid4())])
    assert len(result) == 2


def test_compute_avg_seed_score_with_scores():
    """Average seed score from provided dict."""
    facts = [make_fact(id=uuid.UUID("00000000-0000-0000-0000-000000000001"))]
    scores = {"00000000-0000-0000-0000-000000000001": 0.8}
    avg = _compute_avg_seed_score(facts, scores)
    assert avg == 0.8


def test_compute_avg_seed_score_none():
    """Default 0.5 when no scores provided."""
    facts = [make_fact()]
    avg = _compute_avg_seed_score(facts, None)
    assert avg == 0.5


def test_score_fact_with_decay():
    """Scored fact should incorporate decay and avg seed score."""
    now = datetime.now(UTC)
    fact = make_fact(importance=0.5, times_retrieved=0, last_retrieved_at=None, user_correction_count=0)
    score = _score_fact(fact, decay=0.6, avg_seed_score=0.8, now=now)
    assert 0 < score < 1.0


# ---------------------------------------------------------------------------
# is_fact_active tests
# ---------------------------------------------------------------------------


def test_is_fact_active_no_expiry():
    """Active fact with no valid_to."""
    fact = make_fact(valid_to=None, is_stale=False)
    assert is_fact_active(fact) is True


def test_is_fact_active_stale():
    """Stale fact should not be active."""
    fact = make_fact(valid_to=None, is_stale=True)
    assert is_fact_active(fact) is False


def test_is_fact_active_expired():
    """Expired fact should not be active."""
    past = datetime(2020, 1, 1, tzinfo=UTC)
    fact = make_fact(valid_to=past, is_stale=False)
    assert is_fact_active(fact) is False


# ---------------------------------------------------------------------------
# is_key_allowed tests
# ---------------------------------------------------------------------------


def test_is_key_allowed_open_mode():
    """All keys allowed when allowed_keys is None."""
    assert is_key_allowed("any.key", None) is True


def test_is_key_allowed_empty_set():
    """All keys allowed when allowed_keys is empty set."""
    assert is_key_allowed("any.key", set()) is True


def test_is_key_allowed_in_set():
    """Key in allowed set."""
    assert is_key_allowed("preference", {"preference", "name"}) is True


def test_is_key_allowed_not_in_set():
    """Key not in allowed set."""
    assert is_key_allowed("secret", {"preference", "name"}) is False


def test_is_key_allowed_none_key():
    """None key is always allowed."""
    assert is_key_allowed(None, {"preference"}) is True


# ---------------------------------------------------------------------------
# SpreadingActivationResult dataclass
# ---------------------------------------------------------------------------


def test_spreading_result_defaults():
    """SpreadingActivationResult should have sane defaults."""
    result = SpreadingActivationResult()
    assert result.candidates == []
    assert result.entities_explored == []
    assert result.hop1_count == 0
    assert result.hop2_count == 0


# ---------------------------------------------------------------------------
# Config enforcement tests
# ---------------------------------------------------------------------------


def test_hops_zero_returns_empty():
    """With spreading_activation_hops=0, spreading returns empty with no DB queries."""
    from arandu.read.spreading_activation import spread_activation

    session = AsyncMock()
    config = MemoryConfig(spreading_activation_hops=0)

    result = run_async(
        spread_activation(
            session,
            "user_123",
            [str(uuid.uuid4())],
            config,
        )
    )

    assert result == []
    # No DB queries should have been made
    session.execute.assert_not_called()


def test_decay_factor_used_in_scoring():
    """Score should use config.spreading_decay_factor, not a hardcoded constant."""
    now = datetime.now(UTC)
    fact = make_fact(
        importance=1.0,
        times_retrieved=0,
        last_retrieved_at=None,
        user_correction_count=0,
    )

    # With decay 0.3
    score_low = _score_fact(fact, decay=0.3, avg_seed_score=1.0, now=now)
    # With decay 0.9
    score_high = _score_fact(fact, decay=0.9, avg_seed_score=1.0, now=now)

    # Score should scale with decay factor
    assert score_high > score_low
    assert abs(score_high / score_low - 3.0) < 0.01  # 0.9/0.3 = 3.0


def test_facts_per_entity_config_respected():
    """Hop 1 entity expansion should use config.spreading_facts_per_entity as limit."""
    from arandu.read.spreading_activation import _spread_activation_impl

    seed_id = uuid.uuid4()
    fact = make_fact(
        id=seed_id,
        entity_key="person:carlos",
        entity_type="person",
        user_id="user_123",
    )

    # Mock session to return seed fact then neighbor facts
    session = AsyncMock()
    call_count = 0

    async def mock_execute(stmt):
        nonlocal call_count
        call_count += 1
        mock_result = MagicMock()
        if call_count == 1:
            # Seed facts query
            mock_result.scalars.return_value.all.return_value = [fact]
        else:
            # All other queries return empty
            mock_result.scalars.return_value.all.return_value = []
            mock_result.all.return_value = []
        return mock_result

    session.execute = mock_execute

    config = MemoryConfig(
        spreading_activation_hops=1,
        spreading_facts_per_entity=7,
    )

    run_async(
        _spread_activation_impl(
            session,
            "user_123",
            [str(seed_id)],
            config,
        )
    )
