"""Tests para circuit_breaker."""

import sys
import os
import threading
import time

sys.path.insert(0, os.path.dirname(__file__))

from circuit_breaker import CircuitBreaker, CircuitBreakerOpen, CircuitBreakerState


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


def _transient_error() -> Exception:
    return Exception("HTTP 503 Service Unavailable")


def _permanent_error() -> Exception:
    return Exception("HTTP 401 Unauthorized")


# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------


def test_closed_to_open_after_n_failures() -> None:
    """Transicion CLOSED → OPEN despues de N fallos"""
    cb = CircuitBreaker(failure_threshold=3, reset_timeout=60.0)

    cb.check()  # Should not raise

    cb.record_failure(_transient_error())
    cb.record_failure(_transient_error())
    assert cb._state is CircuitBreakerState.CLOSED  # Still closed after 2

    cb.record_failure(_transient_error())
    assert cb._state is CircuitBreakerState.OPEN

    try:
        cb.check()
        assert False, "Should have raised CircuitBreakerOpen"
    except CircuitBreakerOpen:
        pass

    print("PASS: Transicion CLOSED → OPEN despues de N fallos")


def test_open_to_half_open_after_timeout() -> None:
    """Transicion OPEN → HALF_OPEN despues de timeout"""
    cb = CircuitBreaker(failure_threshold=1, reset_timeout=0.05)
    cb.record_failure(_transient_error())
    assert cb._state is CircuitBreakerState.OPEN

    time.sleep(0.1)

    cb.check()  # Should not raise — transitions to HALF_OPEN
    assert cb._state is CircuitBreakerState.HALF_OPEN

    print("PASS: Transicion OPEN → HALF_OPEN despues de timeout")


def test_half_open_to_closed_on_success() -> None:
    """Transicion HALF_OPEN → CLOSED en exito"""
    cb = CircuitBreaker(failure_threshold=1, reset_timeout=0.05)
    cb.record_failure(_transient_error())
    time.sleep(0.1)
    cb.check()  # enters HALF_OPEN
    assert cb._state is CircuitBreakerState.HALF_OPEN

    cb.record_success()
    assert cb._state is CircuitBreakerState.CLOSED

    cb.check()  # Should not raise

    print("PASS: Transicion HALF_OPEN → CLOSED en exito")


def test_half_open_to_open_on_failure() -> None:
    """Transicion HALF_OPEN → OPEN en fallo"""
    cb = CircuitBreaker(failure_threshold=1, reset_timeout=0.05)
    cb.record_failure(_transient_error())
    time.sleep(0.1)
    cb.check()  # enters HALF_OPEN
    assert cb._state is CircuitBreakerState.HALF_OPEN

    cb.record_failure(_transient_error())
    assert cb._state is CircuitBreakerState.OPEN

    print("PASS: Transicion HALF_OPEN → OPEN en fallo")


def test_permanent_error_opens_immediately() -> None:
    """Error permanente abre inmediatamente"""
    cb = CircuitBreaker(failure_threshold=10, reset_timeout=60.0)
    assert cb._state is CircuitBreakerState.CLOSED

    cb.record_failure(_permanent_error())
    assert cb._state is CircuitBreakerState.OPEN

    print("PASS: Error permanente abre inmediatamente")


def test_thread_safety() -> None:
    """Thread safety (concurrencia)"""
    cb = CircuitBreaker(failure_threshold=5, reset_timeout=60.0)
    errors: list[Exception] = []

    def worker() -> None:
        try:
            for _ in range(10):
                cb.check()
                cb.record_failure(_transient_error())
        except CircuitBreakerOpen:
            pass
        except Exception as exc:
            errors.append(exc)

    threads = [threading.Thread(target=worker) for _ in range(20)]
    for t in threads:
        t.start()
    for t in threads:
        t.join()

    assert not errors, f"Thread errors: {errors}"
    # After concurrent failures the circuit must be OPEN or HALF_OPEN
    assert cb._state in (CircuitBreakerState.OPEN, CircuitBreakerState.HALF_OPEN, CircuitBreakerState.CLOSED)

    print("PASS: Thread safety (concurrencia)")


def test_retry_after_returns_zero_when_not_open() -> None:
    """retry_after retorna 0 cuando no esta OPEN"""
    cb = CircuitBreaker(failure_threshold=5, reset_timeout=60.0)
    assert cb.retry_after == 0.0

    cb.record_failure(_transient_error())
    # Still CLOSED (threshold not reached)
    assert cb.retry_after == 0.0

    print("PASS: retry_after retorna 0 cuando no esta OPEN")


if __name__ == "__main__":
    test_closed_to_open_after_n_failures()
    test_open_to_half_open_after_timeout()
    test_half_open_to_closed_on_success()
    test_half_open_to_open_on_failure()
    test_permanent_error_opens_immediately()
    test_thread_safety()
    test_retry_after_returns_zero_when_not_open()
    print("\nAll tests passed.")
