#!/usr/bin/env python3
"""Tests for Phase 0 (Enums + Event Bus + Reconsolidation), Phase 1 (Reward Signal),
and Phase 2 (Attention + Metacognitive Control)."""
import json
import pytest
from unittest.mock import patch, MagicMock


# ============================================================
# Phase 0: Enums
# ============================================================

class TestEnums:
    def test_knowledge_type_string_equality(self):
        from pulse.core.enums import KnowledgeType
        assert KnowledgeType.LESSON == "lesson"
        assert KnowledgeType.FAILURE == "failure"
        assert KnowledgeType.PATTERN == "pattern"

    def test_knowledge_type_json_serializable(self):
        from pulse.core.enums import KnowledgeType
        result = json.dumps({"type": KnowledgeType.LESSON})
        assert '"lesson"' in result

    def test_validity_string_equality(self):
        from pulse.core.enums import Validity
        assert Validity.VALID == "valid"
        assert Validity.CONTESTED == "contested"
        assert Validity.SUPERSEDED == "superseded"

    def test_task_status_values(self):
        from pulse.core.enums import TaskStatus, TERMINAL_TASK_STATES, ACTIVE_TASK_STATES
        assert TaskStatus.OPEN == "open"
        assert TaskStatus.DONE in TERMINAL_TASK_STATES
        assert TaskStatus.OPEN in ACTIVE_TASK_STATES
        assert TaskStatus.DONE not in ACTIVE_TASK_STATES

    def test_validate_knowledge_type_valid(self):
        from pulse.core.enums import validate_knowledge_type
        assert validate_knowledge_type("lesson") == "lesson"
        assert validate_knowledge_type("failure") == "failure"

    def test_validate_knowledge_type_invalid(self):
        from pulse.core.enums import validate_knowledge_type
        with pytest.raises(ValueError, match="Invalid knowledge type"):
            validate_knowledge_type("bogus")

    def test_validate_validity_valid(self):
        from pulse.core.enums import validate_validity
        assert validate_validity("valid") == "valid"
        assert validate_validity("contested") == "contested"

    def test_validate_validity_invalid(self):
        from pulse.core.enums import validate_validity
        with pytest.raises(ValueError, match="Invalid validity state"):
            validate_validity("nonsense")

    def test_is_valid_transition(self):
        from pulse.core.enums import is_valid_transition
        assert is_valid_transition("valid", "superseded") is True
        assert is_valid_transition("valid", "contested") is True
        assert is_valid_transition("historical", "valid") is False
        assert is_valid_transition("expired", "historical") is True

    def test_knowledge_type_prefix_map(self):
        from pulse.core.enums import KnowledgeType, KNOWLEDGE_TYPE_PREFIX
        assert KNOWLEDGE_TYPE_PREFIX[KnowledgeType.LESSON] == "L"
        assert KNOWLEDGE_TYPE_PREFIX[KnowledgeType.FAILURE] == "F"
        assert KNOWLEDGE_TYPE_PREFIX[KnowledgeType.PATTERN] == "P"

    def test_competence_level_values(self):
        from pulse.core.enums import CompetenceLevel
        assert CompetenceLevel.BLIND_SPOT == "blind_spot"
        assert CompetenceLevel.EXPERT == "expert"

    def test_agent_tier_values(self):
        from pulse.core.enums import AgentTier
        assert AgentTier.FAST == "fast"
        assert AgentTier.PREMIUM == "premium"

    def test_event_channel_constants(self):
        from pulse.core.enums import EventChannel
        assert EventChannel.MEMORY_REINFORCE == "memory.reconsolidation.reinforce"
        assert EventChannel.SENTINEL_ALERT == "sentinel.alert"


# ============================================================
# Phase 0: Event Bus Subscribers
# ============================================================

