"""
Test Suite for Cascading Guard Engine — ASI08: Cascading Failures

PhD-Level Testing:
- Circuit breaker state machine tests
- Fanout detection tests
- Feedback loop detection tests
- Rollback coordination tests
- Integration tests
"""

import pytest
from datetime import datetime, timedelta
from typing import List

from engines.cascading_guard import (
    CircuitState,
    AgentAction,
    FanoutResult,
    RollbackPlan,
    CascadeAnalysisResult,
    CascadingGuard,
)


# ============================================================================
# Fixtures
# ============================================================================


@pytest.fixture
def guard() -> CascadingGuard:
    """Create cascading guard."""
    return CascadingGuard()


@pytest.fixture
def sample_action() -> AgentAction:
    """Create sample agent action."""
    return AgentAction(
        action_id="action-001",
        agent_id="agent-A",
        action_type="process",
        timestamp=datetime.now(),
        target_agents=["agent-B", "agent-C"],
        parent_action=None,
    )


@pytest.fixture
def chain_actions() -> List[AgentAction]:
    """Creates a chain of agent actions for cascade testing."""
    now = datetime.now()
    return [
        AgentAction("act-1", "agent-A", "call", now, ["agent-B"], None),
        AgentAction("act-2", "agent-B", "call", now +
                    timedelta(milliseconds=100), ["agent-C"], "act-1"),
        AgentAction("act-3", "agent-C", "call", now +
                    timedelta(milliseconds=200), ["agent-D"], "act-2"),
        AgentAction("act-4", "agent-D", "call", now +
                    timedelta(milliseconds=300), [], "act-3"),
    ]


@pytest.fixture
def loop_actions() -> List[AgentAction]:
    """Creates a feedback loop: A -> B -> C -> A."""
    now = datetime.now()
    return [
        AgentAction("loop-1", "agent-A", "call", now, ["agent-B"], None),
        AgentAction("loop-2", "agent-B", "call", now +
                    timedelta(milliseconds=100), ["agent-C"], "loop-1"),
        AgentAction("loop-3", "agent-C", "call", now +
                    timedelta(milliseconds=200), ["agent-A"], "loop-2"),
        AgentAction("loop-4", "agent-A", "call", now +
                    timedelta(milliseconds=300), ["agent-B"], "loop-3"),
    ]


# ============================================================================
# CircuitBreaker Tests
# ============================================================================


class TestCircuitBreaker:
    """Tests for CircuitBreaker dataclass."""

    def test_initial_state_closed(self, guard):
        """New circuit breaker should start closed."""
        cb = guard.get_or_create_circuit_breaker("agent-X", "action")

        assert cb.state == CircuitState.CLOSED

    def test_trip_opens_circuit(self, guard):
        """Tripping should open circuit."""
        cb = guard.get_or_create_circuit_breaker("agent-X", "action")
        cb.trip()

        assert cb.state == CircuitState.OPEN

    def test_reset_closes_circuit(self, guard):
        """Reset should close circuit."""
        cb = guard.get_or_create_circuit_breaker("agent-X", "action")
        cb.trip()
        cb.reset()

        assert cb.state == CircuitState.CLOSED

    def test_half_open_state(self, guard):
        """Half-open allows testing recovery."""
        cb = guard.get_or_create_circuit_breaker("agent-X", "action")
        cb.trip()
        cb.half_open()

        assert cb.state == CircuitState.HALF_OPEN


# ============================================================================
# Failure Recording Tests
# ============================================================================


class TestFailureRecording:
    """Tests for failure/success recording."""

    def test_record_failure_increments_count(self, guard):
        """Recording failure should increment count."""
        cb = guard.get_or_create_circuit_breaker("agent-A", "action")
        initial = cb.failure_count

        guard.record_failure("agent-A", "action")

        assert cb.failure_count == initial + 1

    def test_record_success_increments_count(self, guard):
        """Recording success should increment count."""
        cb = guard.get_or_create_circuit_breaker("agent-A", "action")
        initial = cb.success_count

        guard.record_success("agent-A", "action")

        assert cb.success_count == initial + 1

    def test_multiple_failures_trip_circuit(self, guard):
        """Multiple failures should trip circuit breaker."""
        agent = "fail-agent"
        action = "flaky-action"

        # Get the threshold from config or use default
        for _ in range(10):  # Assuming threshold is <= 10
            guard.record_failure(agent, action)

        cb = guard.get_or_create_circuit_breaker(agent, action)

        # Should be tripped after many failures
        assert cb.failure_count >= 10


# ============================================================================
# Circuit Breaker Enforcement Tests
# ============================================================================


class TestCircuitBreakerEnforcement:
    """Tests for circuit breaker enforcement."""

    def test_closed_circuit_allows_action(self, guard):
        """Closed circuit should allow action."""
        allowed = guard.enforce_circuit_breaker("agent-X", "action")

        assert allowed is True

    def test_open_circuit_blocks_action(self, guard):
        """Open circuit should block action."""
        cb = guard.get_or_create_circuit_breaker("agent-X", "action")
        cb.trip()

        allowed = guard.enforce_circuit_breaker("agent-X", "action")

        assert allowed is False

    def test_half_open_allows_test_action(self, guard):
        """Half-open circuit should allow test action."""
        cb = guard.get_or_create_circuit_breaker("agent-X", "action")
        cb.trip()
        cb.half_open()

        allowed = guard.enforce_circuit_breaker("agent-X", "action")

        assert allowed is True


