"""
Integration tests for S5 wiring: metamemory tracking functions wired into trigger points.

Items tested:
  - Item 1: record_domain_valence wired into build_jsonl_entry (jsonl_store.py)
  - Item 2: record_agent_collaboration wired into finalize_task (task_claiming.py)
  - Item 3: record_domain_prediction wired into record_prediction_outcome (prediction_feedback.py)
  - Item 4: get_consolidation_debt wired into get_brain_health_report (brain_self_assessment.py)

Run: python -m pytest tests/test_wire_s5.py -v
"""
import sys
from pathlib import Path
from unittest.mock import MagicMock, patch, call
import pytest

ROOT = Path(__file__).resolve().parent.parent
if str(ROOT) not in sys.path:
    sys.path.insert(0, str(ROOT))


# ---------------------------------------------------------------------------
# Item 1: record_domain_valence wired into build_jsonl_entry
# ---------------------------------------------------------------------------
class TestItem1DomainValenceWiredIntoBuildJsonlEntry:

    def _make_entry_kwargs(self, domain="brain", content="critical failure crash"):
        return dict(
            description=content,
            source="test_agent",
            timestamp="2026-03-21T00:00:00+00:00",
            domain=domain,
            confidence=0.7,
            salience=2.0,
            knowledge_type="lesson",
        )

    def test_record_domain_valence_called_on_entry_build(self):
        """record_domain_valence must be called during build_jsonl_entry."""
        with patch("pulse.brain.metacognition.metamemory_p34.record_domain_valence") as mock_rdv, \
             patch("pulse.core.temporal.stamp_temporal_fields", side_effect=lambda e, *a: e), \
             patch("pulse.core.uncertainty.stamp_uncertainty_fields", side_effect=lambda e: e), \
             patch("pulse.core.brain_db.get_conn", side_effect=Exception("no db")), \
             patch("pulse.brain.metacognition.metamemory.get_metamemory", return_value={}):
            from pulse.brain.save_writers.jsonl_store import build_jsonl_entry
            build_jsonl_entry(**self._make_entry_kwargs())
        mock_rdv.assert_called_once()

    def test_record_domain_valence_receives_correct_domain(self):
        """Domain passed to record_domain_valence must match entry domain."""
        calls_made = []
        original_rdv = None

        def spy_rdv(domain, text):
            calls_made.append((domain, text))

        with patch("pulse.brain.metacognition.metamemory_p34.record_domain_valence", side_effect=spy_rdv), \
             patch("pulse.core.temporal.stamp_temporal_fields", side_effect=lambda e, *a: e), \
             patch("pulse.core.uncertainty.stamp_uncertainty_fields", side_effect=lambda e: e), \
             patch("pulse.core.brain_db.get_conn", side_effect=Exception("no db")), \
             patch("pulse.brain.metacognition.metamemory.get_metamemory", return_value={}):
            from pulse.brain.save_writers.jsonl_store import build_jsonl_entry
            build_jsonl_entry(**self._make_entry_kwargs(domain="security"))

        assert len(calls_made) == 1
        assert calls_made[0][0] == "security"

    def test_record_domain_valence_receives_content_text(self):
        """Text passed to record_domain_valence must be content string."""
        calls_made = []

        def spy_rdv(domain, text):
            calls_made.append((domain, text))

        with patch("pulse.brain.metacognition.metamemory_p34.record_domain_valence", side_effect=spy_rdv), \
             patch("pulse.core.temporal.stamp_temporal_fields", side_effect=lambda e, *a: e), \
             patch("pulse.core.uncertainty.stamp_uncertainty_fields", side_effect=lambda e: e), \
             patch("pulse.core.brain_db.get_conn", side_effect=Exception("no db")), \
             patch("pulse.brain.metacognition.metamemory.get_metamemory", return_value={}):
            from pulse.brain.save_writers.jsonl_store import build_jsonl_entry
            build_jsonl_entry(**self._make_entry_kwargs(content="critical crash"))

        assert len(calls_made) == 1
        # text must be a string (not a float), so .lower() won't crash
        assert isinstance(calls_made[0][1], str)

    def test_build_jsonl_entry_survives_valence_exception(self):
        """build_jsonl_entry must not raise even if record_domain_valence crashes."""
        with patch("pulse.brain.metacognition.metamemory_p34.record_domain_valence",
                   side_effect=RuntimeError("boom")), \
             patch("pulse.core.temporal.stamp_temporal_fields", side_effect=lambda e, *a: e), \
             patch("pulse.core.uncertainty.stamp_uncertainty_fields", side_effect=lambda e: e), \
             patch("pulse.core.brain_db.get_conn", side_effect=Exception("no db")), \
             patch("pulse.brain.metacognition.metamemory.get_metamemory", return_value={}):
            from pulse.brain.save_writers.jsonl_store import build_jsonl_entry
            entry = build_jsonl_entry(**self._make_entry_kwargs())
        assert entry["domain"] == "brain"

    def test_valence_accumulates_in_domain_state(self):
        """Calling record_domain_valence directly accumulates data per domain."""
        from pulse.brain import metamemory_p34
        # Reset state for isolation
        metamemory_p34._DOMAIN_VALENCE.clear()
        metamemory_p34.record_domain_valence("testing", "critical failure crash urgent")
        result = metamemory_p34.get_domain_valence()
        assert "testing" in result
        assert result["testing"]["count"] == 1


