#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
"""Integration tests for P33 neuroscience refinements (items 136-140).

Tests use mocks to avoid DB/filesystem dependencies.
Run: python -m pytest tests/test_neuro_p33.py -v
"""
import sys
import types
from unittest.mock import MagicMock, patch

# ---------------------------------------------------------------------------
# Stubs
# ---------------------------------------------------------------------------
def _ensure_stub(name, attrs=None):
    if name not in sys.modules:
        mod = types.ModuleType(name)
        if attrs:
            for k, v in attrs.items():
                setattr(mod, k, v)
        sys.modules[name] = mod
    return sys.modules[name]

_ensure_stub("pulse")
_ensure_stub("pulse.core")
_ensure_stub("pulse.brain")
_ensure_stub("pulse.brain.brain_search")
_ensure_stub("pulse.brain.save_writers")

_bu = _ensure_stub("pulse.core.brain_utils", {
    "load_config": lambda: {},
    "text_similarity": lambda a, b: (
        1.0 if a == b else (
            0.0 if not a or not b else
            len(set(a.lower().split()) & set(b.lower().split())) /
            max(len(set(a.lower().split())), len(set(b.lower().split())), 1)
        )
    ),
})
_ensure_stub("pulse.brain.integrity.provenance", {"get_provenance": lambda fp: {}})

_mock_conn = MagicMock()
_mock_conn.execute.return_value.fetchone.return_value = [100, 30]  # total=100, fails=30 -> 30% failure rate
_ensure_stub("pulse.core.brain_db", {"get_conn": lambda: _mock_conn})
_ensure_stub("pulse.brain.metacognition.metamemory", {
    "get_metamemory": lambda: {},
    "_get_calibration_factor": lambda: 1.0,
})
_ensure_stub("pulse.core.brain_io", {"_acquire": MagicMock(return_value=MagicMock(
    __enter__=lambda s, *a: s, __exit__=lambda s, *a: False))})
_ensure_stub("pulse.core.paths", {"get_state_dir": lambda: __import__("pathlib").Path("/tmp/pulse_test")})

# ---------------------------------------------------------------------------
# Module loaders
# ---------------------------------------------------------------------------
import importlib.util
import os

_BASE = os.path.join(os.path.dirname(__file__), "..")


def _load(rel_path, name):
    spec = importlib.util.spec_from_file_location(
        name, os.path.normpath(os.path.join(_BASE, rel_path))
    )
    mod = importlib.util.module_from_spec(spec)
    sys.modules[name] = mod
    spec.loader.exec_module(mod)
    return mod


def _load_reward_momentum():
    return _load("pulse/core/reward/reward_momentum.py", "pulse.core.reward.reward_momentum")


def _load_temporal_bridge():
    return _load("pulse/brain/save_writers/temporal_bridge.py",
                 "pulse.brain.save_writers.temporal_bridge")


def _load_tracking():
    return _load("pulse/brain/metacognition/metamemory_tracking.py", "pulse.brain.metacognition.metamemory_tracking")


def _load_p32():
    return _load(
        "pulse/brain/brain_search/search_scoring_p32.py",
        "pulse.brain.brain_search.search_scoring_p32",
    )


def _load_stdp_annealing():
    return _load("pulse/brain/stdp_annealing.py", "pulse.brain.stdp_annealing")


