"""
Integration tests for P43 neuroscience items 186-190.
Run: python -m pytest tests/test_neuro_p43.py -v
"""
import json
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 186: Social broadcast consumer — broadcast flag boosts confidence
# ---------------------------------------------------------------------------
class TestItem186BroadcastBoost:

    def _make_item(self, broadcast=False, social_broadcast=False, confidence=0.5):
        item = {
            "validity": "valid",
            "confidence": confidence,
            "type": "failure",
            "timestamp": "",
        }
        if broadcast:
            item["broadcast"] = True
        if social_broadcast:
            item["_social_broadcast"] = True
        return item

    def _run_score(self, item):
        from pulse.brain.brain_search.search_scoring import _score_item
        tc_fn = lambda c, knowledge_type="", timestamp="": c
        ts_fn = lambda s, ts: s
        wilson_fn = lambda c, ec: (c * 0.9, c)
        with patch("pulse.brain.integrity.provenance.get_provenance", return_value=None):
            return _score_item(
                item, "wins", "query", None, set(),
                None, {}, {},
                tc_fn, ts_fn, wilson_fn,
            )

    def test_broadcast_flag_boosts_confidence(self):
        item = self._make_item(broadcast=True, confidence=0.5)
        result = self._run_score(item)
        assert result is not None
        assert result.get("_broadcast_boosted") is True
        assert result["confidence"] > 0.5

    def test_social_broadcast_flag_boosts_confidence(self):
        item = self._make_item(social_broadcast=True, confidence=0.4)
        result = self._run_score(item)
        assert result is not None
        assert result.get("_broadcast_boosted") is True
        assert result["confidence"] > 0.4

    def test_no_broadcast_flag_no_boost(self):
        item = self._make_item(confidence=0.5)
        result = self._run_score(item)
        assert result is not None
        assert not result.get("_broadcast_boosted")

    def test_broadcast_boost_capped_at_1(self):
        item = self._make_item(broadcast=True, confidence=0.95)
        result = self._run_score(item)
        assert result is not None
        assert result["confidence"] <= 1.0

    def test_broadcast_boost_magnitude(self):
        # confidence starts at 0.5; broadcast adds +0.15 = 0.65
        # task_type=None so no context-match boost; mocked calibration is identity
        item = {"validity": "valid", "confidence": 0.5, "type": "lesson", "timestamp": ""}
        item["broadcast"] = True
        result = self._run_score(item)
        assert result is not None
        # Broadcast adds 0.15; other boosts may vary — just verify it increased by >= 0.1
        assert result["confidence"] >= 0.6


