"""P45 Neuroscience Fixes — tests for N51–N55."""
from pathlib import Path
import sys
import time

sys.path.insert(0, str(Path(__file__).resolve().parent.parent))

import pytest


# ── N51: Perseveration detector ───────────────────────────────────────────────

class TestPerseverationDetector:
    def setup_method(self):
        """Reset the global violation dict before each test."""
        import pulse.daemons.neural.gate_feedback as gf
        gf._violation_counts.clear()

    def test_three_violations_detected(self):
        """3 violations for same agent+pattern in 1 hour → perseveration detected."""
        from pulse.daemons.neural.gate_feedback import (
            record_perseveration_violation, check_perseveration,
        )
        for _ in range(3):
            record_perseveration_violation("agent-A", "pat-1")
        assert check_perseveration("agent-A", "pat-1") is True

    def test_two_violations_not_detected(self):
        """2 violations → below threshold → not perseveration."""
        from pulse.daemons.neural.gate_feedback import (
            record_perseveration_violation, check_perseveration,
        )
        record_perseveration_violation("agent-B", "pat-2")
        record_perseveration_violation("agent-B", "pat-2")
        assert check_perseveration("agent-B", "pat-2") is False

    def test_different_pattern_not_affected(self):
        """Violations for pat-1 don't bleed into pat-2 check."""
        from pulse.daemons.neural.gate_feedback import (
            record_perseveration_violation, check_perseveration,
        )
        for _ in range(3):
            record_perseveration_violation("agent-C", "pat-X")
        assert check_perseveration("agent-C", "pat-Y") is False

    def test_reset_clears_violations(self):
        """reset_perseveration clears the count so perseveration is no longer detected."""
        from pulse.daemons.neural.gate_feedback import (
            record_perseveration_violation, check_perseveration, reset_perseveration,
        )
        for _ in range(3):
            record_perseveration_violation("agent-D", "pat-R")
        assert check_perseveration("agent-D", "pat-R") is True
        reset_perseveration("agent-D", "pat-R")
        assert check_perseveration("agent-D", "pat-R") is False

    def test_expired_violations_not_counted(self):
        """Violations older than 24h are pruned and not counted."""
        import pulse.daemons.neural.gate_feedback as gf

        agent_id = "agent-E"
        pattern_id = "pat-old"
        past = time.time() - 25 * 3600  # 25 hours ago — outside window
        gf._violation_counts[agent_id] = {pattern_id: [past, past, past]}
        # No new violations recorded — all should be pruned
        assert gf.check_perseveration(agent_id, pattern_id) is False


# ── N52: Quality-based selection in approval_gate ─────────────────────────────

class TestApprovalGateQualitySelection:
    def test_high_quality_item_at_position_8_approved(self):
        """Item at position 8 with highest quality should be auto-approved."""
        from pulse.daemons.consolidation.approval_gate import check_approval

        # 10 items; only position-8 item has high salience*confidence
        items = [{"salience": 0.1, "confidence": 0.1} for _ in range(10)]
        items[8] = {"salience": 5.0, "confidence": 0.9, "label": "best"}

        # threshold for "reconsolidation" is 10 — all auto-approved in that stage
        # Use "schema_promotion" (threshold=3) to force a split
        approved, blocked = check_approval("schema_promotion", items)

        approved_labels = [i.get("label") for i in approved]
        assert "best" in approved_labels, (
            f"Highest quality item should be in approved; got {approved_labels}"
        )

    def test_position_ordering_irrelevant(self):
        """The first 3 positional items are NOT necessarily the approved set."""
        from pulse.daemons.consolidation.approval_gate import check_approval

        # All items look the same except item at index 9 which has perfect quality
        items = [{"salience": 0.0, "confidence": 0.0, "idx": i} for i in range(10)]
        items[9] = {"salience": 1.0, "confidence": 1.0, "idx": 9}

        approved, blocked = check_approval("schema_promotion", items)  # threshold=3

        approved_idxs = [i.get("idx") for i in approved]
        assert 9 in approved_idxs, f"Best item (idx=9) should be approved, got {approved_idxs}"

    def test_threshold_not_exceeded_all_approved(self):
        """When item count <= threshold, all are approved regardless of quality."""
        from pulse.daemons.consolidation.approval_gate import check_approval

        items = [{"salience": 0.1, "confidence": 0.1} for _ in range(3)]
        approved, blocked = check_approval("schema_promotion", items)  # threshold=3
        assert len(approved) == 3
        assert len(blocked) == 0


