"""
Integration tests for P34 neuroscience items 141-145.
Run: python -m pytest tests/test_neuro_p34.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 141: Inhibitory control -- LOW CONFIDENCE warning (briefing_ext.py)
# ---------------------------------------------------------------------------
class TestItem141LowConfidenceWarning:

    def test_warning_fires_below_threshold(self):
        from pulse.brain.brain_query.briefing_ext import check_low_confidence_warning
        hits = [{"text": "example", "confidence": 0.3}]
        result = check_low_confidence_warning(0.25, hits)
        assert len(result) == 1
        assert "LOW CONFIDENCE" in result[0]

    def test_warning_fires_at_exact_threshold(self):
        from pulse.brain.brain_query.briefing_ext import check_low_confidence_warning
        hits = [{"text": "x"}]
        result = check_low_confidence_warning(0.39, hits)
        assert len(result) == 1

    def test_no_warning_above_threshold(self):
        from pulse.brain.brain_query.briefing_ext import check_low_confidence_warning
        hits = [{"text": "x"}]
        result = check_low_confidence_warning(0.6, hits)
        assert result == []

    def test_no_warning_when_no_hits(self):
        from pulse.brain.brain_query.briefing_ext import check_low_confidence_warning
        result = check_low_confidence_warning(0.1, [])
        assert result == []

    def test_warning_message_contains_verify_suggestion(self):
        from pulse.brain.brain_query.briefing_ext import check_low_confidence_warning
        hits = [{"text": "x"}]
        result = check_low_confidence_warning(0.2, hits)
        assert any("verif" in r.lower() for r in result)

    def test_wired_into_briefing_format(self):
        """format_briefing emits LOW CONFIDENCE when avg_conf < 0.4."""
        from pulse.brain.brain_query.briefing import format_briefing
        # Items must exceed ignition threshold (default 0.4) to appear in all_hits_flat
        # Use conf=0.45 so they pass gate but avg is still < 0.4 check in check_low_confidence_warning
        results = {
            "total_results": 3,
            "elapsed_ms": 10,
            "results": {
                "wins": [
                    {"text": "low conf item", "confidence": 0.41, "domain": "test"},
                    {"text": "another item", "confidence": 0.42, "domain": "test"},
                    {"text": "third item", "confidence": 0.43, "domain": "test"},
                ]
            }
        }
        # avg_conf = ~0.42 but check_low_confidence_warning fires at < 0.4; force low avg via mock
        with patch("pulse.brain.brain_query.briefing.check_low_confidence_warning",
                   return_value=["!! LOW CONFIDENCE: Brain avg confidence 0.35 — Consider verifying before acting"]):
            with patch("pulse.brain.brain_query.briefing._check_somatic_veto", return_value=[]):
                output = format_briefing("some query", results)
        assert "LOW CONFIDENCE" in output


# ---------------------------------------------------------------------------
# Item 142: Memory trace decay monitoring (metamemory_p34.py)
# ---------------------------------------------------------------------------
class TestItem142DecayMonitoring:

    def test_returns_required_keys(self):
        from pulse.brain.metacognition.metamemory_p34 import get_decay_rate_24h
        mock_conn = MagicMock()
        mock_conn.execute.return_value.fetchone.return_value = [100]
        with patch("pulse.core.brain_db.get_conn", return_value=mock_conn):
            result = get_decay_rate_24h()
        assert "decay_count" in result
        assert "total_valid" in result
        assert "decay_rate_pct" in result
        assert "warning" in result

    def test_warning_true_when_high_decay(self):
        from pulse.brain.metacognition.metamemory_p34 import get_decay_rate_24h
        mock_conn = MagicMock()
        call_n = [0]

        def se(sql, *args):
            m = MagicMock()
            call_n[0] += 1
            if call_n[0] % 2 == 1:
                m.fetchone.return_value = [100]   # total valid
            else:
                m.fetchone.return_value = [20]    # decayed (20% > 5% threshold)
            return m

        mock_conn.execute.side_effect = se
        with patch("pulse.core.brain_db.get_conn", return_value=mock_conn):
            result = get_decay_rate_24h()
        assert result["warning"] is True

    def test_warning_false_when_low_decay(self):
        from pulse.brain.metacognition.metamemory_p34 import get_decay_rate_24h
        mock_conn = MagicMock()
        call_n = [0]

        def se(sql, *args):
            m = MagicMock()
            call_n[0] += 1
            if call_n[0] % 2 == 1:
                m.fetchone.return_value = [1000]  # total
            else:
                m.fetchone.return_value = [1]     # 0.1% decayed
            return m

        mock_conn.execute.side_effect = se
        with patch("pulse.core.brain_db.get_conn", return_value=mock_conn):
            result = get_decay_rate_24h()
        assert result["warning"] is False

    def test_exception_returns_zeros(self):
        from pulse.brain.metacognition.metamemory_p34 import get_decay_rate_24h
        with patch("pulse.core.brain_db.get_conn", side_effect=Exception("boom")):
            result = get_decay_rate_24h()
        assert result["decay_count"] == 0
        assert result["warning"] is False

    def test_zero_total_avoids_division(self):
        from pulse.brain.metacognition.metamemory_p34 import get_decay_rate_24h
        mock_conn = MagicMock()
        mock_conn.execute.return_value.fetchone.return_value = [0]
        with patch("pulse.core.brain_db.get_conn", return_value=mock_conn):
            result = get_decay_rate_24h()
        assert result["decay_rate_pct"] == 0.0


# ---------------------------------------------------------------------------
# Item 143: Associative strength weighting (search_scoring_p34.py)
# ---------------------------------------------------------------------------
class TestItem143AssociativeStrength:

    def test_bonus_applied_to_neighbour_item(self):
        from pulse.brain.brain_search.search_scoring_p34 import apply_associative_strength
        results = {
            "wins": [
                {"id": "top", "confidence": 0.8, "node_id": "top"},
                {"id": "neighbour", "confidence": 0.5, "node_id": "n1"},
                {"id": "unrelated", "confidence": 0.5, "node_id": "u1"},
            ]
        }
        mock_conn = MagicMock()

        def exe(sql, params):
            if "from_node" in sql:
                return MagicMock(fetchall=MagicMock(return_value=[("n1",)]))
            return MagicMock(fetchall=MagicMock(return_value=[]))

        mock_conn.execute.side_effect = exe
        with patch("pulse.core.brain_db.get_conn", return_value=mock_conn):
            apply_associative_strength(results)
        neighbour = next(h for h in results["wins"] if h["id"] == "neighbour")
        assert neighbour.get("_assoc_strength_bonus") is True
        assert neighbour["confidence"] > 0.5

    def test_non_neighbour_not_boosted(self):
        from pulse.brain.brain_search.search_scoring_p34 import apply_associative_strength
        results = {
            "wins": [
                {"id": "top", "confidence": 0.8, "node_id": "top"},
                {"id": "unrelated", "confidence": 0.5, "node_id": "u99"},
            ]
        }
        mock_conn = MagicMock()
        mock_conn.execute.return_value.fetchall.return_value = []
        with patch("pulse.core.brain_db.get_conn", return_value=mock_conn):
            apply_associative_strength(results)
        unrelated = next(h for h in results["wins"] if h["id"] == "unrelated")
        assert not unrelated.get("_assoc_strength_bonus")
        assert unrelated["confidence"] == 0.5

    def test_db_exception_fails_open(self):
        from pulse.brain.brain_search.search_scoring_p34 import apply_associative_strength
        results = {"wins": [{"id": "top", "confidence": 0.9, "node_id": "top"}]}
        with patch("pulse.core.brain_db.get_conn", side_effect=Exception("db down")):
            apply_associative_strength(results)  # must not raise
        assert results["wins"][0]["confidence"] == 0.9

    def test_empty_results_no_error(self):
        from pulse.brain.brain_search.search_scoring_p34 import apply_associative_strength
        apply_associative_strength({})  # no exception

    def test_confidence_capped_at_1_0(self):
        from pulse.brain.brain_search.search_scoring_p34 import apply_associative_strength
        results = {
            "wins": [
                {"id": "top", "confidence": 0.99, "node_id": "top"},
                {"id": "n1", "confidence": 0.99, "node_id": "n1"},
            ]
        }
        mock_conn = MagicMock()

        def exe(sql, params):
            if "from_node" in sql:
                return MagicMock(fetchall=MagicMock(return_value=[("n1",)]))
            return MagicMock(fetchall=MagicMock(return_value=[]))

        mock_conn.execute.side_effect = exe
        with patch("pulse.core.brain_db.get_conn", return_value=mock_conn):
            apply_associative_strength(results)
        n1 = next(h for h in results["wins"] if h["id"] == "n1")
        assert n1["confidence"] <= 1.0


# ---------------------------------------------------------------------------
# Item 144: Task complexity estimation (task_complexity.py)
# ---------------------------------------------------------------------------
class TestItem144TaskComplexity:

    def test_returns_required_keys(self):
        from pulse.tasks.task_complexity import estimate_task_complexity
        mock_conn = MagicMock()
        mock_conn.execute.return_value.fetchone.return_value = [0]
        with patch("pulse.core.brain_db.get_conn", return_value=mock_conn):
            result = estimate_task_complexity({"domain": "testing"})
        for k in ("complexity_score", "antipattern_count", "failure_count", "suggested_tier"):
            assert k in result, f"Missing key: {k}"

    def test_high_complexity_upgrades_to_premium(self):
        from pulse.tasks.task_complexity import estimate_task_complexity
        mock_conn = MagicMock()
        mock_conn.execute.return_value.fetchone.return_value = [10]
        with patch("pulse.core.brain_db.get_conn", return_value=mock_conn):
            task = {"domain": "security", "recommended_tier": "fast"}
            result = estimate_task_complexity(task)
        assert result["suggested_tier"] == "premium"

    def test_low_complexity_keeps_tier(self):
        from pulse.tasks.task_complexity import estimate_task_complexity
        mock_conn = MagicMock()
        mock_conn.execute.return_value.fetchone.return_value = [0]
        with patch("pulse.core.brain_db.get_conn", return_value=mock_conn):
            task = {"domain": "simple", "recommended_tier": "fast"}
            result = estimate_task_complexity(task)
        assert result["complexity_score"] == 0
        assert result["suggested_tier"] == "fast"

    def test_exception_fails_open(self):
        from pulse.tasks.task_complexity import estimate_task_complexity
        with patch("pulse.core.brain_db.get_conn", side_effect=Exception("db")):
            result = estimate_task_complexity({"domain": "testing"})
        assert result["complexity_score"] == 0
        assert result["suggested_tier"] is None

    def test_apply_complexity_routing_mutates_tasks(self):
        from pulse.tasks.task_complexity import apply_complexity_routing
        mock_conn = MagicMock()
        mock_conn.execute.return_value.fetchone.return_value = [10]
        tasks = [{"domain": "security", "recommended_tier": "fast"}]
        with patch("pulse.core.brain_db.get_conn", return_value=mock_conn):
            apply_complexity_routing(tasks)
        assert "_complexity" in tasks[0]

    def test_rejected_tasks_skipped(self):
        from pulse.tasks.task_complexity import apply_complexity_routing
        tasks = [{"status": "rejected", "domain": "x", "recommended_tier": "fast"}]
        apply_complexity_routing(tasks)
        assert "_complexity" not in tasks[0]


# ---------------------------------------------------------------------------
# Item 145: Knowledge freshness indicator (search_scoring_p34.py)
# ---------------------------------------------------------------------------
class TestItem145FreshnessIndicator:

    def test_fresh_item(self):
        from pulse.brain.brain_search.search_scoring_p34 import _compute_freshness
        from datetime import datetime, timezone, timedelta
        recent = (datetime.now(timezone.utc) - timedelta(hours=6)).isoformat()
        assert _compute_freshness(recent) == "fresh"

    def test_recent_item(self):
        from pulse.brain.brain_search.search_scoring_p34 import _compute_freshness
        from datetime import datetime, timezone, timedelta
        dt = (datetime.now(timezone.utc) - timedelta(days=3)).isoformat()
        assert _compute_freshness(dt) == "recent"

    def test_aging_item(self):
        from pulse.brain.brain_search.search_scoring_p34 import _compute_freshness
        from datetime import datetime, timezone, timedelta
        dt = (datetime.now(timezone.utc) - timedelta(days=15)).isoformat()
        assert _compute_freshness(dt) == "aging"

    def test_stale_item(self):
        from pulse.brain.brain_search.search_scoring_p34 import _compute_freshness
        from datetime import datetime, timezone, timedelta
        dt = (datetime.now(timezone.utc) - timedelta(days=60)).isoformat()
        assert _compute_freshness(dt) == "stale"

    def test_missing_timestamp_is_stale(self):
        from pulse.brain.brain_search.search_scoring_p34 import _compute_freshness
        assert _compute_freshness(None) == "stale"
        assert _compute_freshness("") == "stale"

    def test_annotate_freshness_sets_metadata(self):
        from pulse.brain.brain_search.search_scoring_p34 import annotate_freshness
        from datetime import datetime, timezone
        results = {
            "wins": [
                {"text": "a", "timestamp": datetime.now(timezone.utc).isoformat()},
                {"text": "b"},
            ]
        }
        annotate_freshness(results)
        assert results["wins"][0]["_freshness"] == "fresh"
        assert results["wins"][1]["_freshness"] == "stale"
