"""Integration tests: verify 4 dead-code items are wired into production.

Wire S1-1: apply_stdp_with_annealing wired into reward_signal (replaces bare apply_stdp)
Wire S1-2: _record_agent_domain wired into record_task_outcome (agent specialization)
Wire S1-3: record_knowledge_type_outcome wired into record_task_outcome (meta-learning)
Wire S1-4: increment_regulation_count wired into propagate_reward (emotional regulation tracking)
"""
import inspect
import sys
from pathlib import Path
from unittest.mock import patch, MagicMock, call

import pytest

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


# ---------------------------------------------------------------------------
# Wire S1-1: apply_stdp_with_annealing wired into reward_signal
# ---------------------------------------------------------------------------

class TestWireS1_1_StdpAnnealing:
    def test_reward_signal_references_stdp_with_annealing(self):
        """record_task_outcome source must reference apply_stdp_with_annealing."""
        from pulse.core.reward import reward_signal
        src = inspect.getsource(reward_signal.record_task_outcome)
        assert "apply_stdp_with_annealing" in src, (
            "Wire S1-1 missing: record_task_outcome does not call apply_stdp_with_annealing"
        )

    def test_stdp_annealing_import_path_correct(self):
        """Import must come from pulse.brain.stdp_annealing."""
        from pulse.core.reward import reward_signal
        src = inspect.getsource(reward_signal.record_task_outcome)
        assert "pulse.brain.stdp_annealing" in src, (
            "Wire S1-1: wrong import path — expected pulse.brain.stdp_annealing"
        )

    def test_stdp_annealing_called_with_domain(self):
        """apply_stdp_with_annealing must be called with domain keyword argument."""
        from pulse.core.reward import reward_signal
        src = inspect.getsource(reward_signal.record_task_outcome)
        assert "domain=domain" in src, (
            "Wire S1-1: apply_stdp_with_annealing must pass domain=domain for maturity annealing"
        )

    def test_stdp_annealing_has_fallback_to_bare_apply_stdp(self):
        """Fallback to bare apply_stdp must be present in case stdp_annealing import fails."""
        from pulse.core.reward import reward_signal
        src = inspect.getsource(reward_signal.record_task_outcome)
        # Annealing call must be followed by an except block that falls back
        anneal_idx = src.find("apply_stdp_with_annealing")
        except_idx = src.find("except", anneal_idx)
        fallback_idx = src.find("apply_stdp", except_idx)
        assert anneal_idx != -1, "apply_stdp_with_annealing not found"
        assert except_idx != -1, "No except block after apply_stdp_with_annealing"
        assert fallback_idx != -1, (
            "Wire S1-1: no fallback apply_stdp() after except block — "
            "if stdp_annealing import fails, STDP fires no update at all"
        )

    def test_apply_stdp_with_annealing_callable(self):
        """apply_stdp_with_annealing must be importable and callable."""
        from pulse.brain.plasticity.stdp_annealing import apply_stdp_with_annealing
        assert callable(apply_stdp_with_annealing), (
            "Wire S1-1: apply_stdp_with_annealing is not callable"
        )

    def test_apply_stdp_with_annealing_accepts_domain_kwarg(self):
        """Function signature must accept domain keyword argument."""
        from pulse.brain.plasticity.stdp_annealing import apply_stdp_with_annealing
        sig = inspect.signature(apply_stdp_with_annealing)
        assert "domain" in sig.parameters, (
            "Wire S1-1: apply_stdp_with_annealing missing 'domain' parameter"
        )

    def test_apply_stdp_with_annealing_invoked_during_record_task_outcome(self):
        """record_task_outcome must actually call apply_stdp_with_annealing at runtime."""
        from pulse.core.reward import reward_signal

        call_log = []

        def fake_annealing(session_id, success, domain=""):
            call_log.append((session_id, success, domain))
            return 0

        with patch("pulse.brain.stdp_annealing.apply_stdp_with_annealing", fake_annealing), \
             patch("pulse.core.reward.reward_signal.propagate_reward", return_value=0), \
             patch("pulse.core.reward.reward_signal._update_credit_ledger"), \
             patch("pulse.core.reward.reward_signal.REWARD_SIGNALS_FILE"), \
             patch("pulse.core.reward.reward_signal._REWARD_DIR") as mock_dir:
            mock_dir.mkdir = MagicMock()
            # Patch file I/O used in signal logging
            with patch("pulse.core.reward.reward_signal.assign_credit", return_value={}), \
                 patch("pulse.core.reward.reward_signal._get_expected_success_rate", return_value=0.5), \
                 patch("pulse.core.brain_io._acquire", side_effect=Exception("skip io")):
                try:
                    reward_signal.record_task_outcome(
                        task_id="t-test-001",
                        agent="test-agent",
                        success=True,
                        domain="testing",
                    )
                except Exception:
                    pass  # IO patching may raise — that's fine

        assert len(call_log) >= 1, (
            "Wire S1-1: apply_stdp_with_annealing was never called during record_task_outcome"
        )
        _, _, called_domain = call_log[0]
        assert called_domain == "testing", (
            f"Wire S1-1: apply_stdp_with_annealing called with domain={called_domain!r}, expected 'testing'"
        )