# ── N53: Penalty stacking cap in search scoring ───────────────────────────────

class TestPenaltyStackingCap:
    def test_floor_code_present_in_score_item(self):
        """Verify the N53 floor line is present in search_scoring._score_item source."""
        import inspect
        from pulse.brain.brain_search import search_scoring
        src = inspect.getsource(search_scoring._score_item)
        assert 'max(h.get("confidence", 0), 0.15)' in src, (
            "N53 floor line not found in _score_item source"
        )

    def test_floor_value_from_zero(self):
        """max(0, 0.15) = 0.15 — the floor expression evaluated directly."""
        item = {"confidence": 0.0}
        item["confidence"] = max(item.get("confidence", 0), 0.15)
        assert item["confidence"] == 0.15

    def test_floor_from_compound_penalties(self):
        """Simulate 4 sequential confidence multiplications and verify floor holds."""
        # contradiction: *0.7, attention_fatigue: *0.8, IOR: *0.5, trust: *0.3
        conf = 0.5
        conf *= 0.7   # contradiction
        conf *= 0.8   # attention fatigue
        conf *= 0.5   # IOR
        conf *= 0.3   # trust penalty
        # Before floor: 0.5 * 0.7 * 0.8 * 0.5 * 0.3 = 0.042
        assert conf < 0.15, "Test setup: compound penalties should drop below 0.15"
        # Apply floor as in search_scoring.py
        conf = max(conf, 0.15)
        assert conf >= 0.15, f"Floor should prevent going below 0.15, got {conf}"

    def test_floor_does_not_raise_above_legitimate_confidence(self):
        """High-confidence item is not affected by the floor."""
        item = {"confidence": 0.8}
        item["confidence"] = max(item.get("confidence", 0), 0.15)
        assert item["confidence"] == 0.8


# ── N54: Resolution (gamma) metric in prediction_calibration ──────────────────

class TestComputeResolution:
    def setup_method(self):
        import pulse.brain.metacognition.prediction_calibration as pc
        pc._DOMAIN_CALIBRATION.clear()
        pc._RAW_OBSERVATIONS.clear()

    def test_clear_discrimination_high_resolution(self):
        """High-conf predictions succeed, low-conf fail → resolution > 0.5."""
        from pulse.brain.metacognition.prediction_calibration import record_domain_prediction, compute_resolution

        # 5 successes with high predicted confidence
        for _ in range(5):
            record_domain_prediction("test", 0.9, True)
        # 5 failures with low predicted confidence
        for _ in range(5):
            record_domain_prediction("test", 0.1, False)

        r = compute_resolution()
        assert r > 0.5, f"Expected resolution > 0.5 for clear discrimination, got {r}"

    def test_random_predictions_near_zero_resolution(self):
        """Random predictions (no discrimination) → resolution near 0."""
        from pulse.brain.metacognition.prediction_calibration import record_domain_prediction, compute_resolution
        import random
        random.seed(42)

        for i in range(20):
            # Alternate high/low confidence but random outcome — no systematic discrimination
            conf = 0.9 if i % 2 == 0 else 0.1
            outcome = random.choice([True, False])
            record_domain_prediction("test", conf, outcome)

        r = compute_resolution()
        # Near-random: resolution should be well below 0.5 in magnitude
        assert abs(r) < 0.7, f"Expected near-zero resolution for random, got {r}"

    def test_insufficient_data_returns_zero(self):
        """Fewer than 4 observations → returns 0.0."""
        from pulse.brain.metacognition.prediction_calibration import (
            record_domain_prediction, compute_resolution,
        )
        record_domain_prediction("test", 0.8, True)
        record_domain_prediction("test", 0.2, False)
        r = compute_resolution()
        assert r == 0.0

    def test_resolution_in_valid_range(self):
        """Resolution must always be in [-1, 1]."""
        from pulse.brain.metacognition.prediction_calibration import record_domain_prediction, compute_resolution
        for i in range(10):
            record_domain_prediction("test", float(i) / 9, i % 2 == 0)
        r = compute_resolution()
        assert -1.0 <= r <= 1.0


