"""
Integration tests for P29 neuroscience items 121-125.
Run: python -m pytest tests/test_neuro_p29.py -v
"""
import sys
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest

ROOT = Path(__file__).resolve().parent.parent
if str(ROOT) not in sys.path:
    sys.path.insert(0, str(ROOT))


# ---------------------------------------------------------------------------
# Item 121: Learning rate annealing per domain (stdp_annealing.py)
# ---------------------------------------------------------------------------

class TestItem121LearningRateAnnealing:

    def test_new_domain_returns_fast_rate(self):
        """Unknown/sparse domains return maturity factor 1.5 (learn fast)."""
        from pulse.brain.plasticity.stdp_annealing import _get_domain_maturity_factor
        with patch("pulse.core.brain_db.get_conn") as mock_gc:
            conn = MagicMock()
            conn.execute.return_value.fetchone.return_value = {"cnt": 2, "avg_conf": 0.3}
            mock_gc.return_value = conn
            factor = _get_domain_maturity_factor("new_domain_xyz")
        assert factor == 1.5, f"Expected 1.5 for new domain, got {factor}"

    def test_mature_domain_returns_slow_rate(self):
        """Mature domain (many items, high confidence) returns factor 0.5."""
        from pulse.brain.plasticity.stdp_annealing import _get_domain_maturity_factor
        with patch("pulse.core.brain_db.get_conn") as mock_gc:
            conn = MagicMock()
            conn.execute.return_value.fetchone.return_value = {"cnt": 25, "avg_conf": 0.85}
            mock_gc.return_value = conn
            factor = _get_domain_maturity_factor("mature_domain")
        assert factor == 0.5, f"Expected 0.5 for mature domain, got {factor}"

    def test_intermediate_domain_returns_normal_rate(self):
        """Intermediate domain (10+ items, moderate confidence) returns 1.0."""
        from pulse.brain.plasticity.stdp_annealing import _get_domain_maturity_factor
        with patch("pulse.core.brain_db.get_conn") as mock_gc:
            conn = MagicMock()
            conn.execute.return_value.fetchone.return_value = {"cnt": 12, "avg_conf": 0.6}
            mock_gc.return_value = conn
            factor = _get_domain_maturity_factor("intermediate_domain")
        assert factor == 1.0, f"Expected 1.0 for intermediate domain, got {factor}"

    def test_empty_domain_returns_default(self):
        """Empty string domain returns 1.0 (fail-open default)."""
        from pulse.brain.plasticity.stdp_annealing import _get_domain_maturity_factor
        factor = _get_domain_maturity_factor("")
        assert factor == 1.0

    def test_apply_stdp_with_annealing_scales_deltas(self):
        """apply_stdp_with_annealing delegates to apply_stdp with scaled deltas."""
        from pulse.brain import stdp_annealing
        import pulse.brain.plasticity.stdp as stdp_mod
        original_ltp = stdp_mod.LTP_DELTA
        original_ltd = stdp_mod.LTD_DELTA
        captured = []

        def fake_apply(session_id, success):
            captured.append((stdp_mod.LTP_DELTA, stdp_mod.LTD_DELTA))
            return 3

        with patch.object(stdp_mod, "apply_stdp", side_effect=fake_apply):
            with patch.object(stdp_annealing, "_get_domain_maturity_factor", return_value=1.5):
                result = stdp_annealing.apply_stdp_with_annealing("sess_1", True, "new_d")

        assert result == 3
        assert captured[0][0] == pytest.approx(original_ltp * 1.5, abs=1e-4)
        # Deltas restored
        assert stdp_mod.LTP_DELTA == pytest.approx(original_ltp, abs=1e-6)

    def test_apply_stdp_with_annealing_restores_on_exception(self):
        """LTP/LTD deltas are restored even if apply_stdp raises."""
        from pulse.brain import stdp_annealing
        import pulse.brain.plasticity.stdp as stdp_mod
        original_ltp = stdp_mod.LTP_DELTA
        original_ltd = stdp_mod.LTD_DELTA

        with patch.object(stdp_mod, "apply_stdp", side_effect=RuntimeError("boom")):
            with patch.object(stdp_annealing, "_get_domain_maturity_factor", return_value=2.0):
                result = stdp_annealing.apply_stdp_with_annealing("sess_err", False)

        assert result == 0  # fail-open
        assert stdp_mod.LTP_DELTA == pytest.approx(original_ltp, abs=1e-6)
        assert stdp_mod.LTD_DELTA == pytest.approx(original_ltd, abs=1e-6)


# ---------------------------------------------------------------------------
# Item 122: Catastrophic forgetting protection (forgetting_protection.py)
# ---------------------------------------------------------------------------