# ===========================================================================
# Item 136: Reward momentum
# ===========================================================================
class TestRewardMomentum:
    def setup_method(self):
        self.mod = _load_reward_momentum()
        # Reset all streaks
        self.mod._CONSECUTIVE_SUCCESSES.clear()

    def test_first_success_no_bonus(self):
        """First success in a domain gives zero bonus (no streak yet)."""
        bonus = self.mod.record_success_momentum("auth", True)
        assert bonus == 0.0

    def test_second_success_gives_bonus(self):
        """Second consecutive success in domain gives +0.05."""
        self.mod.record_success_momentum("auth", True)
        bonus = self.mod.record_success_momentum("auth", True)
        assert abs(bonus - 0.05) < 0.001

    def test_failure_resets_streak(self):
        """Failure resets streak and returns 0.0."""
        self.mod.record_success_momentum("auth", True)
        self.mod.record_success_momentum("auth", True)
        bonus = self.mod.record_success_momentum("auth", False)
        assert bonus == 0.0
        assert self.mod.get_domain_streak("auth") == 0

    def test_streak_compounds(self):
        """3 consecutive successes give +0.10 bonus."""
        for _ in range(3):
            self.mod.record_success_momentum("deploy", True)
        # After 3rd success: streak=3, bonus=(3-1)*0.05=0.10
        bonus = self.mod.record_success_momentum("deploy", True)
        assert abs(bonus - 0.15) < 0.001  # 4th success: (4-1)*0.05=0.15

    def test_bonus_capped(self):
        """Momentum bonus is capped at 0.30."""
        for _ in range(20):
            self.mod.record_success_momentum("domx", True)
        bonus = self.mod.record_success_momentum("domx", True)
        assert bonus <= 0.30

    def test_independent_domains(self):
        """Streaks are per-domain and independent."""
        self.mod.record_success_momentum("auth", True)
        self.mod.record_success_momentum("auth", True)
        # deploy has no streak
        bonus_deploy = self.mod.record_success_momentum("deploy", True)
        assert bonus_deploy == 0.0
        # auth has streak=2, third success gives (2)*0.05=0.10
        bonus_auth = self.mod.record_success_momentum("auth", True)
        assert abs(bonus_auth - 0.10) < 0.001

    def test_get_domain_streak(self):
        """get_domain_streak returns current streak count."""
        for _ in range(4):
            self.mod.record_success_momentum("testing", True)
        assert self.mod.get_domain_streak("testing") == 4

    def test_reset_domain_streak(self):
        """reset_domain_streak clears the streak."""
        for _ in range(5):
            self.mod.record_success_momentum("db", True)
        self.mod.reset_domain_streak("db")
        assert self.mod.get_domain_streak("db") == 0


# ===========================================================================
# Item 137: Temporal bridging
# ===========================================================================
class TestTemporalBridging:
    def setup_method(self):
        self.mod = _load_temporal_bridge()

    def test_no_timestamp_skip(self):
        """Entry without timestamp does not crash."""
        self.mod.maybe_create_temporal_bridge({"content": "test", "id": "x1"})

    def test_no_content_skip(self):
        """Entry without content does not crash."""
        from datetime import datetime, timezone
        entry = {"id": "x2", "timestamp": datetime.now(timezone.utc).isoformat()}
        self.mod.maybe_create_temporal_bridge(entry)

    def test_creates_bridge_when_similar_old_item_found(self):
        """When DB has a similar item from >24h ago, TEMPORAL_BRIDGE edge is created."""
        from datetime import datetime, timezone, timedelta
        # Set up mock DB to return a candidate item
        now = datetime.now(timezone.utc)
        old_ts = (now - timedelta(hours=48)).isoformat()
        entry_ts = now.isoformat()

        _mock_conn.execute.return_value.fetchall.return_value = [
            ("old-id-1", "database connection pool timeout error", "title")
        ]

        entry = {
            "id": "new-id-1",
            "content": "database connection pool timeout error",
            "timestamp": entry_ts,
            "domain": "backend",
        }
        # Should not raise
        self.mod.maybe_create_temporal_bridge(entry)

    def test_fail_open_on_db_error(self):
        """Failure in DB call does not propagate."""
        _mock_conn.execute.side_effect = Exception("DB down")
        entry = {
            "id": "x3",
            "content": "some content here",
            "timestamp": "2026-03-21T10:00:00+00:00",
        }
        # Should not raise
        self.mod.maybe_create_temporal_bridge(entry)
        # Restore mock
        _mock_conn.execute.side_effect = None
        _mock_conn.execute.return_value.fetchone.return_value = [100, 30]

    def test_fail_open_on_missing_id(self):
        """Entry without 'id' is silently skipped."""
        self.mod.maybe_create_temporal_bridge({
            "content": "some content",
            "timestamp": "2026-03-21T10:00:00+00:00",
        })

    def test_similarity_threshold_blocks_dissimilar(self):
        """Dissimilar items (sim < 0.5) do not create a bridge edge."""
        _mock_conn.execute.return_value.fetchall.return_value = [
            ("old-id-2", "completely unrelated text about something else entirely", "t")
        ]
        _mock_conn.execute.return_value.fetchone.return_value = [100, 30]
        entry = {
            "id": "new-id-2",
            "content": "quantum computing photon entanglement",
            "timestamp": "2026-03-21T14:00:00+00:00",
        }
        # Should not crash
        self.mod.maybe_create_temporal_bridge(entry)