# ---------------------------------------------------------------------------
# Item 187: Cortical columns consumer — _organize_columns wired into briefing
# ---------------------------------------------------------------------------
class TestItem187CorticalColumns:

    def test_organize_columns_exists_and_callable(self):
        from pulse.brain.brain_search.search_scoring_ext import _organize_columns
        results = {
            "wins": [{"domain": "brain", "confidence": 0.8, "_wilson_lower": 0.7}],
            "fails": [{"domain": "api", "confidence": 0.6, "_wilson_lower": 0.5}],
        }
        out = _organize_columns(results)
        assert "columns" in out
        assert "flat" in out

    def test_organize_groups_by_domain(self):
        from pulse.brain.brain_search.search_scoring_ext import _organize_columns
        results = {
            "wins": [
                {"domain": "brain", "confidence": 0.8, "_wilson_lower": 0.7},
                {"domain": "api",   "confidence": 0.6, "_wilson_lower": 0.5},
            ],
        }
        out = _organize_columns(results)
        cols = out["columns"]
        assert "brain" in cols
        assert "api" in cols

    def test_column_top_item_is_highest_confidence(self):
        from pulse.brain.brain_search.search_scoring_ext import _organize_columns
        results = {
            "wins": [
                {"domain": "brain", "confidence": 0.9, "_wilson_lower": 0.85},
                {"domain": "brain", "confidence": 0.6, "_wilson_lower": 0.55},
            ],
        }
        out = _organize_columns(results)
        top = out["columns"]["brain"]["top"]
        assert top["confidence"] == 0.9

    def test_briefing_emits_columns_line_with_multiple_domains(self):
        from pulse.brain.brain_query.briefing import format_briefing
        results = {
            "results": {
                "wins": [
                    {"domain": "brain", "confidence": 0.8, "_wilson_lower": 0.7,
                     "text": "brain knowledge item"},
                    {"domain": "api",   "confidence": 0.7, "_wilson_lower": 0.6,
                     "text": "api knowledge item"},
                ],
            },
            "total_results": 2,
            "elapsed_ms": 5,
        }
        out = format_briefing("test query", results)
        assert "COLUMNS:" in out

    def test_organize_columns_fails_open_on_empty(self):
        from pulse.brain.brain_search.search_scoring_ext import _organize_columns
        out = _organize_columns({})
        assert "columns" in out
        assert out["columns"] == {}

    def test_column_size_tracked(self):
        from pulse.brain.brain_search.search_scoring_ext import _organize_columns
        results = {
            "wins": [
                {"domain": "brain", "confidence": 0.9, "_wilson_lower": 0.85},
                {"domain": "brain", "confidence": 0.6, "_wilson_lower": 0.55},
                {"domain": "brain", "confidence": 0.4, "_wilson_lower": 0.35},
            ],
        }
        out = _organize_columns(results)
        assert out["columns"]["brain"]["column_size"] == 3


# ---------------------------------------------------------------------------
# Item 188: Near-miss learning loop — domain warning
# ---------------------------------------------------------------------------
class TestItem188NearMissWarning:

    def test_returns_none_when_no_near_misses(self):
        from pulse.brain.brain_search.near_miss_advisor import get_near_miss_warning
        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_near_miss_warning("brain query", domain="brain")
        assert result is None

    def test_returns_warning_when_near_misses_exist(self):
        from pulse.brain.brain_search.near_miss_advisor import get_near_miss_warning
        mock_conn = MagicMock()
        mock_conn.execute.return_value.fetchone.return_value = (3,)
        with patch("pulse.core.brain_db.get_conn", return_value=mock_conn):
            result = get_near_miss_warning("brain query", domain="brain")
        assert result is not None
        assert "NEAR-MISS" in result
        assert "brain" in result
        assert "3" in result

    def test_no_warning_for_general_domain(self):
        from pulse.brain.brain_search.near_miss_advisor import get_near_miss_warning
        result = get_near_miss_warning("some query", domain="general")
        assert result is None

    def test_infers_domain_from_query(self):
        from pulse.brain.brain_search.near_miss_advisor import get_near_miss_warning
        mock_conn = MagicMock()
        mock_conn.execute.return_value.fetchone.return_value = (2,)
        with patch("pulse.core.brain_db.get_conn", return_value=mock_conn):
            result = get_near_miss_warning("database migration strategy")
        assert result is not None
        assert "NEAR-MISS" in result

    def test_fails_open_on_db_error(self):
        from pulse.brain.brain_search.near_miss_advisor import get_near_miss_warning
        with patch("pulse.core.brain_db.get_conn", side_effect=Exception("db down")):
            result = get_near_miss_warning("some query", domain="api")
        assert result is None

    def test_briefing_includes_near_miss_warning(self):
        from pulse.brain.brain_query.briefing import format_briefing
        mock_warning = "!! NEAR-MISS WARNING: 2 near-miss event(s) in domain 'api'"
        fake_nm = lambda q, domain="": mock_warning
        results = {
            "results": {
                "wins": [{"domain": "api", "confidence": 0.7, "text": "api item",
                          "_wilson_lower": 0.6}],
            },
            "total_results": 1,
            "elapsed_ms": 3,
        }
        with patch("pulse.brain.brain_query.briefing._get_nm_warning", fake_nm):
            out = format_briefing("api migration", results)
        assert "NEAR-MISS" in out