class TestEventSubscribers:
    def test_wire_all_subscribers_returns_count(self):
        from pulse.core import event_subscribers
        # Reset the wired flag for testing
        event_subscribers._wired = False
        count = event_subscribers.wire_all_subscribers()
        assert count >= 8  # At least 8 subscribers wired

    def test_wire_all_subscribers_idempotent(self):
        from pulse.core import event_subscribers
        event_subscribers._wired = False
        count1 = event_subscribers.wire_all_subscribers()
        count2 = event_subscribers.wire_all_subscribers()
        assert count1 > 0
        assert count2 == 0  # Second call should return 0 (already wired)

    def test_subscriber_callbacks_are_defensive(self):
        from pulse.core.event_subscribers import (
            _on_sentinel_alert, _on_conscience_drift,
            _on_interoception_alert, _on_pattern_evolved,
        )
        # All should handle empty/malformed events without raising
        for fn in [_on_sentinel_alert, _on_conscience_drift,
                   _on_interoception_alert, _on_pattern_evolved]:
            fn({})  # Should not raise
            fn({"payload": {}})  # Should not raise


# ============================================================
# Phase 0: Reconsolidation SQL Injection Prevention
# ============================================================

class TestReconsolidationSecurity:
    def test_valid_tables_whitelist(self):
        from pulse.brain.plasticity.reconsolidation import _VALID_TABLES
        assert "lessons" in _VALID_TABLES
        assert "failures" in _VALID_TABLES
        assert "patterns" in _VALID_TABLES
        assert len(_VALID_TABLES) >= 7

    def test_mark_labile_rejects_invalid_table(self):
        from pulse.brain.plasticity.reconsolidation import mark_labile
        # Should silently reject — no exception, no state change
        mark_labile("test_id", "DROP TABLE lessons; --")
        mark_labile("test_id", "malicious_table")


# ============================================================
# Phase 0: Epistemic Qualifiers
# ============================================================

class TestEpistemicQualifiers:
    def test_low_domain_conf_qualifier(self):
        from pulse.brain.workspace.epistemic import apply_epistemic_qualifiers
        item = {
            "title": "test item",
            "confidence": 0.6,
            "_wilson_lower": 0.5,
            "_low_confidence_domain": True,
            "_domain_competence": "blind_spot",
            "timestamp": "2026-03-13T00:00:00+00:00",
        }
        result = apply_epistemic_qualifiers(item)
        assert "[LOW DOMAIN CONF]" in result["title"]

    def test_no_qualifier_for_expert_domain(self):
        from pulse.brain.workspace.epistemic import apply_epistemic_qualifiers
        item = {
            "title": "expert item",
            "confidence": 0.8,
            "_wilson_lower": 0.7,
            "_low_confidence_domain": False,
            "_domain_competence": "expert",
            "timestamp": "2026-03-13T00:00:00+00:00",
        }
        result = apply_epistemic_qualifiers(item)
        assert "[LOW DOMAIN CONF]" not in result["title"]


# ============================================================
# Phase 1: Reward Signal
# ============================================================

class TestRewardSignal:
    def test_compute_rpe_surprising_success(self):
        from pulse.core.reward.reward_signal import compute_rpe
        rpe = compute_rpe(success=True, expected_success_rate=0.3)
        assert rpe > 0.5  # Big positive surprise

    def test_compute_rpe_surprising_failure(self):
        from pulse.core.reward.reward_signal import compute_rpe
        rpe = compute_rpe(success=False, expected_success_rate=0.8)
        assert rpe < -0.5  # Big negative surprise

    def test_compute_rpe_expected_outcome(self):
        from pulse.core.reward.reward_signal import compute_rpe
        rpe = compute_rpe(success=True, expected_success_rate=0.95)
        assert abs(rpe) < 0.1  # No surprise

    def test_assign_credit_empty(self):
        from pulse.core.reward.reward_signal import assign_credit
        assert assign_credit([], 0.5) == {}
        assert assign_credit(["L-abc"], 0.0) == {}

    def test_assign_credit_decays(self):
        from pulse.core.reward.reward_signal import assign_credit
        credits = assign_credit(["L-1", "L-2", "L-3"], 1.0)
        assert len(credits) == 3
        # First item gets most credit
        assert credits["L-1"] > credits["L-2"] > credits["L-3"]

    def test_assign_credit_negative(self):
        from pulse.core.reward.reward_signal import assign_credit
        credits = assign_credit(["L-1", "L-2"], -0.8)
        assert credits["L-1"] < 0
        assert credits["L-2"] < 0

    def test_propagate_reward_no_items(self):
        from pulse.core.reward.reward_signal import propagate_reward
        assert propagate_reward({}) == 0


