"""
TDD: Deep Active Inference Integration Tests.

Tests the v2 Active Inference Engine behaviors:
- Consecutive error → policy escalation
- Interaction probability throttling
- Session abort recommendation
- Diagnostics reporting
- Backward compatibility with existing callers
"""

import time
from unittest.mock import patch

import pytest


@pytest.fixture
def ai():
    """Fresh Active Inference engine for each test."""
    from GramAddict.core.active_inference import ActiveInferenceEngine

    return ActiveInferenceEngine("test_user")


class TestPolicyEscalation:
    """Consecutive prediction errors must escalate the policy."""

    def test_single_error_stays_stable(self, ai):
        """One prediction error should not change policy from STABLE."""
        ai.predict_state(["feed_tab"])
        ai.evaluate_prediction("<hierarchy><node text='wrong'/></hierarchy>")
        # Free energy from single error: 0.0 * 0.7 + 1.0 * 0.3 = 0.3
        # 0.3 < 0.75, so still STABLE
        assert ai.policy == "STABLE"
        assert ai._consecutive_prediction_errors == 1

    def test_three_errors_goes_cautious(self, ai):
        """3 consecutive errors must trigger CAUTIOUS policy."""
        for _ in range(3):
            ai.predict_state(["nonexistent"])
            ai.evaluate_prediction("<hierarchy><node text='wrong'/></hierarchy>")

        assert ai.policy == "CAUTIOUS"
        assert ai._consecutive_prediction_errors == 3

    def test_five_errors_goes_dormant(self, ai):
        """5 consecutive errors must trigger DORMANT policy."""
        for _ in range(5):
            ai.predict_state(["nonexistent"])
            ai.evaluate_prediction("<hierarchy><node text='wrong'/></hierarchy>")

        assert ai.policy == "DORMANT"
        assert ai._consecutive_prediction_errors == 5

    def test_successful_prediction_resets_counter(self, ai):
        """A successful prediction must reset the consecutive error counter."""
        # Build up 3 errors
        for _ in range(3):
            ai.predict_state(["missing"])
            ai.evaluate_prediction("<hierarchy><node text='wrong'/></hierarchy>")

        assert ai._consecutive_prediction_errors == 3

        # Now succeed
        ai.predict_state(["feed_tab"])
        ai.evaluate_prediction('<hierarchy><node resource-id="feed_tab"/></hierarchy>')

        assert ai._consecutive_prediction_errors == 0

    def test_error_rate_tracking(self, ai):
        """Error rate must be accurately tracked across the session."""
        # 3 errors, 2 successes = 3/5 = 0.6
        for _ in range(3):
            ai.predict_state(["missing"])
            ai.evaluate_prediction("<hierarchy/>")
        for _ in range(2):
            ai.predict_state(["found"])
            ai.evaluate_prediction('<hierarchy><node text="found"/></hierarchy>')

        assert ai.get_error_rate() == pytest.approx(0.6)


class TestInteractionProbability:
    """Interaction probability must decrease under stress."""

    def test_stable_has_full_probability(self, ai):
        """STABLE policy → 100% interaction probability."""
        ai.policy = "STABLE"
        assert ai.get_interaction_probability() == 1.0

    def test_cautious_halves_probability(self, ai):
        """CAUTIOUS policy → 50% interaction probability."""
        ai.policy = "CAUTIOUS"
        assert ai.get_interaction_probability() == 0.5

    def test_dormant_minimal_probability(self, ai):
        """DORMANT policy → 10% interaction probability."""
        ai.policy = "DORMANT"
        assert ai.get_interaction_probability() == 0.1

    def test_probability_bounds(self, ai):
        """Interaction probability must always be in [0.0, 1.0]."""
        for policy in ["STABLE", "CAUTIOUS", "DORMANT"]:
            ai.policy = policy
            prob = ai.get_interaction_probability()
            assert 0.0 <= prob <= 1.0