# ---------------------------------------------------------------------------
# Item 189: Prediction no-feedback decay via SQLite
# ---------------------------------------------------------------------------
class TestItem189PredictionDecay:

    def test_decay_function_exists(self):
        from pulse.daemons.neural.prediction_daemon import _decay_no_feedback_predictions
        assert callable(_decay_no_feedback_predictions)

    def test_decay_updates_predictions_table(self):
        from pulse.daemons.neural.prediction_daemon import _decay_no_feedback_predictions
        mock_conn = MagicMock()
        mock_conn.execute.return_value.fetchone.return_value = ("predictions",)
        with patch("pulse.core.brain_db.get_conn", return_value=mock_conn):
            _decay_no_feedback_predictions()
        calls_with_update = [
            str(c) for c in mock_conn.execute.call_args_list
            if "UPDATE predictions" in str(c)
        ]
        assert len(calls_with_update) >= 1

    def test_decay_skips_when_table_missing(self):
        from pulse.daemons.neural.prediction_daemon import _decay_no_feedback_predictions
        mock_conn = MagicMock()
        mock_conn.execute.return_value.fetchone.return_value = None
        with patch("pulse.core.brain_db.get_conn", return_value=mock_conn):
            _decay_no_feedback_predictions()
        update_calls = [
            str(c) for c in mock_conn.execute.call_args_list
            if "UPDATE predictions" in str(c)
        ]
        assert len(update_calls) == 0

    def test_decay_fails_open_on_db_error(self):
        from pulse.daemons.neural.prediction_daemon import _decay_no_feedback_predictions
        with patch("pulse.core.brain_db.get_conn", side_effect=Exception("conn failed")):
            _decay_no_feedback_predictions()  # must not raise

    def test_decay_commits_after_update(self):
        from pulse.daemons.neural.prediction_daemon import _decay_no_feedback_predictions
        mock_conn = MagicMock()
        mock_conn.execute.return_value.fetchone.return_value = ("predictions",)
        with patch("pulse.core.brain_db.get_conn", return_value=mock_conn):
            _decay_no_feedback_predictions()
        mock_conn.commit.assert_called()

    def test_decay_uses_48h_cutoff(self):
        from pulse.daemons.neural.prediction_daemon import _decay_no_feedback_predictions
        mock_conn = MagicMock()
        mock_conn.execute.return_value.fetchone.return_value = ("predictions",)
        with patch("pulse.core.brain_db.get_conn", return_value=mock_conn):
            _decay_no_feedback_predictions()
        # The UPDATE call should pass a timestamp parameter
        update_call = next(
            (c for c in mock_conn.execute.call_args_list if "UPDATE predictions" in str(c)),
            None,
        )
        assert update_call is not None
        args = update_call[0]
        # Second positional arg is the params tuple
        if len(args) > 1:
            params = args[1]
            assert len(params) >= 1


