"""
Integration tests for P30 neuroscience items 126-130.
Run: python -m pytest tests/test_neuro_p30.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 126: Cortical column organization (search_scoring_ext.py)
# ---------------------------------------------------------------------------

class TestItem126CorticalColumns:

    def test_organizes_by_domain(self):
        """Results grouped by domain into cortical columns."""
        from pulse.brain.brain_search.search_scoring_ext import _organize_columns
        results = {
            "wins": [
                {"id": "a", "domain": "python", "confidence": 0.8, "_wilson_lower": 0.7},
                {"id": "b", "domain": "python", "confidence": 0.5, "_wilson_lower": 0.4},
                {"id": "c", "domain": "databases", "confidence": 0.6, "_wilson_lower": 0.5},
            ]
        }
        organized = _organize_columns(results)
        assert "columns" in organized
        assert "python" in organized["columns"]
        assert "databases" in organized["columns"]
        assert organized["columns"]["python"]["column_size"] == 2
        assert organized["columns"]["databases"]["column_size"] == 1

    def test_top_item_is_highest_scored(self):
        """Column top item has highest _wilson_lower score."""
        from pulse.brain.brain_search.search_scoring_ext import _organize_columns
        results = {
            "wins": [
                {"id": "a", "domain": "python", "confidence": 0.5, "_wilson_lower": 0.3},
                {"id": "b", "domain": "python", "confidence": 0.9, "_wilson_lower": 0.85},
                {"id": "c", "domain": "python", "confidence": 0.6, "_wilson_lower": 0.55},
            ]
        }
        organized = _organize_columns(results)
        top = organized["columns"]["python"]["top"]
        assert top["id"] == "b", f"Top item should be highest-scored, got {top.get('id')}"

    def test_rest_items_nested_under_top(self):
        """Non-top items placed in rest list."""
        from pulse.brain.brain_search.search_scoring_ext import _organize_columns
        results = {
            "wins": [
                {"id": "a", "domain": "x", "_wilson_lower": 0.9},
                {"id": "b", "domain": "x", "_wilson_lower": 0.5},
                {"id": "c", "domain": "x", "_wilson_lower": 0.3},
            ]
        }
        organized = _organize_columns(results)
        rest = organized["columns"]["x"]["rest"]
        assert len(rest) == 2
        rest_ids = [h["id"] for h in rest]
        assert "a" not in rest_ids  # top should not be in rest

    def test_flat_results_preserved(self):
        """Original results preserved under flat key."""
        from pulse.brain.brain_search.search_scoring_ext import _organize_columns
        results = {"wins": [{"domain": "d", "id": "z", "_wilson_lower": 0.5}]}
        organized = _organize_columns(results)
        assert "flat" in organized
        assert organized["flat"] is results

    def test_empty_results_handled(self):
        """Empty input returns empty columns dict."""
        from pulse.brain.brain_search.search_scoring_ext import _organize_columns
        organized = _organize_columns({})
        assert organized["columns"] == {}

    def test_fails_open_on_exception(self):
        """Errors return empty columns, not an exception."""
        from pulse.brain.brain_search.search_scoring_ext import _organize_columns
        result = _organize_columns({"bad": None})
        assert isinstance(result, dict)
        assert "columns" in result


# ---------------------------------------------------------------------------
# Item 127: Sleep pressure accumulation (sleep_pressure.py)
# ---------------------------------------------------------------------------

class TestItem127SleepPressure:

    def test_low_pressure_returns_min_batch(self):
        """<=50 items returns minimum batch size."""
        from pulse.daemons.synthesis.sleep.sleep_pressure import get_adaptive_batch_size, _MIN_BATCH_SIZE
        brain_files = {}
        with patch("pulse.daemons.synthesis.sleep.sleep_pressure._get_sleep_pressure", return_value=30):
            result = get_adaptive_batch_size(brain_files)
        assert result == _MIN_BATCH_SIZE

    def test_high_pressure_returns_max_batch(self):
        """>=500 items returns maximum batch size."""
        from pulse.daemons.synthesis.sleep.sleep_pressure import get_adaptive_batch_size, _MAX_BATCH_SIZE
        with patch("pulse.daemons.synthesis.sleep.sleep_pressure._get_sleep_pressure", return_value=600):
            result = get_adaptive_batch_size({})
        assert result == _MAX_BATCH_SIZE

    def test_medium_pressure_linear_scale(self):
        """Medium pressure (275 items = midpoint) returns ~27 (midpoint of 5-50)."""
        from pulse.daemons.synthesis.sleep.sleep_pressure import get_adaptive_batch_size
        # midpoint of 50-500 = 275 -> should be approx (5+50)/2 = 27.5 -> 28 or 27
        with patch("pulse.daemons.synthesis.sleep.sleep_pressure._get_sleep_pressure", return_value=275):
            result = get_adaptive_batch_size({})
        assert 20 <= result <= 35, f"Expected mid-range batch ~27, got {result}"

    def test_zero_pressure_returns_min(self):
        """Zero items (no backlog) returns minimum batch."""
        from pulse.daemons.synthesis.sleep.sleep_pressure import get_adaptive_batch_size, _MIN_BATCH_SIZE
        with patch("pulse.daemons.synthesis.sleep.sleep_pressure._get_sleep_pressure", return_value=0):
            result = get_adaptive_batch_size({})
        assert result == _MIN_BATCH_SIZE

    def test_exception_returns_default(self):
        """Any exception returns default of 10."""
        from pulse.daemons.synthesis.sleep.sleep_pressure import get_adaptive_batch_size
        with patch("pulse.daemons.synthesis.sleep.sleep_pressure._get_sleep_pressure",
                   side_effect=Exception("boom")):
            result = get_adaptive_batch_size({})
        assert result == 10

    def test_sleep_pressure_counts_items(self):
        """_get_sleep_pressure sums items across brain files."""
        from pulse.daemons.synthesis.sleep.sleep_pressure import _get_sleep_pressure
        fake_path = MagicMock(spec=Path)
        fake_path.exists.return_value = True
        # read_jsonl_entries is imported inside the function from pulse.core.brain_utils
        with patch("pulse.core.brain_utils.read_jsonl_entries", return_value=[{}, {}, {}]):
            result = _get_sleep_pressure({"lessons": fake_path, "failures": fake_path})
        assert result == 6  # 3 items * 2 files


# ---------------------------------------------------------------------------
# Item 128: Emotional regulation — dampen extreme salience (reward_signal.py)
# ---------------------------------------------------------------------------

class TestItem128EmotionalRegulation:

    def _make_conn_with_salience(self, sal_value):
        """Create mock DB conn that returns given salience."""
        conn = MagicMock()
        sal_row = MagicMock()
        sal_row.__getitem__ = lambda self, k: sal_value if k == 0 else sal_value
        sal_row.__iter__ = lambda self: iter([sal_value])
        conn.execute.return_value.fetchone.return_value = [sal_value]
        return conn

    def test_salience_cap_in_source(self):
        """reward_signal.py contains the Item 128 emotional regulation cap logic."""
        rws_path = ROOT / "pulse" / "core" / "reward_signal.py"
        source = rws_path.read_text(encoding="utf-8")
        assert "Item 128" in source, "Item 128 comment not found"
        assert "_overflow" in source or "overflow" in source.lower(), "Overflow variable not found"
        assert "6.0" in source, "Threshold 6.0 not found"

    def test_diminishing_returns_formula_above_6(self):
        """Verify formula: if new_sal > 6.0 -> overflow * 0.5 applied."""
        # Direct formula test (no DB needed)
        base_sal = 5.5
        sal_delta = 1.0
        new_sal = base_sal + sal_delta  # 6.5

        if new_sal > 6.0:
            overflow = new_sal - 6.0
            dampened = 6.0 + overflow * 0.5
        else:
            dampened = new_sal

        assert dampened == pytest.approx(6.25, abs=0.01), (
            f"Expected 6.25 for 6.5 input, got {dampened}"
        )

    def test_formula_well_above_threshold(self):
        """Very high salience (8.0) gets dampened to 7.0."""
        base_sal = 7.0
        sal_delta = 1.0
        new_sal = base_sal + sal_delta  # 8.0
        if new_sal > 6.0:
            overflow = new_sal - 6.0
            dampened = 6.0 + overflow * 0.5
        else:
            dampened = new_sal
        assert dampened == pytest.approx(7.0, abs=0.01)

    def test_emotional_regulation_in_reward_signal_source(self):
        """Source code contains the Item 128 emotional regulation formula."""
        rws_path = ROOT / "pulse" / "core" / "reward_signal.py"
        source = rws_path.read_text(encoding="utf-8")
        assert "Item 128" in source, "Item 128 comment not found in reward_signal.py"
        assert "6.0" in source, "Threshold 6.0 not found in reward_signal.py"
        assert "* 0.5" in source, "Dampening factor 0.5 not found"


# ---------------------------------------------------------------------------
# Item 129: Knowledge type routing in search (query_routing.py)
# ---------------------------------------------------------------------------

class TestItem129KnowledgeTypeRouting:

    def test_how_to_query_puts_procedures_first(self):
        """'How to' queries should put procedures first in source order."""
        from pulse.brain.brain_search.query_routing import boost_sources_by_intent
        sources = {
            "wins": lambda q, c: [],
            "procedures": lambda q, c: [],
            "fails": lambda q, c: [],
        }
        reordered = boost_sources_by_intent("How to implement STDP", sources)
        keys = list(reordered.keys())
        assert keys[0] == "procedures", f"Expected procedures first, got {keys[0]}"

    def test_why_did_query_puts_fails_first(self):
        """'Why did' queries should put fails first."""
        from pulse.brain.brain_search.query_routing import boost_sources_by_intent
        sources = {
            "facts": lambda q, c: [],
            "fails": lambda q, c: [],
            "decisions": lambda q, c: [],
        }
        reordered = boost_sources_by_intent("Why did the pipeline fail", sources)
        keys = list(reordered.keys())
        assert keys[0] == "fails", f"Expected fails first, got {keys[0]}"

    def test_what_is_query_puts_facts_first(self):
        """'What is' queries should put facts first."""
        from pulse.brain.brain_search.query_routing import boost_sources_by_intent
        sources = {
            "wins": lambda q, c: [],
            "facts": lambda q, c: [],
            "schemas": lambda q, c: [],
        }
        reordered = boost_sources_by_intent("What is the salience threshold", sources)
        keys = list(reordered.keys())
        assert keys[0] in ("facts", "schemas"), f"Expected facts/schemas first, got {keys[0]}"

    def test_no_match_returns_original_order(self):
        """Queries without intent pattern return sources unchanged."""
        from pulse.brain.brain_search.query_routing import boost_sources_by_intent
        sources = {"wins": None, "fails": None, "patterns": None}
        reordered = boost_sources_by_intent("random query without intent", sources)
        assert list(reordered.keys()) == list(sources.keys())

    def test_exception_returns_original(self):
        """Exception returns original sources (fail-open)."""
        from pulse.brain.brain_search.query_routing import boost_sources_by_intent
        sources = {"wins": None}
        with patch("pulse.brain.brain_search.query_routing._detect_intent",
                   side_effect=Exception("boom")):
            result = boost_sources_by_intent("test", sources)
        assert result is sources

    def test_detect_intent_error_pattern(self):
        """Error-related queries trigger fails/anti_patterns boost."""
        from pulse.brain.brain_search.query_routing import _detect_intent
        result = _detect_intent("ImportError crash on startup")
        assert len(result) > 0
        assert any(s in ("fails", "anti_patterns") for s in result)


# ---------------------------------------------------------------------------
# Item 130: Brain utilization metrics (metamemory.py)
# ---------------------------------------------------------------------------

class TestItem130BrainUtilization:

    def test_utilization_returns_correct_structure(self):
        """get_brain_utilization returns dict with required keys."""
        from pulse.brain.metacognition.metamemory import get_brain_utilization
        with patch("pulse.core.brain_db.get_conn") as mock_gc:
            conn = MagicMock()
            conn.execute.return_value.fetchone.return_value = [100]
            mock_gc.return_value = conn
            result = get_brain_utilization()
        assert isinstance(result, dict)
        for key in ("total_items", "used_last_7d", "utilization_rate"):
            assert key in result, f"Missing key: {key}"

    def test_utilization_rate_computed_correctly(self):
        """Utilization rate = used_last_7d / total_items."""
        from pulse.brain.metacognition.metamemory import get_brain_utilization
        mock_conn = MagicMock()
        call_count = [0]
        def side_eff(sql, *args):
            m = MagicMock()
            call_count[0] += 1
            if call_count[0] % 2 == 1:
                m.fetchone.return_value = [100]  # total
            else:
                m.fetchone.return_value = [25]   # used
            return m
        mock_conn.execute.side_effect = side_eff
        with patch("pulse.core.brain_db.get_conn", return_value=mock_conn):
            result = get_brain_utilization()
        # 4 tables x (total=100, used=25) -> total=400, used=100 -> rate=0.25
        assert result["utilization_rate"] == pytest.approx(0.25, abs=0.01)

    def test_zero_total_returns_zero_rate(self):
        """When no items exist, utilization_rate is 0.0 (no division by zero)."""
        from pulse.brain.metacognition.metamemory import get_brain_utilization
        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_brain_utilization()
        assert result["utilization_rate"] == 0.0

    def test_exception_returns_zeros(self):
        """DB exception returns safe zero-valued dict."""
        from pulse.brain.metacognition.metamemory import get_brain_utilization
        with patch("pulse.core.brain_db.get_conn", side_effect=Exception("boom")):
            result = get_brain_utilization()
        assert result["total_items"] == 0
        assert result["utilization_rate"] == 0.0

    def test_function_in_source(self):
        """get_brain_utilization and Item 130 comment exist in metamemory.py."""
        meta_path = ROOT / "pulse" / "brain" / "metamemory.py"
        source = meta_path.read_text(encoding="utf-8")
        assert "get_brain_utilization" in source
        assert "Item 130" in source