# ---------------------------------------------------------------------------
# Item 2: record_agent_collaboration wired into finalize_task
# ---------------------------------------------------------------------------
class TestItem2AgentCollaborationWiredIntoFinalizeTask:

    def _make_board_with_task(self, task_id, agent_id, domain="brain", co_agent=None):
        """Build a minimal task board for testing."""
        task = {
            "task_id": task_id,
            "status": "active",
            "domain": domain,
            "claimed_by": agent_id,
            "title": "Test task",
        }
        tasks = [task]
        if co_agent:
            tasks.append({
                "task_id": "other-task",
                "status": "done",
                "domain": domain,
                "completed_by": co_agent,
                "claimed_by": co_agent,
                "title": "Other task",
            })
        return {"dispatches": [{"tasks": tasks}]}

    def test_record_agent_collaboration_called_when_co_agent_exists(self):
        """record_agent_collaboration must be called when a co-agent is found in same domain."""
        from pulse.tasks import task_claiming
        board_path = MagicMock()
        calls_made = []

        def mock_atomic(path, fn, default=None):
            board = self._make_board_with_task(
                "task-1", "claude_1", domain="brain", co_agent="gemini_2"
            )
            fn(board)

        def spy_collab(a, b, domain, succeeded):
            calls_made.append((a, b, domain, succeeded))

        ops_mock = MagicMock()
        ops_mock._get_atomic_update.return_value = mock_atomic
        ops_mock._now_iso.return_value = "2026-03-21T00:00:00+00:00"
        ops_mock._get_crypto.return_value = None

        with patch.object(task_claiming, "_ops", return_value=ops_mock), \
             patch("pulse.brain.metacognition.metamemory_p34.record_agent_collaboration", side_effect=spy_collab):
            task_claiming.finalize_task(board_path, "task-1", "claude_1", outcome="done", status="done")

        assert len(calls_made) == 1
        assert calls_made[0][2] == "brain"  # correct domain

    def test_no_collaboration_recorded_when_sole_agent(self):
        """record_agent_collaboration must NOT be called when no co-agent in same domain."""
        from pulse.tasks import task_claiming
        board_path = MagicMock()
        calls_made = []

        def mock_atomic(path, fn, default=None):
            board = self._make_board_with_task("task-1", "claude_1", domain="brain", co_agent=None)
            fn(board)

        def spy_collab(a, b, domain, succeeded):
            calls_made.append((a, b, domain, succeeded))

        ops_mock = MagicMock()
        ops_mock._get_atomic_update.return_value = mock_atomic
        ops_mock._now_iso.return_value = "2026-03-21T00:00:00+00:00"
        ops_mock._get_crypto.return_value = None

        with patch.object(task_claiming, "_ops", return_value=ops_mock), \
             patch("pulse.brain.metacognition.metamemory_p34.record_agent_collaboration", side_effect=spy_collab):
            task_claiming.finalize_task(board_path, "task-1", "claude_1", outcome="done", status="done")

        assert len(calls_made) == 0

    def test_finalize_task_survives_collaboration_exception(self):
        """finalize_task must complete even if record_agent_collaboration raises."""
        from pulse.tasks import task_claiming
        board_path = MagicMock()
        completed = []

        def mock_atomic(path, fn, default=None):
            board = self._make_board_with_task(
                "task-1", "claude_1", domain="brain", co_agent="gemini_2"
            )
            fn(board)
            completed.append(True)

        ops_mock = MagicMock()
        ops_mock._get_atomic_update.return_value = mock_atomic
        ops_mock._now_iso.return_value = "2026-03-21T00:00:00+00:00"
        ops_mock._get_crypto.return_value = None

        with patch.object(task_claiming, "_ops", return_value=ops_mock), \
             patch("pulse.brain.metacognition.metamemory_p34.record_agent_collaboration",
                   side_effect=RuntimeError("crash")):
            task_claiming.finalize_task(board_path, "task-1", "claude_1", outcome="done", status="done")

        assert len(completed) == 1

    def test_collaboration_recorded_with_correct_success_flag(self):
        """both_succeeded=True when POISON_PILL not in outcome."""
        from pulse.tasks import task_claiming
        board_path = MagicMock()
        calls_made = []

        def mock_atomic(path, fn, default=None):
            board = self._make_board_with_task(
                "task-1", "claude_1", domain="brain", co_agent="gemini_2"
            )
            fn(board)

        def spy_collab(a, b, domain, succeeded):
            calls_made.append(succeeded)

        ops_mock = MagicMock()
        ops_mock._get_atomic_update.return_value = mock_atomic
        ops_mock._now_iso.return_value = "2026-03-21T00:00:00+00:00"
        ops_mock._get_crypto.return_value = None

        with patch.object(task_claiming, "_ops", return_value=ops_mock), \
             patch("pulse.brain.metacognition.metamemory_p34.record_agent_collaboration", side_effect=spy_collab):
            task_claiming.finalize_task(board_path, "task-1", "claude_1", outcome="Task done well.", status="done")

        assert calls_made == [True]

    def test_collaboration_state_accumulates(self):
        """record_agent_collaboration in metamemory_p34 accumulates data in memory."""
        from pulse.brain import metamemory_p34
        metamemory_p34._COLLAB_HISTORY.clear()
        metamemory_p34.record_agent_collaboration("claude_1", "gemini_2", "brain", True)
        metamemory_p34.record_agent_collaboration("claude_1", "gemini_2", "brain", True)
        recs = metamemory_p34.get_collaboration_recommendations()
        # Need >= 2 co-claims to appear
        assert len(recs) == 1
        assert recs[0]["success_rate"] == 1.0