# ---------------------------------------------------------------------------
# Wire S1-2: _record_agent_domain wired into record_task_outcome
# ---------------------------------------------------------------------------

class TestWireS1_2_AgentDomain:
    def test_record_task_outcome_references_record_agent_domain(self):
        """record_task_outcome source must reference _record_agent_domain."""
        from pulse.core.reward import reward_signal
        src = inspect.getsource(reward_signal.record_task_outcome)
        assert "_record_agent_domain" in src, (
            "Wire S1-2 missing: record_task_outcome does not call _record_agent_domain"
        )

    def test_record_agent_domain_import_path(self):
        """Import must come from pulse.brain.metacognition.metamemory_tracking."""
        from pulse.core.reward import reward_signal
        src = inspect.getsource(reward_signal.record_task_outcome)
        assert "pulse.brain.metacognition.metamemory_tracking" in src, (
            "Wire S1-2: _record_agent_domain must be imported from pulse.brain.metacognition.metamemory_tracking"
        )

    def test_record_agent_domain_callable(self):
        """_record_agent_domain must be importable and callable."""
        from pulse.brain.metacognition.metamemory_tracking import _record_agent_domain
        assert callable(_record_agent_domain), (
            "Wire S1-2: _record_agent_domain is not callable"
        )

    def test_record_agent_domain_accepts_correct_args(self):
        """_record_agent_domain(agent, domain, success) must not raise."""
        from pulse.brain.metacognition.metamemory_tracking import _record_agent_domain, _AGENT_SPECIALIZATION
        _record_agent_domain("test-agent", "testing", True)
        spec = _AGENT_SPECIALIZATION.get("test-agent", {})
        assert "testing" in spec, (
            "Wire S1-2: _record_agent_domain did not update _AGENT_SPECIALIZATION"
        )

    def test_get_agent_specialization_returns_data_after_recording(self):
        """get_agent_specialization() must reflect calls to _record_agent_domain."""
        from pulse.brain.metacognition.metamemory_tracking import _record_agent_domain, get_agent_specialization
        _record_agent_domain("spec-agent", "brain", True)
        _record_agent_domain("spec-agent", "brain", False)
        result = get_agent_specialization()
        assert "spec-agent" in result, (
            "Wire S1-2: get_agent_specialization returns empty after _record_agent_domain calls"
        )
        assert "brain" in result["spec-agent"], (
            "Wire S1-2: domain 'brain' missing from specialization map after recording"
        )
        brain = result["spec-agent"]["brain"]
        assert brain["tasks"] == 2, f"Expected 2 tasks, got {brain['tasks']}"
        assert brain["successes"] == 1, f"Expected 1 success, got {brain['successes']}"
        assert brain["success_rate"] == 0.5, f"Expected 0.5 rate, got {brain['success_rate']}"

    def test_record_agent_domain_called_with_domain_in_record_task_outcome(self):
        """record_task_outcome must pass the domain arg to _record_agent_domain."""
        from pulse.core.reward import reward_signal
        src = inspect.getsource(reward_signal.record_task_outcome)
        # After the import, the call should pass agent, domain, success
        domain_idx = src.find("_record_agent_domain(agent")
        assert domain_idx != -1, (
            "Wire S1-2: _record_agent_domain must be called with agent as first arg"
        )
        snippet = src[domain_idx:domain_idx + 80]
        assert "domain" in snippet, (
            f"Wire S1-2: _record_agent_domain call missing domain argument: {snippet!r}"
        )


# ---------------------------------------------------------------------------
# Wire S1-3: record_knowledge_type_outcome wired into record_task_outcome
# ---------------------------------------------------------------------------

