"""
Tests for insa_its/interventions/persistence.py - Persistence Engine (Phase 3.6-3.11)

Covers:
- Escalation levels 1-5
- Back-off logic (error type change, tool diversity)
- Task context isolation and agent isolation
- Timeout reset after 10 minutes
- Stealth mode visibility rules
- Circuit breaker at level 5
- Message content varies by level
"""

import time

import pytest

from insa_its.interventions.persistence import (
    LEVEL_CIRCUIT_BREAK,
    LEVEL_DECOMPOSE,
    LEVEL_ESCALATE,
    LEVEL_NUDGE,
    LEVEL_REDIRECT,
    MAX_LEVEL,
    RESET_TIMEOUT_SECONDS,
    EscalationAction,
    PersistenceEngine,
)


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _record_n_failures(
    engine: PersistenceEngine,
    n: int,
    agent_id: str = "agent-1",
    task_context: str = "task-1",
    error_type: str = "SameError",
    tool_name: str = "Bash",
) -> EscalationAction:
    """Record n identical failures and return the last action."""
    action = None
    for _ in range(n):
        action = engine.record_failure(
            agent_id=agent_id,
            task_context=task_context,
            error_type=error_type,
            tool_name=tool_name,
        )
    assert action is not None
    return action


# ---------------------------------------------------------------------------
# Escalation level progression
# ---------------------------------------------------------------------------

class TestEscalationLevels:
    """Test that failure count maps correctly to escalation levels."""

    def test_first_failure_returns_nudge(self):
        engine = PersistenceEngine()
        action = engine.record_failure("agent-1", "task-1", "TypeError", "Bash")
        assert action.level == LEVEL_NUDGE
        assert action.should_circuit_break is False
        assert action.should_log is True
        assert action.should_output is True  # non-stealth: always visible
        assert len(action.message) > 0

    def test_second_failure_returns_redirect(self):
        engine = PersistenceEngine()
        action = _record_n_failures(engine, 2)
        assert action.level == LEVEL_REDIRECT

    def test_escalation_to_decompose(self):
        engine = PersistenceEngine()
        action = _record_n_failures(engine, 3)
        assert action.level == LEVEL_DECOMPOSE

    def test_escalation_to_escalate(self):
        engine = PersistenceEngine()
        action = _record_n_failures(engine, 4)
        assert action.level == LEVEL_ESCALATE
        assert action.should_circuit_break is False

    def test_escalation_to_circuit_break(self):
        engine = PersistenceEngine()
        action = _record_n_failures(engine, 5)
        assert action.level == LEVEL_CIRCUIT_BREAK
        assert action.should_circuit_break is True

    def test_no_escalation_past_level_5(self):
        engine = PersistenceEngine()
        action = _record_n_failures(engine, 10)
        assert action.level == MAX_LEVEL
        assert action.level == LEVEL_CIRCUIT_BREAK
        assert action.should_circuit_break is True


# ---------------------------------------------------------------------------
# Back-off logic
# ---------------------------------------------------------------------------

class TestBackoff:
    """Test that agents making progress get reduced escalation."""

    def test_different_error_reduces_level(self):
        engine = PersistenceEngine()
        # 1st failure: TypeError -> level 1
        engine.record_failure("a", "t", "TypeError", "Bash")
        # 2nd failure: same error -> level 2
        engine.record_failure("a", "t", "TypeError", "Bash")
        # 3rd failure: DIFFERENT error -> should back off by 1
        action = engine.record_failure("a", "t", "KeyError", "Bash")
        # Raw level would be 3 (DECOMPOSE), but error changed -> 2 (REDIRECT)
        assert action.level == LEVEL_REDIRECT

    def test_multiple_tools_tried_reduces_level(self):
        engine = PersistenceEngine()
        # Build up failures
        engine.record_failure("a", "t", "Error", "Bash")
        engine.record_failure("a", "t", "Error", "Bash")
        engine.record_failure("a", "t", "Error", "Bash")
        # 4th failure but with 3 different tools since last surrender
        # Simulate by recording tools in tools_since_last_surrender
        # before the next record_failure call
        state = engine._states[("a", "t")]
        state.tools_since_last_surrender = {"Bash", "Read", "Grep"}
        action = engine.record_failure("a", "t", "Error", "Write")
        # 4 tools_since_last_surrender >= 3 threshold -> back to level 1
        assert action.level == LEVEL_NUDGE

    def test_same_error_no_backoff(self):
        engine = PersistenceEngine()
        engine.record_failure("a", "t", "TypeError", "Bash")
        action = engine.record_failure("a", "t", "TypeError", "Bash")
        # Same error, same tool: no back-off, level 2
        assert action.level == LEVEL_REDIRECT


# ---------------------------------------------------------------------------
# Task context and agent isolation
# ---------------------------------------------------------------------------