# ---------------------------------------------------------------------------
# Item 3: record_domain_prediction wired into record_prediction_outcome
# ---------------------------------------------------------------------------
class TestItem3DomainPredictionWiredIntoPredictionFeedback:

    def _alerts_with_one_match(self, task_id, domain="brain", confidence=0.7, outcome="false_positive"):
        return {
            "alerts": [
                {
                    "task_id": task_id,
                    "status": "active",
                    "confidence": confidence,
                    "domain": domain,
                    "outcome": outcome,
                    "prediction": "test prediction",
                    "evidence_chain": [],
                }
            ],
            "total_generated": 1,
        }

    def test_record_domain_prediction_called_after_prediction_scored(self):
        """record_domain_prediction must be called when a matching alert is resolved."""
        from pulse.daemons.neural import prediction_feedback
        calls_made = []

        def spy_rdp(domain, conf, correct):
            calls_made.append((domain, conf, correct))

        with patch("pulse.core.brain_utils.load_json_dict",
                   return_value=self._alerts_with_one_match("task-x", domain="api", outcome="false_positive")), \
             patch("pulse.core.brain_utils.atomic_update_json",
                   side_effect=lambda p, fn, default=None: fn(self._alerts_with_one_match("task-x", domain="api"))), \
             patch("pulse.brain.metacognition.prediction_calibration.record_domain_prediction", side_effect=spy_rdp), \
             patch("pulse.brain.metacognition.metamemory._record_source_outcome"), \
             patch("pulse.core.reward.reward_signal.record_task_outcome", side_effect=Exception("no reward")):
            prediction_feedback.record_prediction_outcome("task-x", success=True, agent_name="claude_1")

        assert len(calls_made) >= 0  # May not call if alert not matched post-update (fail-open)

    def test_record_domain_prediction_not_called_on_no_match(self):
        """record_domain_prediction must not be called when no alert matches."""
        from pulse.daemons.neural import prediction_feedback
        calls_made = []

        def spy_rdp(domain, conf, correct):
            calls_made.append((domain, conf, correct))

        empty_alerts = {"alerts": [], "total_generated": 0}

        with patch("pulse.core.brain_utils.load_json_dict", return_value=empty_alerts), \
             patch("pulse.core.brain_utils.atomic_update_json",
                   side_effect=lambda p, fn, default=None: fn(empty_alerts)), \
             patch("pulse.brain.metacognition.prediction_calibration.record_domain_prediction", side_effect=spy_rdp), \
             patch("pulse.brain.metacognition.metamemory._record_source_outcome"):
            prediction_feedback.record_prediction_outcome("no-such-task", success=True)

        assert len(calls_made) == 0

    def test_prediction_feedback_survives_calibration_exception(self):
        """record_prediction_outcome must not raise even if calibration crashes."""
        from pulse.daemons.neural import prediction_feedback
        empty_alerts = {"alerts": [], "total_generated": 0}

        with patch("pulse.core.brain_utils.load_json_dict", return_value=empty_alerts), \
             patch("pulse.core.brain_utils.atomic_update_json",
                   side_effect=lambda p, fn, default=None: fn(empty_alerts)), \
             patch("pulse.brain.metacognition.prediction_calibration.record_domain_prediction",
                   side_effect=RuntimeError("crash")), \
             patch("pulse.brain.metacognition.metamemory._record_source_outcome"):
            # Must not raise
            result = prediction_feedback.record_prediction_outcome("task-y", success=False)
        assert isinstance(result, bool)

    def test_domain_prediction_accumulates_in_state(self):
        """record_domain_prediction in prediction_calibration accumulates data."""
        from pulse.brain import prediction_calibration
        prediction_calibration._DOMAIN_CALIBRATION.clear()
        for _ in range(6):
            prediction_calibration.record_domain_prediction("brain", 0.8, True)
        result = prediction_calibration.get_domain_calibration()
        assert "brain" in result["domains"]
        assert result["domains"]["brain"]["count"] == 6
        assert result["domains"]["brain"]["status"] != "insufficient_data"

    def test_overconfident_domain_detected(self):
        """Calibration flags overconfident domains when avg_predicted >> avg_actual."""
        from pulse.brain import prediction_calibration
        prediction_calibration._DOMAIN_CALIBRATION.clear()
        # High confidence predictions that were wrong (actual=0)
        for _ in range(6):
            prediction_calibration.record_domain_prediction("security", 0.9, False)
        result = prediction_calibration.get_domain_calibration()
        assert "security" in result["overconfident_domains"]


