"""Tests for neonlink.circuit_breaker."""

import time

import pytest

from neonlink.circuit_breaker import (
    CircuitBreaker,
    CircuitBreakerOpen,
    CircuitBreakerState,
)
from neonlink.errors import PermanentError, is_permanent


class TestCircuitBreaker:
    def test_success_keeps_closed(self):
        cb = CircuitBreaker("test", max_failures=3, reset_timeout_sec=1)
        for _ in range(5):
            cb.execute(lambda: None)
        assert cb.state == CircuitBreakerState.CLOSED

    def test_trips_after_max_failures(self):
        cb = CircuitBreaker("test", max_failures=3, reset_timeout_sec=1)

        for _ in range(3):
            with pytest.raises(ValueError):
                cb.execute(_raise_value_error)

        with pytest.raises(CircuitBreakerOpen):
            cb.execute(lambda: None)

    def test_half_open_after_timeout(self):
        cb = CircuitBreaker("test", max_failures=2, reset_timeout_sec=0.1)

        for _ in range(2):
            with pytest.raises(ValueError):
                cb.execute(_raise_value_error)

        assert cb.state == CircuitBreakerState.OPEN

        time.sleep(0.15)
        assert cb.state == CircuitBreakerState.HALF_OPEN

        # Successful call closes it.
        cb.execute(lambda: None)
        assert cb.state == CircuitBreakerState.CLOSED

    def test_half_open_failure_reopens(self):
        cb = CircuitBreaker("test", max_failures=1, reset_timeout_sec=0.1)

        with pytest.raises(ValueError):
            cb.execute(_raise_value_error)

        time.sleep(0.15)
        assert cb.state == CircuitBreakerState.HALF_OPEN

        with pytest.raises(ValueError):
            cb.execute(_raise_value_error)

        assert cb.state == CircuitBreakerState.OPEN

    def test_success_resets_failure_count(self):
        cb = CircuitBreaker("test", max_failures=3, reset_timeout_sec=1)

        # 2 failures, then success.
        for _ in range(2):
            with pytest.raises(ValueError):
                cb.execute(_raise_value_error)

        cb.execute(lambda: None)

        # 2 more failures should not trip (count was reset).
        for _ in range(2):
            with pytest.raises(ValueError):
                cb.execute(_raise_value_error)

        assert cb.state == CircuitBreakerState.CLOSED


class TestAllowAndRecord:
    def test_allow_when_closed(self):
        cb = CircuitBreaker("test", max_failures=2, reset_timeout_sec=0.1)
        cb.allow()  # Should not raise

    def test_allow_when_open(self):
        cb = CircuitBreaker("test", max_failures=1, reset_timeout_sec=1)
        cb.record_failure()

        with pytest.raises(CircuitBreakerOpen):
            cb.allow()

    def test_record_success_resets(self):
        cb = CircuitBreaker("test", max_failures=2, reset_timeout_sec=0.1)
        cb.record_failure()
        cb.record_failure()
        assert cb.state == CircuitBreakerState.OPEN

        time.sleep(0.15)
        cb.record_success()
        assert cb.state == CircuitBreakerState.CLOSED


class TestExecuteWithClassifier:
    def test_permanent_errors_dont_trip(self):
        cb = CircuitBreaker("test", max_failures=2, reset_timeout_sec=1)

        for _ in range(10):
            with pytest.raises(PermanentError):
                cb.execute_with_classifier(
                    _raise_permanent_error,
                    should_trip=lambda err: not is_permanent(err),
                )

        assert cb.state == CircuitBreakerState.CLOSED

    def test_transient_errors_trip(self):
        cb = CircuitBreaker("test", max_failures=2, reset_timeout_sec=1)

        for _ in range(2):
            with pytest.raises(ValueError):
                cb.execute_with_classifier(
                    _raise_value_error,
                    should_trip=lambda err: not is_permanent(err),
                )

        assert cb.state == CircuitBreakerState.OPEN


class TestStateChangeCallback:
    def test_callback_invoked(self):
        transitions = []

        def on_change(name, from_state, to_state):
            transitions.append(f"{from_state}->{to_state}")

        cb = CircuitBreaker(
            "test", max_failures=1, reset_timeout_sec=0.1,
            on_state_change=on_change,
        )

        with pytest.raises(ValueError):
            cb.execute(_raise_value_error)

        assert len(transitions) == 1
        assert transitions[0] == "closed->open"

        time.sleep(0.15)
        _ = cb.state  # triggers half-open

        assert len(transitions) == 2
        assert transitions[1] == "open->half_open"


def _raise_value_error():
    raise ValueError("test error")


def _raise_permanent_error():
    raise PermanentError(ValueError("bad input"))