# ===========================================================================
# Item 138: Cognitive flexibility
# ===========================================================================
class TestCognitiveFlexibility:
    def setup_method(self):
        self.mod = _load_tracking()
        self.mod._AGENT_SESSION_DOMAINS.clear()

    def test_single_domain_tunnel_vision(self):
        """Agent working in only 1 domain gets tunnel_vision_warning=True."""
        self.mod.record_agent_session_domain("claude", "sess1", "backend")
        result = self.mod.get_cognitive_flexibility_index("claude", "sess1")
        assert result["tunnel_vision_warning"] is True
        assert result["unique_domains"] == 1

    def test_multiple_domains_high_flexibility(self):
        """Agent working in 5 domains gets flexibility_score=1.0."""
        for d in ["backend", "frontend", "security", "testing", "deployment"]:
            self.mod.record_agent_session_domain("gemini", "sess2", d)
        result = self.mod.get_cognitive_flexibility_index("gemini", "sess2")
        assert result["flexibility_score"] == 1.0
        assert result["tunnel_vision_warning"] is False

    def test_domains_deduplicated(self):
        """Same domain recorded multiple times counts as 1."""
        for _ in range(10):
            self.mod.record_agent_session_domain("codex", "sess3", "backend")
        result = self.mod.get_cognitive_flexibility_index("codex", "sess3")
        assert result["unique_domains"] == 1

    def test_unknown_agent_defaults(self):
        """Unknown agent/session returns safe defaults."""
        result = self.mod.get_cognitive_flexibility_index("unknown_agent", "unknown_sess")
        assert result["unique_domains"] == 0
        assert result["flexibility_score"] == 0.0
        assert result["tunnel_vision_warning"] is True

    def test_flexibility_score_normalised(self):
        """3 domains gives flexibility_score = 3/5 = 0.6."""
        for d in ["auth", "cache", "metrics"]:
            self.mod.record_agent_session_domain("sonnet", "sess4", d)
        result = self.mod.get_cognitive_flexibility_index("sonnet", "sess4")
        assert abs(result["flexibility_score"] - 0.6) < 0.001

    def test_domains_list_in_result(self):
        """Result includes list of domain names."""
        self.mod.record_agent_session_domain("haiku", "sess5", "api")
        self.mod.record_agent_session_domain("haiku", "sess5", "db")
        result = self.mod.get_cognitive_flexibility_index("haiku", "sess5")
        assert "api" in result["domains"]
        assert "db" in result["domains"]