class TestWireS1_3_KnowledgeTypeOutcome:
    def test_record_task_outcome_references_record_knowledge_type_outcome(self):
        """record_task_outcome source must reference record_knowledge_type_outcome."""
        from pulse.core.reward import reward_signal
        src = inspect.getsource(reward_signal.record_task_outcome)
        assert "record_knowledge_type_outcome" in src, (
            "Wire S1-3 missing: record_task_outcome does not call record_knowledge_type_outcome"
        )

    def test_knowledge_type_outcome_import_from_metamemory(self):
        """Import must come from pulse.brain.metacognition.metamemory."""
        from pulse.core.reward import reward_signal
        src = inspect.getsource(reward_signal.record_task_outcome)
        # Find the import adjacent to record_knowledge_type_outcome
        idx = src.find("record_knowledge_type_outcome")
        # Search backwards for the import statement
        block = src[max(0, idx - 100):idx + 100]
        assert "pulse.brain.metacognition.metamemory" in block, (
            "Wire S1-3: record_knowledge_type_outcome must be imported from pulse.brain.metacognition.metamemory"
        )

    def test_record_knowledge_type_outcome_callable(self):
        """record_knowledge_type_outcome must be importable and callable."""
        from pulse.brain.metacognition.metamemory import record_knowledge_type_outcome
        assert callable(record_knowledge_type_outcome), (
            "Wire S1-3: record_knowledge_type_outcome is not callable"
        )

    def test_record_knowledge_type_outcome_updates_stats(self):
        """After calls, get_knowledge_type_priority must reflect recorded outcomes."""
        from pulse.brain.metacognition.metamemory import (
            _record_knowledge_type_outcome,
            get_knowledge_type_priority,
            _KNOWLEDGE_TYPE_SUCCESS,
        )
        # Seed with known data to avoid interference from other tests
        _KNOWLEDGE_TYPE_SUCCESS["wire_s1_test"] = {"hits": 0, "successes": 0}
        _record_knowledge_type_outcome("wire_s1_test", True)
        _record_knowledge_type_outcome("wire_s1_test", True)
        _record_knowledge_type_outcome("wire_s1_test", False)

        priority = get_knowledge_type_priority()
        # 3 hits → enough for a real rate
        assert "wire_s1_test" in priority, (
            "Wire S1-3: get_knowledge_type_priority missing 'wire_s1_test' after 3 records"
        )
        rate = priority["wire_s1_test"]
        assert abs(rate - (2 / 3)) < 0.01, (
            f"Wire S1-3: expected success rate ~0.667, got {rate}"
        )

    def test_knowledge_type_called_per_retrieved_item(self):
        """record_task_outcome must call record_knowledge_type_outcome for each retrieved item (up to 5)."""
        from pulse.core.reward import reward_signal
        src = inspect.getsource(reward_signal.record_task_outcome)
        # The call should be inside a loop over retrieved_item_ids[:5]
        assert "retrieved_item_ids[:5]" in src, (
            "Wire S1-3: record_knowledge_type_outcome must be called per item (loop over retrieved_item_ids[:5])"
        )

    def test_knowledge_type_outcome_guarded_by_try_except(self):
        """record_knowledge_type_outcome call must be wrapped in try/except (fail-open)."""
        from pulse.core.reward import reward_signal
        src = inspect.getsource(reward_signal.record_task_outcome)
        idx = src.find("record_knowledge_type_outcome")
        # Look for a try: before the call
        try_idx = src.rfind("try:", 0, idx)
        except_idx = src.find("except", idx)
        assert try_idx != -1 and except_idx != -1, (
            "Wire S1-3: record_knowledge_type_outcome must be wrapped in try/except for fail-open"
        )


# ---------------------------------------------------------------------------
# Wire S1-4: increment_regulation_count wired into propagate_reward
# ---------------------------------------------------------------------------

