"""Step definitions for circuit_breaker_coverage.feature.

These steps target specific uncovered lines in circuit_breaker.py:
- Lines 160, 162-163, 165-167, 169: sync `call()` — except CircuitBreakerOpen
  handler during HALF_OPEN state (inner-service cascade prevention).
- Lines 191, 193-195: sync `call()` — except BaseException handler during
  HALF_OPEN state (permit leak prevention for KeyboardInterrupt etc.).
- Lines 286, 288-289, 291-293, 295: async `async_call()` — except
  CircuitBreakerOpen handler during HALF_OPEN (same as sync equivalent).
- Line 360: `_on_success()` — stale half-open generation discarded.
- Line 384: `_on_failure()` — no-op when circuit is already OPEN.
"""

import asyncio
import contextlib
import time

from behave import given, then, when

from cleveragents.core.circuit_breaker import (
    CircuitBreaker,
    CircuitBreakerOpen,
    CircuitBreakerState,
)


# ---------------------------------------------------------------------------
# Background
# ---------------------------------------------------------------------------
@given("the circuit breaker coverage module is imported")
def step_circuit_breaker_coverage_module_imported(context):
    """Verify the module is importable."""
    assert CircuitBreaker is not None
    assert CircuitBreakerOpen is not None
    assert CircuitBreakerState is not None


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _open_breaker(cb):
    """Drive the breaker into OPEN state by inducing enough failures."""
    for _ in range(cb.failure_threshold):
        with contextlib.suppress(RuntimeError):
            cb.call(lambda: (_ for _ in ()).throw(RuntimeError("fail")))
    assert cb.state == CircuitBreakerState.OPEN


def _transition_to_half_open(cb):
    """Open the breaker, wait for recovery_timeout, then trigger the
    OPEN -> HALF_OPEN transition via a successful probe call.

    After one success the breaker is still HALF_OPEN because it needs
    half_open_max_successes=2 successes to close.
    """
    _open_breaker(cb)
    # Wait long enough for recovery_timeout + cooldown to elapse.
    time.sleep(0.05)
    # The next call transitions the breaker to HALF_OPEN internally
    # and counts as the first success.
    result = cb.call(lambda: "probe_ok")
    assert result == "probe_ok"
    assert cb.state == CircuitBreakerState.HALF_OPEN


# ---------------------------------------------------------------------------
# Given: create a circuit breaker with fast recovery
# ---------------------------------------------------------------------------
@given(
    "a coverage circuit breaker with failure threshold {threshold:d} and fast recovery"
)
def step_create_coverage_fast_recovery_breaker(context, threshold):
    """Create a breaker that transitions quickly for testing."""
    context.cb = CircuitBreaker(
        failure_threshold=threshold,
        recovery_timeout=0.01,
        expected_exception=RuntimeError,
        half_open_max_successes=2,
        cooldown_seconds=0.0,
        name="test-coverage",
    )


@given("the coverage circuit breaker is transitioned to half-open state")
def step_coverage_transition_to_half_open(context):
    """Transition the breaker CLOSED -> OPEN -> HALF_OPEN."""
    _transition_to_half_open(context.cb)
    # Record the permits before the next call.
    context.permits_before = context.cb._half_open_permits


# ---------------------------------------------------------------------------
# When: sync CircuitBreakerOpen from inner service (lines 160-169)
# ---------------------------------------------------------------------------
@when("I sync-call a function that raises inner CircuitBreakerOpen")
def step_sync_call_inner_cbo(context):
    """Call a function that raises CircuitBreakerOpen inside the breaker."""

    def inner_raises_cbo():
        raise CircuitBreakerOpen("inner service is open")

    context.caught_exception = None
    try:
        context.cb.call(inner_raises_cbo)
    except CircuitBreakerOpen as exc:
        context.caught_exception = exc


# ---------------------------------------------------------------------------
# When: sync BaseException (KeyboardInterrupt) during HALF_OPEN (lines 191-195)
# ---------------------------------------------------------------------------
@when("I sync-call a function that raises a KeyboardInterrupt")
def step_sync_call_keyboard_interrupt(context):
    """Call a function that raises KeyboardInterrupt (a BaseException)."""

    def inner_raises_kbi():
        raise KeyboardInterrupt("simulated ctrl-c")

    context.caught_exception = None
    try:
        context.cb.call(inner_raises_kbi)
    except KeyboardInterrupt as exc:
        context.caught_exception = exc


# ---------------------------------------------------------------------------
# When: async CircuitBreakerOpen from inner service (lines 286-295)
# ---------------------------------------------------------------------------
@when("I async-call a function that raises inner CircuitBreakerOpen")
def step_async_call_inner_cbo(context):
    """Async-call a function that raises CircuitBreakerOpen."""

    async def async_inner_raises_cbo():
        raise CircuitBreakerOpen("inner async service is open")

    context.caught_exception = None
    loop = asyncio.new_event_loop()
    try:
        loop.run_until_complete(context.cb.async_call(async_inner_raises_cbo))
    except CircuitBreakerOpen as exc:
        context.caught_exception = exc
    finally:
        loop.close()


