import threading

import pytest


class FakeClock:
    def __init__(self):
        self.now = 0.0

    def __call__(self):
        return self.now

    def advance(self, seconds):
        self.now += seconds


def test_breaker_opens_after_three_failures_and_recovers():
    from app.core.data_source.circuit_breaker import CircuitBreaker
    from app.core.data_source.errors import DataSourceCircuitOpen

    clock = FakeClock()
    breaker = CircuitBreaker(
        failure_threshold=3,
        recovery_seconds=30,
        clock=clock,
    )

    breaker.record_failure()
    breaker.record_failure()
    breaker.record_failure()
    assert breaker.state == "open"
    assert breaker.consecutive_failures == 3

    with pytest.raises(DataSourceCircuitOpen):
        with breaker.probe_permission():
            pass

    clock.advance(30)
    with breaker.probe_permission():
        assert breaker.state == "half_open"
        breaker.record_success()

    assert breaker.state == "closed"
    assert breaker.consecutive_failures == 0


def test_only_one_half_open_probe_is_allowed():
    from app.core.data_source.circuit_breaker import CircuitBreaker
    from app.core.data_source.errors import DataSourceCircuitOpen

    clock = FakeClock()
    breaker = CircuitBreaker(
        failure_threshold=1,
        recovery_seconds=30,
        clock=clock,
    )
    breaker.record_failure()
    clock.advance(30)
    entered = threading.Event()
    release = threading.Event()

    def hold_probe():
        with breaker.probe_permission():
            entered.set()
            release.wait(timeout=2)
            breaker.record_success()

    thread = threading.Thread(target=hold_probe)
    thread.start()
    assert entered.wait(timeout=2)
    try:
        with pytest.raises(DataSourceCircuitOpen):
            with breaker.probe_permission():
                pass
    finally:
        release.set()
        thread.join(timeout=2)

    assert thread.is_alive() is False
    assert breaker.state == "closed"


def test_failed_half_open_probe_reopens_for_a_full_window():
    from app.core.data_source.circuit_breaker import CircuitBreaker

    clock = FakeClock()
    breaker = CircuitBreaker(
        failure_threshold=1,
        recovery_seconds=30,
        clock=clock,
    )
    breaker.record_failure()
    clock.advance(30)

    with breaker.probe_permission():
        breaker.record_failure()

    assert breaker.state == "open"
    assert breaker.open_until == 60