class TestItem122CatastrophicForgetting:

    def test_low_confidence_existing_item_unchanged(self):
        """When existing item has confidence <= 0.8, IDs are unchanged."""
        from pulse.core.forgetting_protection import protect_high_confidence
        conn = MagicMock()
        row = {"confidence": 0.5, "id": "L-existing"}
        conn.execute.return_value.fetchone.return_value = row
        item_id, fp = protect_high_confidence(conn, "lessons", "fp123", "L-abc", "2026-01-01")
        assert item_id == "L-abc"
        assert fp == "fp123"

    def test_high_confidence_existing_generates_new_id(self):
        """When existing item has confidence > 0.8, new item gets a distinct ID."""
        from pulse.core.forgetting_protection import protect_high_confidence
        conn = MagicMock()
        row = {"confidence": 0.95, "id": "L-existing"}
        conn.execute.return_value.fetchone.return_value = row
        item_id, fp = protect_high_confidence(conn, "lessons", "fp123", "L-abc", "2026-01-01")
        assert item_id != "L-abc"
        assert item_id.startswith("L-v2-"), f"Expected L-v2- prefix, got {item_id}"
        assert fp.endswith("-v2"), "Fingerprint should have -v2 suffix"

    def test_exactly_at_threshold_unchanged(self):
        """Item with confidence exactly 0.8 does NOT trigger protection."""
        from pulse.core.forgetting_protection import protect_high_confidence
        conn = MagicMock()
        row = {"confidence": 0.8, "id": "L-existing"}
        conn.execute.return_value.fetchone.return_value = row
        item_id, fp = protect_high_confidence(conn, "lessons", "fp123", "L-abc", "2026-01-01")
        assert item_id == "L-abc"
        assert fp == "fp123"

    def test_no_existing_item_unchanged(self):
        """When no existing item found, IDs are unchanged."""
        from pulse.core.forgetting_protection import protect_high_confidence
        conn = MagicMock()
        conn.execute.return_value.fetchone.return_value = None
        item_id, fp = protect_high_confidence(conn, "lessons", "fp123", "L-abc", "2026-01-01")
        assert item_id == "L-abc"
        assert fp == "fp123"

    def test_db_exception_fails_open(self):
        """Database error during check returns original IDs unchanged."""
        from pulse.core.forgetting_protection import protect_high_confidence
        conn = MagicMock()
        conn.execute.side_effect = Exception("DB error")
        item_id, fp = protect_high_confidence(conn, "lessons", "fp123", "L-abc", "2026-01-01")
        assert item_id == "L-abc"
        assert fp == "fp123"


# ---------------------------------------------------------------------------
# Item 123: Meta-learning knowledge type tracking (metamemory.py)
# ---------------------------------------------------------------------------

class TestItem123MetaLearning:

    def setup_method(self):
        from pulse.brain import metamemory
        metamemory._KNOWLEDGE_TYPE_SUCCESS.clear()

    def test_record_success_increments_both(self):
        """Recording a success increments hits and successes."""
        from pulse.brain.metacognition.metamemory import record_knowledge_type_outcome, _KNOWLEDGE_TYPE_SUCCESS
        record_knowledge_type_outcome("lesson", True)
        assert _KNOWLEDGE_TYPE_SUCCESS["lesson"]["hits"] == 1
        assert _KNOWLEDGE_TYPE_SUCCESS["lesson"]["successes"] == 1

    def test_record_failure_increments_only_hits(self):
        """Recording a failure increments hits but not successes."""
        from pulse.brain.metacognition.metamemory import record_knowledge_type_outcome, _KNOWLEDGE_TYPE_SUCCESS
        record_knowledge_type_outcome("failure", False)
        assert _KNOWLEDGE_TYPE_SUCCESS["failure"]["hits"] == 1
        assert _KNOWLEDGE_TYPE_SUCCESS["failure"]["successes"] == 0

    def test_priority_returns_neutral_below_threshold(self):
        """Types with fewer than 3 hits return 0.5 (not enough data)."""
        from pulse.brain.metacognition.metamemory import record_knowledge_type_outcome, get_knowledge_type_priority
        record_knowledge_type_outcome("pattern", True)
        record_knowledge_type_outcome("pattern", True)
        priority = get_knowledge_type_priority()
        assert priority.get("pattern", 0.5) == 0.5

    def test_priority_computed_after_threshold(self):
        """Types with 3+ hits return computed success rate."""
        from pulse.brain.metacognition.metamemory import record_knowledge_type_outcome, get_knowledge_type_priority
        for _ in range(3):
            record_knowledge_type_outcome("fact", True)
        priority = get_knowledge_type_priority()
        assert priority.get("fact") == pytest.approx(1.0, abs=0.01)

    def test_multiple_types_tracked_independently(self):
        """Different knowledge types tracked independently."""
        from pulse.brain.metacognition.metamemory import record_knowledge_type_outcome, _KNOWLEDGE_TYPE_SUCCESS
        record_knowledge_type_outcome("lesson", True)
        record_knowledge_type_outcome("failure", False)
        assert _KNOWLEDGE_TYPE_SUCCESS["lesson"]["successes"] == 1
        assert _KNOWLEDGE_TYPE_SUCCESS["failure"]["successes"] == 0