# ── N55: Cold-start calibration factor = 0.85 ─────────────────────────────────

class TestColdStartCalibration:
    def setup_method(self):
        """Clear in-memory calibration history before each test."""
        import pulse.brain.metacognition.metamemory as mm
        mm._CALIBRATION_HISTORY.clear()

    def teardown_method(self):
        """Restore clean state."""
        import pulse.brain.metacognition.metamemory as mm
        mm._CALIBRATION_HISTORY.clear()

    def test_cold_start_returns_0_85(self):
        """With no calibration history and empty DB, _get_calibration_factor returns 0.85."""
        import unittest.mock as mock
        import pulse.brain.metacognition.metamemory as mm
        from pulse.brain.metacognition.metamemory import _get_calibration_factor

        mm._CALIBRATION_HISTORY.clear()

        # Mock get_conn so SQLite is never touched (avoids DB init hang)
        mock_conn = mock.MagicMock()
        mock_conn.execute.return_value.fetchall.return_value = []
        # The function does `from pulse.core.brain_db import get_conn` inline,
        # so we patch at the source module level.
        with mock.patch("pulse.core.brain_db.get_conn", return_value=mock_conn):
            factor = _get_calibration_factor()

        assert factor == 0.85, f"Expected 0.85 cold-start factor, got {factor}"

    def test_cold_start_constant_in_source(self):
        """Verify the 0.85 constant is actually present in the source (not 1.0)."""
        import inspect
        from pulse.brain import metamemory
        src = inspect.getsource(metamemory._get_calibration_factor)
        assert "0.85" in src, "Cold-start constant 0.85 not found in _get_calibration_factor"
        # Ensure the old value 1.0 (as a cold-start return) is replaced
        # (1.0 may appear in other contexts like the clamp, so check the comment key)
        assert "Lichtenstein" in src, "Lichtenstein citation missing from cold-start comment"

    def test_non_cold_start_uses_computed_factor(self):
        """With sufficient in-memory history, returns computed factor bypassing SQLite."""
        import pulse.brain.metacognition.metamemory as mm
        from pulse.brain.metacognition.metamemory import _get_calibration_factor

        # Pre-populate: 10 balanced entries bypass the SQLite load path
        mm._CALIBRATION_HISTORY.extend([(0.8, 0.8)] * 10)

        # No need to mock DB — SQLite branch is skipped when len >= 10
        factor = _get_calibration_factor()
        # avg_actual / avg_predicted = 0.8 / 0.8 = 1.0
        assert abs(factor - 1.0) < 0.05, f"Expected ~1.0 for balanced history, got {factor}"

    def test_overconfident_history_gives_factor_below_1(self):
        """Overconfident history (high predicted, low actual) returns factor < 1."""
        import pulse.brain.metacognition.metamemory as mm
        from pulse.brain.metacognition.metamemory import _get_calibration_factor

        # predicted=0.9, actual=0.4 → factor = 0.4/0.9 ≈ 0.44, clamped to 0.5
        mm._CALIBRATION_HISTORY.extend([(0.9, 0.4)] * 10)
        factor = _get_calibration_factor()
        assert factor < 1.0, f"Overconfident history should give factor < 1.0, got {factor}"
        assert factor >= 0.5, "Factor should not go below 0.5 (clamped)"