class TestSessionAbort:
    """Session abort recommendation under extreme instability."""

    def test_no_abort_on_stable(self, ai):
        """STABLE engine should never recommend abort."""
        assert ai.should_abort_session() is False

    def test_abort_after_five_consecutive_errors(self, ai):
        """5 consecutive prediction errors must recommend abort."""
        for _ in range(5):
            ai.predict_state(["missing"])
            ai.evaluate_prediction("<hierarchy/>")

        assert ai.should_abort_session() is True

    def test_abort_on_extreme_free_energy(self, ai):
        """Free energy > 2.0 must recommend abort."""
        ai.free_energy = 2.1
        assert ai.should_abort_session() is True

    def test_no_abort_under_threshold(self, ai):
        """Free energy < 2.0 with few errors should not abort."""
        ai.free_energy = 1.9
        ai._consecutive_prediction_errors = 4
        assert ai.should_abort_session() is False


class TestDiagnostics:
    """Diagnostics must provide accurate runtime snapshot."""

    def test_diagnostics_has_required_fields(self, ai):
        """Diagnostics dict must contain all required fields."""
        diag = ai.get_diagnostics()
        required = [
            "free_energy",
            "policy",
            "consecutive_errors",
            "total_predictions",
            "total_errors",
            "error_rate",
            "session_uptime_minutes",
            "should_abort",
        ]
        for field in required:
            assert field in diag, f"Missing diagnostic field: {field}"

    def test_diagnostics_reflects_state(self, ai):
        """Diagnostics must accurately reflect engine state."""
        ai.predict_state(["test"])
        ai.evaluate_prediction("<wrong/>")

        diag = ai.get_diagnostics()
        assert diag["consecutive_errors"] == 1
        assert diag["total_predictions"] == 1
        assert diag["total_errors"] == 1
        assert diag["error_rate"] == 1.0
        assert diag["should_abort"] is False


class TestBackwardCompatibility:
    """Existing callers must work unchanged."""

    def test_get_sleep_modifier_unchanged(self, ai):
        """Sleep modifier values must match v1 behavior."""
        ai.policy = "STABLE"
        assert ai.get_sleep_modifier() == 1.0
        ai.policy = "CAUTIOUS"
        assert ai.get_sleep_modifier() == 2.0
        ai.policy = "DORMANT"
        assert ai.get_sleep_modifier() == 5.0

    def test_predict_then_evaluate_success(self, ai):
        """Basic predict → evaluate flow must work as before."""
        ai.predict_state(["row_feed", "button_like"])
        result = ai.evaluate_prediction(
            '<hierarchy><node resource-id="row_feed"/><node resource-id="button_like"/></hierarchy>'
        )
        assert result is True

    def test_predict_then_evaluate_failure(self, ai):
        """Failed prediction must still return False and fire Dojo."""
        ai.predict_state(["row_feed", "button_like"])

        with patch("GramAddict.core.dojo_engine.DojoEngine.get_instance") as mock_dojo:
            mock_dojo.return_value.submit_snapshot = lambda **kw: None
            result = ai.evaluate_prediction('<hierarchy><node text="camera"/></hierarchy>')

        assert result is False

    def test_evaluate_without_prediction_is_noop(self, ai):
        """Evaluating without a prior prediction must return True (no-op)."""
        result = ai.evaluate_prediction("<hierarchy/>")
        assert result is True
        assert ai._consecutive_prediction_errors == 0


class TestFreeEnergyDecay:
    """Free energy must decay over time (thermodynamic relaxation)."""

    def test_free_energy_decays_over_time(self, ai):
        """Free energy should reduce after time passes without new errors."""
        ai.free_energy = 1.5
        ai.last_update = time.time() - 7200  # 2 hours ago

        ai.calculate_surprise(1.0, 1.0)  # Perfect prediction

        # Decay: 1.5 * 0.7 + 0.0 * 0.3 = 1.05, then * exp(-0.1 * 2) ≈ 1.05 * 0.818 ≈ 0.86
        assert ai.free_energy < 1.0

    def test_free_energy_stabilizes_on_perfect_predictions(self, ai):
        """Repeated perfect predictions should drive free energy toward zero."""
        ai.free_energy = 1.0
        for _ in range(20):
            ai.calculate_surprise(1.0, 1.0)

        assert ai.free_energy < 0.05  # Near zero