# ============================================================
# Phase 2: Attention System
# ============================================================

class TestAttention:
    def test_get_attention_weights_debug(self):
        from pulse.brain.workspace.attention import get_attention_weights
        weights = get_attention_weights("debug")
        assert weights["fails"] > 1.0
        assert weights["anti_patterns"] > 1.0

    def test_get_attention_weights_design(self):
        from pulse.brain.workspace.attention import get_attention_weights
        weights = get_attention_weights("design")
        assert weights["schemas"] > 1.0
        assert weights["graph"] > 1.0

    def test_get_attention_weights_general(self):
        from pulse.brain.workspace.attention import get_attention_weights
        weights = get_attention_weights("general")
        assert all(v == 1.0 for v in weights.values())

    def test_apply_attention_to_results(self):
        from pulse.brain.workspace.attention import apply_attention_to_results
        results = {
            "results": {
                "fails": [
                    {"salience": 2.0, "confidence": 0.6},
                    {"salience": 1.0, "confidence": 0.4},
                ]
            }
        }
        apply_attention_to_results(results, "debug")
        hits = results["results"]["fails"]
        assert hits[0].get("_attention_salience", 0) > 2.0  # Boosted
        assert hits[0].get("_attention_confidence", 0) > 0.6


# ============================================================
# Phase 2: Metacognitive Control
# ============================================================

class TestMetacognitiveControl:
    def test_infer_domain_database(self):
        from pulse.brain.metacognition.metacognitive_control import infer_domain
        assert infer_domain("fix the sql migration query") == "database"

    def test_infer_domain_security(self):
        from pulse.brain.metacognition.metacognitive_control import infer_domain
        assert infer_domain("check auth token encryption") == "security"

    def test_infer_domain_brain(self):
        from pulse.brain.metacognition.metacognitive_control import infer_domain
        assert infer_domain("consolidation of brain knowledge") == "brain"

    def test_infer_domain_general(self):
        from pulse.brain.metacognition.metacognitive_control import infer_domain
        assert infer_domain("hello world") == "general"

    def test_get_search_directive_defaults(self):
        from pulse.brain.metacognition.metacognitive_control import get_search_directive
        d = get_search_directive("fix the database migration")
        assert d.domain in ("database", "general")
        assert d.max_per_source >= 3
        assert d.confidence_gate > 0
        assert isinstance(d.search_exhaustive, bool)

    def test_directive_competence_levels(self):
        from pulse.brain.metacognition.metacognitive_control import SearchDirective
        # Verify blind_spot directive is most aggressive
        blind = SearchDirective("test", "blind_spot", 5, 0.2, True, True)
        expert = SearchDirective("test", "expert", 3, 0.5, False, False)
        assert blind.max_per_source > expert.max_per_source
        assert blind.confidence_gate < expert.confidence_gate


# ============================================================
# Integration: brain_query explain mode
# ============================================================

class TestBrainQueryExplain:
    def test_format_explain_output(self):
        from pulse.brain.brain_query import _format_explain
        results = {
            "total_results": 2,
            "elapsed_ms": 100,
            "results": {
                "wins": [
                    {"content": "test item", "confidence": 0.8, "salience": 3.0,
                     "_wilson_lower": 0.6, "domain": "testing"},
                ]
            }
        }
        output = _format_explain("test query", results)
        assert "[BRAIN EXPLAIN]" in output
        assert "test query" in output
        assert "WINS" in output
        assert "conf=" in output
        assert "sal=" in output


# ============================================================
# Integration: Capture log truncation fix
# ============================================================

class TestCaptureLogFix:
    def test_claude_clean_text_limit_raised(self):
        from pulse.agents.pulse_claude import clean_text
        long_text = "x" * 12000
        result = clean_text(long_text)
        assert len(result) > 10000  # Should not be truncated at 5000

    def test_codex_clean_text_limit_raised(self):
        from pulse.agents.pulse_codex import clean_text
        long_text = "x" * 12000
        result = clean_text(long_text)
        assert len(result) > 10000