# ---------------------------------------------------------------------------
# Item 190: Superseded item chain — surface replacement alongside superseded
# ---------------------------------------------------------------------------
class TestItem190SupersededChain:

    def test_fetch_replacement_exists(self):
        from pulse.brain.brain_search.search_scoring import _fetch_superseded_replacement
        assert callable(_fetch_superseded_replacement)

    def test_fetch_replacement_returns_none_when_no_metadata(self):
        from pulse.brain.brain_search.search_scoring import _fetch_superseded_replacement
        item = {"validity": "superseded", "metadata": "{}", "id": "old-id"}
        with patch("pulse.core.brain_db.get_conn", MagicMock()):
            result = _fetch_superseded_replacement(item)
        assert result is None

    def test_fetch_replacement_fails_open_on_error(self):
        from pulse.brain.brain_search.search_scoring import _fetch_superseded_replacement
        item = {
            "validity": "superseded",
            "id": "old",
            "metadata": json.dumps({"superseded_by": "new-id"}),
        }
        with patch("pulse.core.brain_db.get_conn", side_effect=Exception("db error")):
            result = _fetch_superseded_replacement(item)
        assert result is None

    def test_superseded_item_triggers_replacement_fetch(self):
        from pulse.brain.brain_search.search_scoring import score_and_filter_results
        superseded = {
            "validity": "superseded",
            "id": "old-id",
            "confidence": 0.8,
            "type": "lesson",
            "timestamp": "",
            "salience": 1.0,
            "evidence_count": 1,
            "title": "Old Title",
            "content": "old content",
            "metadata": json.dumps({"superseded_by": "new-id"}),
        }
        results = {"lessons": [superseded]}
        replacement = {
            "id": "new-id",
            "content": "New knowledge",
            "title": "Updated Title",
            "domain": "brain",
            "confidence": 0.8,
            "type": "lesson",
            "validity": "valid",
            "timestamp": "2026-01-01T00:00:00Z",
            "_is_replacement_for": "old-id",
            "_replacement_source": "lessons",
        }
        with patch("pulse.core.temporal.temporal_confidence", lambda c, **kw: c), \
             patch("pulse.core.temporal.temporal_salience", lambda s, ts: s), \
             patch("pulse.core.uncertainty.wilson_interval", return_value=(0.3, 0.7)), \
             patch("pulse.brain.integrity.provenance.get_provenance", return_value=None), \
             patch("pulse.brain.brain_search.search_scoring._fetch_superseded_replacement",
                   return_value=replacement):
            out = score_and_filter_results(results, "query", None, {}, {})
        all_items = out.get("lessons", [])
        replacement_items = [i for i in all_items if i.get("_is_replacement_for")]
        assert len(replacement_items) >= 1

    def test_non_superseded_items_unaffected(self):
        from pulse.brain.brain_search.search_scoring import score_and_filter_results
        valid_item = {
            "validity": "valid", "id": "id1", "confidence": 0.7,
            "type": "lesson", "timestamp": "", "salience": 1.0,
            "evidence_count": 1, "title": "Valid", "content": "valid content",
            "metadata": "{}",
        }
        results = {"lessons": [valid_item]}
        with patch("pulse.core.temporal.temporal_confidence", lambda c, **kw: c), \
             patch("pulse.core.temporal.temporal_salience", lambda s, ts: s), \
             patch("pulse.core.uncertainty.wilson_interval", return_value=(0.3, 0.7)), \
             patch("pulse.brain.integrity.provenance.get_provenance", return_value=None), \
             patch("pulse.brain.brain_search.search_scoring._fetch_superseded_replacement") as mock_fetch:
            out = score_and_filter_results(results, "query", None, {}, {})
        mock_fetch.assert_not_called()

    def test_replacement_chain_flag_set(self):
        from pulse.brain.brain_search.search_scoring import score_and_filter_results
        superseded = {
            "validity": "superseded",
            "id": "old-id",
            "confidence": 0.8,
            "type": "lesson",
            "timestamp": "",
            "salience": 1.0,
            "evidence_count": 1,
            "title": "Old Title",
            "content": "old content",
            "metadata": json.dumps({"superseded_by": "new-id"}),
        }
        replacement = {
            "id": "new-id",
            "content": "Updated",
            "title": "New",
            "domain": "brain",
            "confidence": 0.9,
            "type": "lesson",
            "validity": "valid",
            "timestamp": "2026-01-01T00:00:00Z",
            "_is_replacement_for": "old-id",
            "_replacement_source": "lessons",
        }
        results = {"lessons": [superseded]}
        with patch("pulse.core.temporal.temporal_confidence", lambda c, **kw: c), \
             patch("pulse.core.temporal.temporal_salience", lambda s, ts: s), \
             patch("pulse.core.uncertainty.wilson_interval", return_value=(0.3, 0.7)), \
             patch("pulse.brain.integrity.provenance.get_provenance", return_value=None), \
             patch("pulse.brain.brain_search.search_scoring._fetch_superseded_replacement",
                   return_value=replacement):
            out = score_and_filter_results(results, "query", None, {}, {})
        chain_items = [i for i in out.get("lessons", []) if i.get("_superseded_chain")]
        assert len(chain_items) >= 1