class TestIsolation:
    """Test that tracking is independent per agent and per task."""

    def test_task_context_change_resets(self):
        engine = PersistenceEngine()
        # Build up to level 3 on task-1
        _record_n_failures(engine, 3, task_context="task-1")
        # New task should start fresh
        action = engine.record_failure("agent-1", "task-2", "Error", "Bash")
        assert action.level == LEVEL_NUDGE

    def test_independent_tracking_per_agent(self):
        engine = PersistenceEngine()
        # Agent-1 at level 3
        _record_n_failures(engine, 3, agent_id="agent-1")
        # Agent-2 should start fresh
        action = engine.record_failure("agent-2", "task-1", "Error", "Bash")
        assert action.level == LEVEL_NUDGE
        # Agent-1 still at level 3
        assert engine.get_escalation_level("agent-1", "task-1") == LEVEL_DECOMPOSE

    def test_independent_tracking_per_task(self):
        engine = PersistenceEngine()
        _record_n_failures(engine, 4, agent_id="agent-1", task_context="task-A")
        _record_n_failures(engine, 2, agent_id="agent-1", task_context="task-B")
        assert engine.get_escalation_level("agent-1", "task-A") == LEVEL_ESCALATE
        assert engine.get_escalation_level("agent-1", "task-B") == LEVEL_REDIRECT


# ---------------------------------------------------------------------------
# Timeout reset
# ---------------------------------------------------------------------------

class TestTimeoutReset:
    """Test that escalation resets after 10 minutes of no failures."""

    def test_timeout_resets_after_10_minutes(self):
        engine = PersistenceEngine()
        _record_n_failures(engine, 4)  # level 4

        # Simulate 10+ minutes passing by manipulating the state timestamp
        state = engine._states[("agent-1", "task-1")]
        state.last_failure_ts = time.monotonic() - RESET_TIMEOUT_SECONDS - 1

        # Next failure should see the timeout and reset
        action = engine.record_failure("agent-1", "task-1", "Error", "Bash")
        assert action.level == LEVEL_NUDGE  # reset + 1 new failure

    def test_no_reset_before_timeout(self):
        engine = PersistenceEngine()
        _record_n_failures(engine, 3)

        # Only 1 second elapsed — should NOT reset
        state = engine._states[("agent-1", "task-1")]
        state.last_failure_ts = time.monotonic() - 1

        action = engine.record_failure("agent-1", "task-1", "SameError", "Bash")
        assert action.level == LEVEL_ESCALATE  # 4th failure, no reset

    def test_get_escalation_level_respects_timeout(self):
        engine = PersistenceEngine()
        _record_n_failures(engine, 3)
        assert engine.get_escalation_level("agent-1", "task-1") == LEVEL_DECOMPOSE

        # Simulate timeout
        state = engine._states[("agent-1", "task-1")]
        state.last_failure_ts = time.monotonic() - RESET_TIMEOUT_SECONDS - 1

        assert engine.get_escalation_level("agent-1", "task-1") == 0


# ---------------------------------------------------------------------------
# Stealth mode
# ---------------------------------------------------------------------------

class TestStealthMode:
    """Test stealth visibility rules for escalation output."""

    def test_stealth_mode_levels_1_3_invisible(self):
        _engine = PersistenceEngine(stealth=True)  # noqa: F841
        for i in range(1, 4):
            action = _record_n_failures(
                PersistenceEngine(stealth=True), i
            )
            assert action.should_output is False, (
                f"Level {action.level} should be invisible in stealth mode"
            )
            assert action.should_log is True

    def test_stealth_mode_levels_4_5_visible(self):
        engine = PersistenceEngine(stealth=True)
        # Level 4
        action = _record_n_failures(engine, 4)
        assert action.level == LEVEL_ESCALATE
        assert action.should_output is True

        # Level 5
        action = engine.record_failure("agent-1", "task-1", "SameError", "Bash")
        assert action.level == LEVEL_CIRCUIT_BREAK
        assert action.should_output is True

    def test_non_stealth_all_levels_visible(self):
        _engine = PersistenceEngine(stealth=False)  # noqa: F841
        for i in range(1, 6):
            action = _record_n_failures(
                PersistenceEngine(stealth=False), i
            )
            assert action.should_output is True, (
                f"Level {action.level} should be visible in non-stealth mode"
            )


# ---------------------------------------------------------------------------
# Intervention messages
# ---------------------------------------------------------------------------