# ---------------------------------------------------------------------------
# Item 124: Surprise-based attention capture (search_scoring_ext.py)
# ---------------------------------------------------------------------------

class TestItem124SurpriseAttention:

    def test_outlier_item_gets_bonus(self):
        """Item 2+ std deviations from domain mean gets confidence bonus."""
        from pulse.brain.brain_search.search_scoring_ext import apply_surprise_attention
        # Need 6 items so outlier (0.99) achieves z >= 2.0
        results = {
            "wins": [
                {"domain": "python", "confidence": 0.5, "id": "a"},
                {"domain": "python", "confidence": 0.5, "id": "b"},
                {"domain": "python", "confidence": 0.5, "id": "c"},
                {"domain": "python", "confidence": 0.5, "id": "e"},
                {"domain": "python", "confidence": 0.5, "id": "f"},
                {"domain": "python", "confidence": 0.99, "id": "d"},  # outlier (z~2.04)
            ]
        }
        apply_surprise_attention(results)
        items_by_id = {h["id"]: h for h in results["wins"]}
        assert "_surprise_bonus" in items_by_id["d"], "Outlier should get surprise bonus"
        assert items_by_id["d"]["confidence"] > 0.99, "Bonus should increase confidence"

    def test_no_variance_no_bonus(self):
        """Identical confidence across domain = no bonus."""
        from pulse.brain.brain_search.search_scoring_ext import apply_surprise_attention
        results = {
            "wins": [
                {"domain": "python", "confidence": 0.5, "id": "a"},
                {"domain": "python", "confidence": 0.5, "id": "b"},
                {"domain": "python", "confidence": 0.5, "id": "c"},
            ]
        }
        original_confs = [h["confidence"] for h in results["wins"]]
        apply_surprise_attention(results)
        new_confs = [h["confidence"] for h in results["wins"]]
        assert original_confs == new_confs

    def test_single_item_no_bonus(self):
        """Single item per domain = no stats = no bonus."""
        from pulse.brain.brain_search.search_scoring_ext import apply_surprise_attention
        results = {"wins": [{"domain": "solo_dom", "confidence": 0.9, "id": "x"}]}
        apply_surprise_attention(results)
        assert "_surprise_bonus" not in results["wins"][0]

    def test_fails_open_on_bad_input(self):
        """Corrupt input does not raise."""
        from pulse.brain.brain_search.search_scoring_ext import apply_surprise_attention
        apply_surprise_attention({"bad": "not_a_list"})
        apply_surprise_attention({})


# ---------------------------------------------------------------------------
# Item 125: Memory reconsolidation window tracking (reconsolidation.py)
# ---------------------------------------------------------------------------

class TestItem125ReconsolidationWindow:

    def setup_method(self):
        from pulse.brain import reconsolidation
        reconsolidation._labile_state = {}

    def test_count_zero_when_empty(self):
        """Empty labile state returns count 0."""
        from pulse.brain.plasticity.reconsolidation import _get_labile_count
        assert _get_labile_count() == 0

    def test_count_reflects_items(self):
        """Count equals number of non-expired items."""
        from pulse.brain import reconsolidation
        from datetime import datetime, timezone, timedelta
        future = (datetime.now(timezone.utc) + timedelta(seconds=300)).isoformat()
        reconsolidation._labile_state = {
            "item1": {"table": "lessons", "expires_at": future},
            "item2": {"table": "patterns", "expires_at": future},
        }
        assert reconsolidation._get_labile_count() == 2

    def test_health_stable_below_threshold(self):
        """Health returns stable=True when count < 20."""
        from pulse.brain.plasticity.reconsolidation import get_reconsolidation_health
        h = get_reconsolidation_health()
        assert h["stable"] is True
        assert h["warning"] is None

    def test_health_warns_above_threshold(self):
        """Health warns when count > 20."""
        from pulse.brain import reconsolidation
        from datetime import datetime, timezone, timedelta
        future = (datetime.now(timezone.utc) + timedelta(seconds=300)).isoformat()
        reconsolidation._labile_state = {
            f"item{i}": {"table": "lessons", "expires_at": future}
            for i in range(25)
        }
        h = reconsolidation.get_reconsolidation_health()
        assert h["stable"] is False
        assert h["warning"] is not None
        assert "instability" in h["warning"].lower()

    def test_health_always_returns_dict(self):
        """Never raises, always returns dict with required keys."""
        from pulse.brain.plasticity.reconsolidation import get_reconsolidation_health
        result = get_reconsolidation_health()
        assert isinstance(result, dict)
        for key in ("labile_count", "stable", "threshold", "warning"):
            assert key in result, f"Missing key: {key}"