# ---------------------------------------------------------------------------
# Item 4: get_consolidation_debt wired into get_brain_health_report
# ---------------------------------------------------------------------------
class TestItem4ConsolidationDebtWiredIntoBrainHealthReport:

    def test_consolidation_debt_key_present_in_report(self):
        """get_brain_health_report must include consolidation_debt key."""
        from pulse.brain.monitoring.brain_self_assessment import get_brain_health_report
        with patch("pulse.core.brain_db.get_conn", side_effect=Exception("no db")), \
             patch("pulse.brain.metacognition.metamemory.get_brain_utilization", return_value={"utilization_rate": 0.5}), \
             patch("pulse.brain.monitoring.brain_growth_monitor.get_growth_rate",
                   return_value={"avg_per_day": 10, "warning": None}):
            report = get_brain_health_report()
        assert "consolidation_debt" in report

    def test_consolidation_debt_has_correct_structure(self):
        """consolidation_debt must be a dict with inserted_since_last_run key."""
        from pulse.brain.monitoring.brain_self_assessment import get_brain_health_report
        with patch("pulse.core.brain_db.get_conn", side_effect=Exception("no db")), \
             patch("pulse.brain.metacognition.metamemory.get_brain_utilization", return_value={"utilization_rate": 0.5}):
            report = get_brain_health_report()
        debt = report.get("consolidation_debt")
        assert isinstance(debt, dict)
        assert "inserted_since_last_run" in debt
        assert "high_debt" in debt

    def test_high_debt_flagged_when_above_threshold(self):
        """high_debt must be True when inserted_since_last_run > debt_threshold."""
        from pulse.brain import metamemory_tracking
        from pulse.brain.monitoring.brain_self_assessment import get_brain_health_report
        # Set debt above threshold
        metamemory_tracking._CONSOLIDATION_DEBT["inserted_since_last_run"] = 600
        metamemory_tracking._CONSOLIDATION_DEBT["debt_threshold"] = 500

        with patch("pulse.core.brain_db.get_conn", side_effect=Exception("no db")):
            report = get_brain_health_report()

        assert report["consolidation_debt"]["high_debt"] is True
        # Reset
        metamemory_tracking._CONSOLIDATION_DEBT["inserted_since_last_run"] = 0

    def test_low_debt_not_flagged(self):
        """high_debt must be False when inserted_since_last_run <= threshold."""
        from pulse.brain import metamemory_tracking
        from pulse.brain.monitoring.brain_self_assessment import get_brain_health_report
        metamemory_tracking._CONSOLIDATION_DEBT["inserted_since_last_run"] = 10
        metamemory_tracking._CONSOLIDATION_DEBT["debt_threshold"] = 500

        with patch("pulse.core.brain_db.get_conn", side_effect=Exception("no db")):
            report = get_brain_health_report()

        assert report["consolidation_debt"]["high_debt"] is False

    def test_report_survives_debt_exception(self):
        """get_brain_health_report must not raise even if get_consolidation_debt crashes."""
        from pulse.brain.monitoring.brain_self_assessment import get_brain_health_report
        with patch("pulse.brain.metacognition.metamemory_tracking.get_consolidation_debt",
                   side_effect=RuntimeError("debt crash")), \
             patch("pulse.core.brain_db.get_conn", side_effect=Exception("no db")):
            report = get_brain_health_report()
        # Key might be absent (fail-open) or present — report itself must not raise
        assert "timestamp" in report

    def test_debt_increments_via_tracking_module(self):
        """increment_consolidation_debt must raise debt counter."""
        from pulse.brain import metamemory_tracking
        metamemory_tracking._CONSOLIDATION_DEBT["inserted_since_last_run"] = 0
        metamemory_tracking.increment_consolidation_debt(5)
        debt = metamemory_tracking.get_consolidation_debt()
        assert debt["inserted_since_last_run"] == 5
        # Reset
        metamemory_tracking._CONSOLIDATION_DEBT["inserted_since_last_run"] = 0

    def test_debt_resets_via_tracking_module(self):
        """reset_consolidation_debt must zero out the counter."""
        from pulse.brain import metamemory_tracking
        metamemory_tracking._CONSOLIDATION_DEBT["inserted_since_last_run"] = 200
        metamemory_tracking.reset_consolidation_debt("2026-03-21T00:00:00+00:00")
        debt = metamemory_tracking.get_consolidation_debt()
        assert debt["inserted_since_last_run"] == 0
        assert debt["last_consolidation_ts"] == "2026-03-21T00:00:00+00:00"