# ---------------------------------------------------------------------------
# When: async BaseException (KeyboardInterrupt) during HALF_OPEN
# ---------------------------------------------------------------------------
@when("I async-call a function that raises a KeyboardInterrupt")
def step_async_call_keyboard_interrupt(context):
    """Async-call a function that raises KeyboardInterrupt."""

    async def async_inner_raises_kbi():
        raise KeyboardInterrupt("simulated async ctrl-c")

    context.caught_exception = None
    loop = asyncio.new_event_loop()
    try:
        loop.run_until_complete(context.cb.async_call(async_inner_raises_kbi))
    except KeyboardInterrupt as exc:
        context.caught_exception = exc
    finally:
        loop.close()


# ---------------------------------------------------------------------------
# Then: verify exceptions
# ---------------------------------------------------------------------------
@then("the caught exception should be a CircuitBreakerOpen")
def step_verify_cbo_raised(context):
    assert isinstance(context.caught_exception, CircuitBreakerOpen), (
        f"Expected CircuitBreakerOpen, got {type(context.caught_exception).__name__}"
    )


@then("the caught exception should be a KeyboardInterrupt")
def step_verify_kbi_raised(context):
    assert isinstance(context.caught_exception, KeyboardInterrupt), (
        f"Expected KeyboardInterrupt, got {type(context.caught_exception).__name__}"
    )


@then("the coverage half-open permit count should be restored")
def step_verify_coverage_permit_restored(context):
    """After the exception handler restores the permit, the count should
    be back to what it was before the call consumed it.
    """
    assert context.cb._half_open_permits == context.permits_before, (
        f"Expected permits={context.permits_before}, "
        f"got {context.cb._half_open_permits}"
    )


# ---------------------------------------------------------------------------
# Stale generation discarded in _on_success (line 360)
# ---------------------------------------------------------------------------
@given("a coverage circuit breaker in half-open state with generation {gen:d}")
def step_breaker_half_open_with_generation(context, gen):
    """Create a breaker and directly set it to HALF_OPEN with a known
    generation counter so we can test stale-generation logic.
    """
    context.cb = CircuitBreaker(
        failure_threshold=3,
        recovery_timeout=60.0,
        expected_exception=RuntimeError,
        half_open_max_successes=2,
        name="stale-gen-test",
    )
    # Directly manipulate internal state for precise control.
    context.cb.state = CircuitBreakerState.HALF_OPEN
    context.cb._half_open_generation = gen
    context.cb.success_count_half_open = 0


@when("_on_success is called with stale generation {stale_gen:d}")
def step_call_on_success_stale(context, stale_gen):
    """Call _on_success with a generation that does not match the current one."""
    context.on_success_result = context.cb._on_success(generation=stale_gen)


@then("the _on_success result should be False")
def step_verify_on_success_returns_false(context):
    assert context.on_success_result is False, (
        f"Expected False, got {context.on_success_result}"
    )


@then("the coverage circuit breaker should still be half-open")
def step_verify_still_half_open(context):
    assert context.cb.state == CircuitBreakerState.HALF_OPEN, (
        f"Expected HALF_OPEN, got {context.cb.state}"
    )


# ---------------------------------------------------------------------------
# _on_failure when already OPEN (line 384)
# ---------------------------------------------------------------------------
@given("a coverage circuit breaker that is already in the open state")
def step_breaker_already_open(context):
    """Create a breaker and force it into the OPEN state."""
    context.cb = CircuitBreaker(
        failure_threshold=3,
        recovery_timeout=60.0,
        expected_exception=RuntimeError,
        name="already-open-test",
    )
    context.cb.state = CircuitBreakerState.OPEN
    context.cb.failure_count = 5
    context.cb.last_failure_time = time.monotonic()


@when("_on_failure is called on the already-open breaker")
def step_call_on_failure_while_open(context):
    """Call _on_failure when the circuit is already OPEN."""
    context.original_failure_count = context.cb.failure_count
    context.on_failure_result = context.cb._on_failure(generation=0)


@then("the _on_failure result should be False")
def step_verify_on_failure_returns_false(context):
    assert context.on_failure_result is False, (
        f"Expected False, got {context.on_failure_result}"
    )


@then("the coverage circuit breaker state should still be open")
def step_verify_still_open(context):
    assert context.cb.state == CircuitBreakerState.OPEN, (
        f"Expected OPEN, got {context.cb.state}"
    )


@then("the failure count should not have changed")
def step_verify_failure_count_unchanged(context):
    assert context.cb.failure_count == context.original_failure_count, (
        f"Expected failure_count={context.original_failure_count}, "
        f"got {context.cb.failure_count}"
    )