# ===========================================================================
# Item 139: Error correction signal strength
# ===========================================================================
class TestErrorCorrectionSignal:
    def setup_method(self):
        self.mod = _load_stdp_annealing()

    def test_low_failure_rate_strong_ltd(self):
        """Domain with low failure rate -> high surprise -> strong LTD."""
        # Mock: 100 total, 5 failures = 5% failure rate -> surprise=0.95
        conn = MagicMock()
        conn.execute.return_value.fetchone.return_value = [100, 5]
        with patch("pulse.core.brain_db.get_conn", return_value=conn):
            delta = self.mod.compute_failure_ltd(-0.04)
        # surprise = 1 - 0.05 = 0.95; delta = -0.04 * 0.95 = -0.038
        assert delta < 0  # still negative (LTD)
        assert abs(delta) > abs(-0.04 * 0.3)  # stronger than low-surprise case

    def test_high_failure_rate_mild_ltd(self):
        """Domain with high failure rate -> low surprise -> milder LTD."""
        conn = MagicMock()
        conn.execute.return_value.fetchone.return_value = [100, 80]  # 80% failure rate
        with patch("pulse.core.brain_db.get_conn", return_value=conn):
            delta_high = self.mod.compute_failure_ltd(-0.04)

        conn2 = MagicMock()
        conn2.execute.return_value.fetchone.return_value = [100, 5]  # 5% failure rate
        with patch("pulse.core.brain_db.get_conn", return_value=conn2):
            delta_low = self.mod.compute_failure_ltd(-0.04)

        # High failure rate -> more expected -> milder LTD (less negative)
        assert abs(delta_high) < abs(delta_low)

    def test_return_is_negative(self):
        """compute_failure_ltd always returns negative value (LTD direction)."""
        conn = MagicMock()
        conn.execute.return_value.fetchone.return_value = [10, 2]
        with patch("pulse.core.brain_db.get_conn", return_value=conn):
            delta = self.mod.compute_failure_ltd(-0.04)
        assert delta < 0

    def test_fail_open_on_db_error(self):
        """DB error returns base_delta unchanged (fail-open)."""
        conn = MagicMock()
        conn.execute.side_effect = Exception("DB down")
        with patch("pulse.core.brain_db.get_conn", return_value=conn):
            delta = self.mod.compute_failure_ltd(-0.04)
        assert abs(delta - (-0.04)) < 0.001

    def test_surprise_floor(self):
        """Surprise is floored at 0.1 (even 100% failure rate)."""
        conn = MagicMock()
        conn.execute.return_value.fetchone.return_value = [100, 100]  # 100% failure rate
        with patch("pulse.core.brain_db.get_conn", return_value=conn):
            delta = self.mod.compute_failure_ltd(-0.04)
        # surprise = max(0.1, 1-1.0) = 0.1; delta = -0.04 * 0.1 = -0.004
        assert abs(delta - (-0.004)) < 0.0001


# ===========================================================================
# Item 140: Search result provenance chain
# ===========================================================================
class TestProvenanceChain:
    def setup_method(self):
        self.mod = _load_p32()

    def test_chain_populated_from_item_fields(self):
        """Provenance chain includes agent, timestamp, domain, type from item."""
        items = [{
            "id": "item-1",
            "agent": "claude",
            "timestamp": "2026-03-21T10:00:00Z",
            "domain": "backend",
            "type": "lesson",
            "confidence": 0.8,
        }]
        results = {"wins": items}
        self.mod.apply_provenance_chain(results)
        chain = items[0].get("_provenance_chain", {})
        assert chain["agent"] == "claude"
        assert chain["source"] == "wins"
        assert chain["domain"] == "backend"
        assert chain["type"] == "lesson"
        assert chain["timestamp"] == "2026-03-21T10:00:00Z"

    def test_chain_enriched_from_existing_provenance(self):
        """Existing _provenance dict is merged into the chain."""
        items = [{
            "id": "item-2",
            "_provenance": {"task_id": "task-42", "saving_agent": "gemini", "session_id": "sess-1"},
            "confidence": 0.7,
        }]
        self.mod.apply_provenance_chain({"fails": items})
        chain = items[0]["_provenance_chain"]
        assert chain["task_id"] == "task-42"
        assert chain["saving_agent"] == "gemini"
        assert chain["session_id"] == "sess-1"

    def test_chain_present_on_all_items(self):
        """Every item in every source gets _provenance_chain."""
        results = {
            "wins": [{"confidence": 0.5}, {"confidence": 0.6}],
            "fails": [{"confidence": 0.4}],
        }
        self.mod.apply_provenance_chain(results)
        for hits in results.values():
            for h in hits:
                assert "_provenance_chain" in h

    def test_chain_source_name_correct(self):
        """chain['source'] matches the dict key (source name)."""
        results = {"anti_patterns": [{"confidence": 0.9}]}
        self.mod.apply_provenance_chain(results)
        assert results["anti_patterns"][0]["_provenance_chain"]["source"] == "anti_patterns"

    def test_fail_open_on_non_list(self):
        """Non-list source values don't cause errors."""
        self.mod.apply_provenance_chain({"bad_source": "not_a_list"})
        self.mod.apply_provenance_chain({})

    def test_chain_item_id_matches(self):
        """chain['item_id'] matches item's 'id' field."""
        item = {"id": "L-abc123", "confidence": 0.7}
        results = {"wins": [item]}
        self.mod.apply_provenance_chain(results)
        assert item["_provenance_chain"]["item_id"] == "L-abc123"


if __name__ == "__main__":
    import pytest
    sys.exit(pytest.main([__file__, "-v"]))