class TestInterventionMessages:
    """Test that messages are distinct and context-aware."""

    def test_get_intervention_message_varies_by_level(self):
        engine = PersistenceEngine()
        messages = set()
        for level in range(1, 6):
            msg = engine.get_intervention_message(level)
            messages.add(msg)
        # All 5 levels should produce distinct messages
        assert len(messages) == 5

    def test_message_includes_task_context(self):
        engine = PersistenceEngine()
        msg = engine.get_intervention_message(LEVEL_NUDGE, "fix-login-bug")
        assert "fix-login-bug" in msg

    def test_message_without_task_context(self):
        engine = PersistenceEngine()
        msg = engine.get_intervention_message(LEVEL_NUDGE)
        assert len(msg) > 0
        # Should not contain placeholder artifacts
        assert "{task}" not in msg

    def test_circuit_break_message_contains_stop(self):
        engine = PersistenceEngine()
        msg = engine.get_intervention_message(LEVEL_CIRCUIT_BREAK)
        assert "STOP" in msg


# ---------------------------------------------------------------------------
# Reset
# ---------------------------------------------------------------------------

class TestReset:
    """Test manual reset of tracking state."""

    def test_reset_specific_task(self):
        engine = PersistenceEngine()
        _record_n_failures(engine, 3, task_context="task-1")
        _record_n_failures(engine, 2, task_context="task-2")
        engine.reset("agent-1", "task-1")
        assert engine.get_escalation_level("agent-1", "task-1") == 0
        assert engine.get_escalation_level("agent-1", "task-2") == LEVEL_REDIRECT

    def test_reset_all_tasks_for_agent(self):
        engine = PersistenceEngine()
        _record_n_failures(engine, 3, task_context="task-1")
        _record_n_failures(engine, 2, task_context="task-2")
        engine.reset("agent-1")
        assert engine.get_escalation_level("agent-1", "task-1") == 0
        assert engine.get_escalation_level("agent-1", "task-2") == 0

    def test_reset_does_not_affect_other_agents(self):
        engine = PersistenceEngine()
        _record_n_failures(engine, 3, agent_id="agent-1")
        _record_n_failures(engine, 2, agent_id="agent-2")
        engine.reset("agent-1")
        assert engine.get_escalation_level("agent-1", "task-1") == 0
        assert engine.get_escalation_level("agent-2", "task-1") == LEVEL_REDIRECT


# ---------------------------------------------------------------------------
# should_back_off
# ---------------------------------------------------------------------------

class TestShouldBackOff:
    """Test the should_back_off query method."""

    def test_returns_false_for_unknown_agent(self):
        engine = PersistenceEngine()
        assert engine.should_back_off("unknown", "task") is False

    def test_returns_true_when_tools_diverse(self):
        engine = PersistenceEngine()
        engine.record_failure("a", "t", "Error", "Bash")
        # Manually set tools_since_last_surrender to simulate exploration
        state = engine._states[("a", "t")]
        state.tools_since_last_surrender = {"Bash", "Read", "Grep"}
        assert engine.should_back_off("a", "t") is True

    def test_returns_false_when_tools_not_diverse(self):
        engine = PersistenceEngine()
        engine.record_failure("a", "t", "Error", "Bash")
        assert engine.should_back_off("a", "t") is False


# ---------------------------------------------------------------------------
# EscalationAction dataclass
# ---------------------------------------------------------------------------

class TestEscalationAction:
    """Test the EscalationAction dataclass properties."""

    def test_frozen_dataclass(self):
        action = EscalationAction(
            level=1,
            message="test",
            should_output=True,
            should_log=True,
            should_circuit_break=False,
        )
        with pytest.raises(AttributeError):
            action.level = 2  # type: ignore[misc]

    def test_all_fields_accessible(self):
        action = EscalationAction(
            level=3,
            message="decompose",
            should_output=False,
            should_log=True,
            should_circuit_break=False,
        )
        assert action.level == 3
        assert action.message == "decompose"
        assert action.should_output is False
        assert action.should_log is True
        assert action.should_circuit_break is False


# ---------------------------------------------------------------------------
# Edge cases
# ---------------------------------------------------------------------------

class TestEdgeCases:
    """Edge cases and boundary conditions."""

    def test_none_error_type_and_tool(self):
        engine = PersistenceEngine()
        action = engine.record_failure("a", "t", None, None)
        assert action.level == LEVEL_NUDGE
        assert action.should_log is True

    def test_empty_strings_as_identifiers(self):
        engine = PersistenceEngine()
        action = engine.record_failure("", "", "Error", "Bash")
        assert action.level == LEVEL_NUDGE

    def test_get_escalation_level_no_state(self):
        engine = PersistenceEngine()
        assert engine.get_escalation_level("no-agent", "no-task") == 0

    def test_intervention_message_clamped_below_1(self):
        engine = PersistenceEngine()
        msg = engine.get_intervention_message(0)
        # Should clamp to level 1
        msg_level_1 = engine.get_intervention_message(1)
        assert msg == msg_level_1

    def test_intervention_message_clamped_above_5(self):
        engine = PersistenceEngine()
        msg = engine.get_intervention_message(99)
        msg_level_5 = engine.get_intervention_message(5)
        assert msg == msg_level_5