# ============================================================================
# Action Recording Tests
# ============================================================================


class TestActionRecording:
    """Tests for action recording."""

    def test_record_single_action(self, guard, sample_action):
        """Should record action."""
        guard.record_action(sample_action)

        # Action should be recorded
        stats = guard.get_statistics()
        assert stats.get("total_actions_tracked", 0) >= 1

    def test_record_chain_of_actions(self, guard, chain_actions):
        """Should record chain of actions."""
        for action in chain_actions:
            guard.record_action(action)

        stats = guard.get_statistics()
        assert isinstance(stats, dict)


# ============================================================================
# Fanout Detection Tests
# ============================================================================


class TestFanoutDetection:
    """Tests for fanout detection."""

    def test_detect_fanout_single_action(self, guard, sample_action):
        """Single action with multiple targets should show fanout."""
        guard.record_action(sample_action)

        result = guard.detect_fanout(sample_action)

        assert isinstance(result, FanoutResult)
        assert result.origin_agent == sample_action.agent_id

    def test_detect_fanout_chain(self, guard, chain_actions):
        """Chain of actions should calculate propagation depth."""
        for action in chain_actions:
            guard.record_action(action)

        result = guard.detect_fanout(chain_actions[0])

        assert isinstance(result, FanoutResult)
        assert result.propagation_depth >= 0


# ============================================================================
# Feedback Loop Detection Tests
# ============================================================================


class TestFeedbackLoopDetection:
    """Tests for feedback loop detection."""

    def test_no_loop_in_chain(self, guard, chain_actions):
        """Linear chain should not have feedback loop."""
        for action in chain_actions:
            guard.record_action(action)

        loops = guard.detect_feedback_loop()

        # Linear chain has no loops
        assert isinstance(loops, list)

    def test_detect_loop(self, guard, loop_actions):
        """Circular actions should detect feedback loop."""
        for action in loop_actions:
            guard.record_action(action)

        loops = guard.detect_feedback_loop()

        # Should detect A -> B -> C -> A loop
        assert isinstance(loops, list)
        # If loop detection works, should have at least one
        # (depends on implementation details)


# ============================================================================
# Rollback Tests
# ============================================================================


class TestRollbackCoordination:
    """Tests for rollback coordination."""

    def test_coordinate_rollback_returns_plan(self, guard, chain_actions):
        """Should generate rollback plan."""
        for action in chain_actions:
            guard.record_action(action)

        plan = guard.coordinate_rollback("cascade-001")

        assert isinstance(plan, RollbackPlan)
        assert plan.cascade_id == "cascade-001"

    def test_rollback_plan_has_order(self, guard, chain_actions):
        """Rollback plan should have rollback order."""
        for action in chain_actions:
            guard.record_action(action)

        plan = guard.coordinate_rollback("cascade-001")

        assert isinstance(plan.rollback_order, list)


# ============================================================================
# Cascade Analysis Tests
# ============================================================================


class TestCascadeAnalysis:
    """Tests for full cascade analysis."""

    def test_analyze_empty_system(self, guard):
        """Empty system should not be cascading."""
        result = guard.analyze_cascade()

        assert isinstance(result, CascadeAnalysisResult)
        assert result.is_cascading is False

    def test_analyze_with_actions(self, guard, chain_actions):
        """System with actions should produce analysis."""
        for action in chain_actions:
            guard.record_action(action)

        result = guard.analyze_cascade()

        assert isinstance(result, CascadeAnalysisResult)
        assert hasattr(result, "risk_score")
        assert hasattr(result, "recommendations")

    def test_analyze_provides_recommendations(self, guard, chain_actions):
        """Analysis should provide recommendations."""
        for action in chain_actions:
            guard.record_action(action)

        result = guard.analyze_cascade()

        assert isinstance(result.recommendations, list)


# ============================================================================
# Statistics Tests
# ============================================================================


class TestStatistics:
    """Tests for statistics collection."""

    def test_get_statistics(self, guard):
        """Should return statistics dict."""
        stats = guard.get_statistics()

        assert isinstance(stats, dict)

    def test_statistics_after_activity(self, guard, chain_actions):
        """Statistics should reflect activity."""
        for action in chain_actions:
            guard.record_action(action)

        guard.record_failure("agent-A", "action")

        stats = guard.get_statistics()

        assert isinstance(stats, dict)


# ============================================================================
# Edge Cases
# ============================================================================


class TestEdgeCases:
    """Edge case testing."""

    def test_empty_target_agents(self, guard):
        """Action with no targets should be handled."""
        action = AgentAction(
            action_id="solo-1",
            agent_id="lone-agent",
            action_type="solo",
            timestamp=datetime.now(),
            target_agents=[],
            parent_action=None,
        )

        guard.record_action(action)
        result = guard.detect_fanout(action)

        assert isinstance(result, FanoutResult)

    def test_self_targeting_action(self, guard):
        """Agent targeting itself should be handled."""
        action = AgentAction(
            action_id="self-1",
            agent_id="agent-X",
            action_type="self-call",
            timestamp=datetime.now(),
            target_agents=["agent-X"],  # Self-target
            parent_action=None,
        )

        guard.record_action(action)
        # Should not crash
        assert True

    def test_wildcard_circuit_breaker(self, guard):
        """Wildcard action type should work."""
        cb = guard.get_or_create_circuit_breaker("agent-A", "*")

        assert cb is not None
        assert cb.action_type == "*"


if __name__ == "__main__":
    pytest.main([__file__, "-v", "--tb=short"])
