"""
Tests for cortex/capture/activation_router.py — W3 activation/storage router (#358).

Covers:
    - Deterministic policy derivation from envelope fields
    - scope narrowing by sensitivity
    - priority computation (stability × explicitness + intrinsic bonus)
    - decay factor by stability + sensitivity
    - trigger_pattern (session_boot, task_start, preference_query, entity_context)
    - suppressors by sensitivity + human_family
    - owner_id propagation (never defaulted away)
    - session_boot threshold behaviour
    - write() best-effort (no create_activation → silent False)
    - write() storage error → silent False
    - write() happy path → True
    - integration: activation stored in SQLiteAdapter (memory, full schema)
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Optional
from unittest.mock import AsyncMock, MagicMock

import pytest
import pytest_asyncio

from cortex.capture.activation_router import (
    ActivationPolicy,
    ActivationRouter,
    _SESSION_BOOT_PRIORITY_THRESHOLD,
    derive_activation,
)


# ---------------------------------------------------------------------------
# Minimal candidate stubs
# ---------------------------------------------------------------------------

@dataclass
class _Cand:
    """Minimal candidate with v1.2 envelope fields."""
    intrinsic_type: str = "fact"
    human_family: Optional[str] = None
    explicitness: str = "inferred"
    stability: str = "volatile"
    sensitivity: str = "normal"


# ---------------------------------------------------------------------------
# derive() — scope
# ---------------------------------------------------------------------------

class TestScope:
    def setup_method(self):
        self.router = ActivationRouter()

    def test_sensitive_maps_to_private(self):
        cand = _Cand(sensitivity="sensitive")
        p = self.router.derive(cand, "claim-1", "owner-1")
        assert p.scope == "private"

    def test_personal_maps_to_user(self):
        cand = _Cand(sensitivity="personal")
        p = self.router.derive(cand, "claim-1", "owner-1")
        assert p.scope == "user"

    def test_normal_maps_to_user(self):
        cand = _Cand(sensitivity="normal")
        p = self.router.derive(cand, "claim-1", "owner-1")
        assert p.scope == "user"

    def test_human_family_upgrades_session_to_user(self):
        # When scope would be 'session' and human_family is set → upgrade to user.
        # Normal sensitivity already gives 'user', so test an edge: make a custom
        # router that would produce session and verify it upgrades.
        # Directly exercise the guard condition:
        cand = _Cand(sensitivity="normal", stability="volatile", human_family="friend")
        p = self.router.derive(cand, "claim-1", "owner-1")
        assert p.scope in ("user", "private")  # never 'session' when human_family set

    def test_owner_id_propagated(self):
        p = self.router.derive(_Cand(), "claim-x", "alice")
        assert p.owner_id == "alice"

    def test_owner_id_not_defaulted(self):
        p = self.router.derive(_Cand(), "claim-x", "tenant-99")
        assert p.owner_id == "tenant-99"


# ---------------------------------------------------------------------------
# derive() — priority
# ---------------------------------------------------------------------------

class TestPriority:
    def setup_method(self):
        self.router = ActivationRouter()

    def test_stable_explicit_fact_gets_high_priority(self):
        cand = _Cand(stability="stable", explicitness="explicit", intrinsic_type="fact")
        p = self.router.derive(cand, "c1", "o1")
        # base=8, bonus=1 → 9
        assert p.priority == 9

    def test_stable_explicit_goal_gets_highest_priority(self):
        cand = _Cand(stability="stable", explicitness="explicit", intrinsic_type="goal")
        p = self.router.derive(cand, "c1", "o1")
        # base=8, bonus=2 → 10 (capped)
        assert p.priority == 10

    def test_volatile_inferred_has_lowest_priority(self):
        cand = _Cand(stability="volatile", explicitness="inferred", intrinsic_type="fact")
        p = self.router.derive(cand, "c1", "o1")
        # base=1, bonus=1 → 2
        assert p.priority == 2

    def test_goal_bonus_is_2(self):
        cand_fact = _Cand(stability="mutable", explicitness="explicit", intrinsic_type="fact")
        cand_goal = _Cand(stability="mutable", explicitness="explicit", intrinsic_type="goal")
        p_fact = self.router.derive(cand_fact, "c1", "o1")
        p_goal = self.router.derive(cand_goal, "c2", "o1")
        assert p_goal.priority == p_fact.priority + 1  # 5+1 fact vs 5+2 goal

    def test_priority_capped_at_10(self):
        cand = _Cand(stability="stable", explicitness="explicit", intrinsic_type="goal")
        p = self.router.derive(cand, "c1", "o1")
        assert p.priority <= 10

    def test_priority_minimum_0(self):
        # Even the weakest combination should be >= 0
        cand = _Cand(stability="volatile", explicitness="inferred", intrinsic_type="fact")
        p = self.router.derive(cand, "c1", "o1")
        assert p.priority >= 0


# ---------------------------------------------------------------------------
# derive() — decay
# ---------------------------------------------------------------------------

class TestDecay:
    def setup_method(self):
        self.router = ActivationRouter()

    def test_stable_no_decay(self):
        cand = _Cand(stability="stable", sensitivity="normal")
        p = self.router.derive(cand, "c1", "o1")
        assert p.decay_factor == 1.0

    def test_mutable_partial_decay(self):
        cand = _Cand(stability="mutable", sensitivity="normal")
        p = self.router.derive(cand, "c1", "o1")
        assert p.decay_factor == 0.85

    def test_volatile_fast_decay(self):
        cand = _Cand(stability="volatile", sensitivity="normal")
        p = self.router.derive(cand, "c1", "o1")
        assert p.decay_factor == 0.5

    def test_sensitive_applies_decay_multiplier(self):
        cand_normal = _Cand(stability="stable", sensitivity="normal")
        cand_sensitive = _Cand(stability="stable", sensitivity="sensitive")
        p_normal = self.router.derive(cand_normal, "c1", "o1")
        p_sensitive = self.router.derive(cand_sensitive, "c2", "o1")
        assert p_sensitive.decay_factor < p_normal.decay_factor


# ---------------------------------------------------------------------------
# derive() — triggers
# ---------------------------------------------------------------------------

class TestTriggers:
    def setup_method(self):
        self.router = ActivationRouter()

    def test_session_boot_for_high_priority_stable(self):
        # priority >= _SESSION_BOOT_PRIORITY_THRESHOLD → includes session_boot
        cand = _Cand(stability="stable", explicitness="explicit", intrinsic_type="fact")
        p = self.router.derive(cand, "c1", "o1")
        assert p.priority >= _SESSION_BOOT_PRIORITY_THRESHOLD
        assert p.trigger_pattern is not None
        assert "session_boot" in p.trigger_pattern

    def test_no_session_boot_for_low_priority_volatile(self):
        cand = _Cand(stability="volatile", explicitness="inferred", intrinsic_type="fact")
        p = self.router.derive(cand, "c1", "o1")
        if p.trigger_pattern:
            assert "session_boot" not in p.trigger_pattern

    def test_goal_gets_task_start_trigger(self):
        cand = _Cand(stability="mutable", explicitness="explicit", intrinsic_type="goal")
        p = self.router.derive(cand, "c1", "o1")
        assert p.trigger_pattern is not None
        assert "task_start" in p.trigger_pattern

    def test_preference_gets_preference_query_trigger(self):
        cand = _Cand(stability="mutable", explicitness="implied", intrinsic_type="preference")
        p = self.router.derive(cand, "c1", "o1")
        assert p.trigger_pattern is not None
        assert "preference_query" in p.trigger_pattern

    def test_human_family_gets_entity_context_trigger(self):
        cand = _Cand(human_family="friend")
        p = self.router.derive(cand, "c1", "o1")
        assert p.trigger_pattern is not None
        assert "entity_context:friend" in p.trigger_pattern

    def test_volatile_inferred_no_trigger(self):
        cand = _Cand(stability="volatile", explicitness="inferred", intrinsic_type="fact")
        p = self.router.derive(cand, "c1", "o1")
        # priority=2 < threshold=6 → no session_boot; no human_family → no entity_context
        # fact has no type-specific trigger
        assert p.trigger_pattern is None


# ---------------------------------------------------------------------------
# derive() — suppressors
# ---------------------------------------------------------------------------

class TestSuppressors:
    def setup_method(self):
        self.router = ActivationRouter()

    def test_sensitive_gets_broad_suppressors(self):
        cand = _Cand(sensitivity="sensitive")
        p = self.router.derive(cand, "c1", "o1")
        assert "low_trust_context" in p.suppressors
        assert "group_context" in p.suppressors
        assert "public_context" in p.suppressors

    def test_personal_gets_group_suppressor(self):
        cand = _Cand(sensitivity="personal")
        p = self.router.derive(cand, "c1", "o1")
        assert "group_context" in p.suppressors
        assert "public_context" in p.suppressors

    def test_normal_no_default_suppressors(self):
        cand = _Cand(sensitivity="normal", human_family=None)
        p = self.router.derive(cand, "c1", "o1")
        assert "low_trust_context" not in p.suppressors
        assert "group_context" not in p.suppressors

    def test_human_family_adds_unrelated_entity_suppressor(self):
        cand = _Cand(human_family="colleague", sensitivity="normal")
        p = self.router.derive(cand, "c1", "o1")
        assert "unrelated_entity_context" in p.suppressors

    def test_no_duplicate_suppressors(self):
        cand = _Cand(sensitivity="sensitive", human_family="family")
        p = self.router.derive(cand, "c1", "o1")
        assert len(p.suppressors) == len(set(p.suppressors))


# ---------------------------------------------------------------------------
# derive() — rationale field
# ---------------------------------------------------------------------------

class TestRationale:
    def setup_method(self):
        self.router = ActivationRouter()

    def test_rationale_is_non_empty(self):
        p = self.router.derive(_Cand(), "c1", "o1")
        assert p.rationale and len(p.rationale) > 10

    def test_rationale_contains_key_fields(self):
        cand = _Cand(stability="stable", sensitivity="sensitive", intrinsic_type="goal")
        p = self.router.derive(cand, "c1", "o1")
        assert "stable" in p.rationale
        assert "sensitive" in p.rationale
        assert "goal" in p.rationale


# ---------------------------------------------------------------------------
# Legacy / missing envelope fields — safe defaults
# ---------------------------------------------------------------------------

class TestLegacyCandidates:
    def setup_method(self):
        self.router = ActivationRouter()

    def test_empty_object_uses_safe_defaults(self):
        """derive() with an object missing all envelope fields should not raise."""
        class _Empty:
            pass

        p = self.router.derive(_Empty(), "c1", "o1")
        assert isinstance(p, ActivationPolicy)
        assert p.scope in ("user", "session", "private")
        assert 0 <= p.priority <= 10
        assert 0.0 <= p.decay_factor <= 1.0

    def test_none_fields_coerced_to_defaults(self):
        cand = _Cand(intrinsic_type=None, explicitness=None, stability=None, sensitivity=None)
        p = self.router.derive(cand, "c1", "o1")
        assert isinstance(p, ActivationPolicy)


# ---------------------------------------------------------------------------
# module-level convenience function
# ---------------------------------------------------------------------------

class TestModuleLevelConvenience:
    def test_derive_activation_returns_policy(self):
        cand = _Cand(stability="stable", explicitness="explicit")
        p = derive_activation(cand, "c1", "o1")
        assert isinstance(p, ActivationPolicy)


# ---------------------------------------------------------------------------
# write() — best-effort
# ---------------------------------------------------------------------------

class TestWrite:
    def setup_method(self):
        self.router = ActivationRouter()

    @pytest.mark.asyncio
    async def test_write_returns_false_when_no_create_activation(self):
        storage = MagicMock(spec=[])  # no create_activation attribute
        policy = ActivationPolicy(claim_id="c1", owner_id="o1")
        result = await self.router.write(storage, policy)
        assert result is False

    @pytest.mark.asyncio
    async def test_write_returns_false_on_storage_error(self):
        storage = MagicMock()
        storage.create_activation = AsyncMock(side_effect=RuntimeError("db error"))
        policy = ActivationPolicy(claim_id="c1", owner_id="o1")
        result = await self.router.write(storage, policy)
        assert result is False

    @pytest.mark.asyncio
    async def test_write_returns_true_on_success(self):
        storage = MagicMock()
        storage.create_activation = AsyncMock(return_value=MagicMock())
        policy = ActivationPolicy(claim_id="c1", owner_id="o1", scope="user", priority=5)
        result = await self.router.write(storage, policy)
        assert result is True

    @pytest.mark.asyncio
    async def test_write_passes_correct_kwargs(self):
        storage = MagicMock()
        storage.create_activation = AsyncMock(return_value=MagicMock())
        policy = ActivationPolicy(
            claim_id="claim-abc",
            owner_id="tenant-xyz",
            scope="private",
            trigger_pattern="session_boot",
            suppressors=["group_context"],
            priority=7,
            decay_factor=0.9,
        )
        await self.router.write(storage, policy)
        storage.create_activation.assert_awaited_once()
        call_kwargs = storage.create_activation.call_args.kwargs
        assert call_kwargs["claim_id"] == "claim-abc"
        assert call_kwargs["owner_id"] == "tenant-xyz"
        assert call_kwargs["scope"] == "private"
        assert call_kwargs["trigger_pattern"] == "session_boot"
        assert call_kwargs["priority"] == 7
        assert call_kwargs["decay_factor"] == 0.9
        import json
        suppressors = json.loads(call_kwargs["suppressors_json"])
        assert "group_context" in suppressors


# ---------------------------------------------------------------------------
# Integration: SQLiteAdapter + ActivationRouter
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_sqlite_create_and_fetch_activation():
    """End-to-end: write an activation row, read it back via the real SQLiteAdapter."""
    from cortex.storage.sqlite_adapter import SQLiteAdapter

    async with SQLiteAdapter(":memory:") as adapter:
        # Create a dummy claim to satisfy the FK
        claim = await adapter.create_claim(
            subject="Eva",
            predicate="runs_on",
            object_value="eva-core",
            owner_id="test-owner",
        )

        # Write activation via router
        router = ActivationRouter()
        cand = _Cand(
            stability="stable",
            explicitness="explicit",
            intrinsic_type="fact",
            sensitivity="normal",
            human_family=None,
        )
        policy = router.derive(cand, claim.id, "test-owner")
        written = await router.write(adapter, policy)

        assert written is True

        # Read back
        activation = await adapter.get_activation_by_claim(claim.id, "test-owner")
        assert activation is not None
        assert activation.claim_id == claim.id
        assert activation.owner_id == "test-owner"
        assert activation.scope == "user"
        assert activation.priority >= 6  # stable + explicit should be ≥ 6
        assert "session_boot" in (activation.trigger_pattern or "")
        assert activation.decay_factor == 1.0  # stable, normal sensitivity


@pytest.mark.asyncio
async def test_sqlite_get_session_boot_activations():
    """session_boot query only returns high-priority claims with session_boot trigger."""
    from cortex.storage.sqlite_adapter import SQLiteAdapter

    async with SQLiteAdapter(":memory:") as adapter:
        router = ActivationRouter()

        # High-priority stable fact — should appear in session_boot results
        claim_hi = await adapter.create_claim(
            subject="Andrew", predicate="prefers", object_value="dark mode",
            owner_id="owner-1",
        )
        policy_hi = router.derive(
            _Cand(stability="stable", explicitness="explicit", intrinsic_type="fact"),
            claim_hi.id, "owner-1",
        )
        await router.write(adapter, policy_hi)

        # Low-priority volatile — should NOT appear in session_boot results
        claim_lo = await adapter.create_claim(
            subject="Andrew", predicate="mentioned", object_value="coffee",
            owner_id="owner-1",
        )
        policy_lo = router.derive(
            _Cand(stability="volatile", explicitness="inferred", intrinsic_type="fact"),
            claim_lo.id, "owner-1",
        )
        await router.write(adapter, policy_lo)

        boot_activations = await adapter.get_session_boot_activations("owner-1")
        boot_claim_ids = {a.claim_id for a in boot_activations}

        assert claim_hi.id in boot_claim_ids
        assert claim_lo.id not in boot_claim_ids


@pytest.mark.asyncio
async def test_sqlite_owner_scope_enforced_on_fetch():
    """get_activation_by_claim must not return rows belonging to a different owner."""
    from cortex.storage.sqlite_adapter import SQLiteAdapter

    async with SQLiteAdapter(":memory:") as adapter:
        router = ActivationRouter()
        claim = await adapter.create_claim(
            subject="Alice", predicate="knows", object_value="Bob",
            owner_id="owner-A",
        )
        policy = router.derive(_Cand(), claim.id, "owner-A")
        await router.write(adapter, policy)

        # Query with wrong owner → should return None
        result = await adapter.get_activation_by_claim(claim.id, "owner-B")
        assert result is None

        # Query with correct owner → should return row
        result = await adapter.get_activation_by_claim(claim.id, "owner-A")
        assert result is not None


@pytest.mark.asyncio
async def test_sensitive_claim_gets_private_scope_in_db():
    """Sensitive claims must be stored with scope='private'."""
    from cortex.storage.sqlite_adapter import SQLiteAdapter

    async with SQLiteAdapter(":memory:") as adapter:
        router = ActivationRouter()
        claim = await adapter.create_claim(
            subject="Eva", predicate="personal_note", object_value="health info",
            owner_id="owner-1",
            sensitivity="sensitive",
        )
        policy = router.derive(
            _Cand(sensitivity="sensitive", stability="stable", explicitness="explicit"),
            claim.id, "owner-1",
        )
        await router.write(adapter, policy)

        activation = await adapter.get_activation_by_claim(claim.id, "owner-1")
        assert activation is not None
        assert activation.scope == "private"

        # Also verify suppressors include low_trust_context
        assert "low_trust_context" in activation.suppressors