class TestWireS1_4_RegulationCount:
    def test_propagate_reward_module_references_increment_regulation_count(self):
        """reward_propagation module source must reference increment_regulation_count."""
        from pulse.core.reward import reward_propagation
        src = inspect.getsource(reward_propagation)
        assert "increment_regulation_count" in src, (
            "Wire S1-4 missing: reward_propagation does not call increment_regulation_count"
        )

    def test_increment_regulation_count_import_path(self):
        """Import must come from pulse.core.reward_regulation."""
        from pulse.core.reward import reward_propagation
        src = inspect.getsource(reward_propagation)
        assert "pulse.core.reward_regulation" in src, (
            "Wire S1-4: increment_regulation_count must be imported from pulse.core.reward_regulation"
        )

    def test_increment_regulation_count_callable(self):
        """increment_regulation_count must be importable and callable."""
        from pulse.core.reward.reward_regulation import increment_regulation_count
        assert callable(increment_regulation_count), (
            "Wire S1-4: increment_regulation_count is not callable"
        )

    def test_increment_regulation_count_increments_counter(self):
        """Calling increment_regulation_count must increase the counter."""
        from pulse.core.reward import reward_regulation
        reward_regulation.reset_regulation_count()
        before = reward_regulation.get_regulation_count()
        reward_regulation.increment_regulation_count()
        after = reward_regulation.get_regulation_count()
        assert after == before + 1, (
            f"Wire S1-4: counter did not increment — before={before}, after={after}"
        )

    def test_increment_regulation_count_inside_diminishing_returns_block(self):
        """The call must be inside the >6.0 diminishing returns block in reward_propagation."""
        from pulse.core.reward import reward_propagation
        src = inspect.getsource(reward_propagation)
        # Find the diminishing returns comment
        dr_idx = src.find("Diminishing returns")
        assert dr_idx != -1, "Cannot find diminishing returns block in reward_propagation"
        # increment_regulation_count must appear after the diminishing returns block
        inc_idx = src.find("increment_regulation_count", dr_idx)
        assert inc_idx != -1, (
            "Wire S1-4: increment_regulation_count must appear AFTER the >6.0 diminishing returns block"
        )

    def test_increment_regulation_count_guarded_by_try_except(self):
        """increment_regulation_count must be wrapped in try/except (fail-open)."""
        from pulse.core.reward import reward_propagation
        src = inspect.getsource(reward_propagation)
        idx = src.find("increment_regulation_count")
        try_idx = src.rfind("try:", 0, idx)
        except_idx = src.find("except", idx)
        assert try_idx != -1 and except_idx != -1, (
            "Wire S1-4: increment_regulation_count must be wrapped in try/except for fail-open"
        )

    def test_regulation_status_returns_structured_dict(self):
        """get_regulation_status() must return dict with required keys."""
        from pulse.core.reward.reward_regulation import get_regulation_status
        status = get_regulation_status()
        assert isinstance(status, dict), "get_regulation_status must return a dict"
        for key in ("regulation_count", "threshold", "over_emotional", "warning"):
            assert key in status, f"get_regulation_status missing key: {key}"

    def test_regulation_count_fires_when_salience_exceeds_6(self):
        """When salience would exceed 6.0, increment_regulation_count must be called."""
        from pulse.core.reward import reward_regulation
        reward_regulation.reset_regulation_count()

        # Simulate the regulation cap firing by calling increment directly
        # (full DB integration test would require a real SQLite setup)
        reward_regulation.increment_regulation_count()
        reward_regulation.increment_regulation_count()

        count = reward_regulation.get_regulation_count()
        assert count == 2, f"Expected count=2 after two increments, got {count}"

    def test_over_emotional_alert_fires_at_threshold(self):
        """get_regulation_status must flag over_emotional=True at threshold."""
        from pulse.core.reward import reward_regulation
        reward_regulation.reset_regulation_count()
        threshold = reward_regulation._REGULATION_FIRE_THRESHOLD

        # Fire exactly at threshold
        for _ in range(threshold):
            reward_regulation.increment_regulation_count()

        status = reward_regulation.get_regulation_status()
        assert status["over_emotional"] is True, (
            f"Wire S1-4: over_emotional must be True when count={threshold} >= threshold={threshold}"
        )
        assert status["warning"] is not None, (
            "Wire S1-4: warning message must be set when over_emotional=True"
        )


# ---------------------------------------------------------------------------
# Cross-wire: reward_signal.propagate_reward delegates to reward_propagation
# ---------------------------------------------------------------------------

class TestPropagateRewardDelegation:
    def test_reward_signal_propagate_reward_delegates(self):
        """reward_signal.propagate_reward must delegate to reward_propagation module."""
        from pulse.core.reward import reward_signal
        src = inspect.getsource(reward_signal.propagate_reward)
        assert "reward_propagation" in src, (
            "reward_signal.propagate_reward must delegate to pulse.core.reward_propagation"
        )

    def test_propagate_reward_empty_credits_still_returns_zero(self):
        """Both delegation paths must handle empty credits correctly."""
        from pulse.core.reward.reward_signal import propagate_reward
        assert propagate_reward({}) == 0, (
            "propagate_reward({}) must return 0 regardless of delegation"
        )

    def test_reward_signal_line_count_under_300(self):
        """reward_signal.py must stay under 300 lines (modularization rule)."""
        import pulse.core.reward.reward_signal as rs_mod
        path = Path(rs_mod.__file__)
        lines = len(path.read_text(encoding="utf-8").splitlines())
        assert lines < 300, (
            f"reward_signal.py is {lines} lines — must be under 300 (modularize further)"
        )

    def test_reward_propagation_line_count_under_300(self):
        """reward_propagation.py must also stay under 300 lines."""
        import pulse.core.reward.reward_propagation as rp_mod
        path = Path(rp_mod.__file__)
        lines = len(path.read_text(encoding="utf-8").splitlines())
        assert lines < 300, (
            f"reward_propagation.py is {lines} lines — must be under 300"
        )
