"""Step definitions for devcontainer lifecycle state model, registry, and JSON parsing.

Covers: lifecycle state enum, state transitions, tracker creation and
retrieval, registry operations (get/set/clear/evict), argument
validation for validate_transition and transition_state, the
transition history cap, and devcontainer-up JSON output parsing.

All mocks are in ``features/mocks/mock_devcontainer_cli.py``.
"""

from __future__ import annotations

import json
from typing import cast

from behave import given, then, when
from behave.runner import Context

from cleveragents.domain.models.core.container_lifecycle import (
    ContainerLifecycleState,
    ContainerLifecycleTracker,
    transition_state,
    validate_transition,
)
from cleveragents.resource.handlers.devcontainer import (
    _lifecycle_registry,
    _parse_devcontainer_up_output,
    _registry_lock,
    clear_lifecycle_registry,
    evict_terminal_trackers,
    get_lifecycle_tracker,
    list_active_containers,
    set_lifecycle_tracker,
)

# ── Shared helper ────────────────────────────────────────────


# R21 fix: shared _OneShotEvent helper — extracted from duplicated
# definitions in step_single_health_probe and step_run_health_check_loop_once.
#
# F8 fix: documented the exact call sequence the counter relies on.
class _OneShotEvent:
    """Stop event that allows exactly one probe iteration.

    Simulates ``threading.Event`` for health check loop testing,
    running exactly one probe cycle before signaling stop.

    The counter relies on the following call sequence inside
    ``_health_check_loop``:

    1. ``while not stop_event.is_set()``  → count=1, returns False
    2. ``stop_event.wait(interval)``      → overridden to no-op
    3. ``if stop_event.is_set()``         → count=2, returns False
    4. (probe executes)
    5. ``while not stop_event.is_set()``  → count=3, returns True → exit

    **WARNING**: If the loop's ``is_set()`` call pattern changes, this
    counter threshold must be updated to match.
    """

    def __init__(self) -> None:
        self._count = 0

    def is_set(self) -> bool:
        self._count += 1
        return self._count > 2

    def set(self) -> None:
        self._count = 999

    def wait(self, timeout: float = 0) -> bool:
        return False


# ── Given steps ──────────────────────────────────────────────
# Registry cleanup is handled by the before_scenario hook in
# features/environment.py so that all scenarios start with a
# clean lifecycle registry regardless of execution order.


@given('a lifecycle tracker for resource "{resource_id}"')
def step_create_tracker(context: Context, resource_id: str) -> None:
    tracker = ContainerLifecycleTracker(resource_id=resource_id)
    set_lifecycle_tracker(tracker)
    context.tracker = tracker
    context.resource_id = resource_id


@given('a lifecycle tracker in "{state}" state for resource "{resource_id}"')
def step_create_tracker_in_state(
    context: Context, state: str, resource_id: str
) -> None:
    tracker = ContainerLifecycleTracker(
        resource_id=resource_id,
        current_state=ContainerLifecycleState(state),
    )
    set_lifecycle_tracker(tracker)
    context.tracker = tracker
    context.resource_id = resource_id


@given("{count:d} stopped container trackers in the registry")
def step_create_many_stopped(context: Context, count: int) -> None:
    with _registry_lock:
        for i in range(count):
            rid = f"01EVICT{i:020d}"
            _lifecycle_registry[rid] = ContainerLifecycleTracker(
                resource_id=rid,
                current_state=ContainerLifecycleState.STOPPED,
            )


# ── When steps ───────────────────────────────────────────────


@when("I inspect ContainerLifecycleState enum values")
def step_inspect_enum(context: Context) -> None:
    context.enum_values = [s.value for s in ContainerLifecycleState]


@when('I transition from "{from_state}" to "{to_state}"')
def step_transition(context: Context, from_state: str, to_state: str) -> None:
    context.transition_error = None
    try:
        context.tracker = transition_state(
            context.tracker,
            ContainerLifecycleState(to_state),
            reason="test transition",
        )
        set_lifecycle_tracker(context.tracker)
    except (ValueError, TypeError) as exc:
        context.transition_error = exc


@when('I attempt to transition from "{from_state}" to "{to_state}"')
def step_attempt_transition(context: Context, from_state: str, to_state: str) -> None:
    context.transition_error = None
    try:
        context.tracker = transition_state(
            context.tracker,
            ContainerLifecycleState(to_state),
            reason="test transition",
        )
        set_lifecycle_tracker(context.tracker)
    except (ValueError, TypeError) as exc:
        context.transition_error = exc


@when("I call validate_transition with non-enum arguments")
def step_validate_transition_bad_types(context: Context) -> None:
    context.type_error = None
    try:
        validate_transition(
            cast(ContainerLifecycleState, "inactive"),
            cast(ContainerLifecycleState, "starting"),
        )
    except TypeError as exc:
        context.type_error = exc


@when("I call validate_transition with non-enum to_state")
def step_validate_transition_bad_to_state(context: Context) -> None:
    context.type_error = None
    try:
        validate_transition(
            ContainerLifecycleState.DETECTED,
            cast(ContainerLifecycleState, "starting"),
        )
    except TypeError as exc:
        context.type_error = exc


@when("I call transition_state with non-tracker argument")
def step_transition_state_bad_tracker(context: Context) -> None:
    context.type_error = None
    try:
        transition_state(
            cast(ContainerLifecycleTracker, "not-a-tracker"),
            ContainerLifecycleState.BUILDING,
        )
    except TypeError as exc:
        context.type_error = exc


@when("I call transition_state with non-enum to_state argument")
def step_transition_state_bad_to_state(context: Context) -> None:
    context.type_error = None
    try:
        tracker = ContainerLifecycleTracker(resource_id="01TESTLIFECYCLE0099000001")
        transition_state(tracker, cast(ContainerLifecycleState, "starting"))
    except TypeError as exc:
        context.type_error = exc


@when('I retrieve the tracker for "{resource_id}"')
def step_retrieve_tracker(context: Context, resource_id: str) -> None:
    context.retrieved_tracker = get_lifecycle_tracker(resource_id)


@when("I list active containers")
def step_list_active(context: Context) -> None:
    context.active_list = list_active_containers()


@when("I perform 105 transitions cycling through valid states")
def step_perform_105_transitions(context: Context) -> None:
    """Cycle detected->building->running->stopping->stopped->building->... 105 times."""
    tracker = context.tracker
    # First transition: detected -> building
    tracker = transition_state(
        tracker, ContainerLifecycleState.BUILDING, reason="t1-init"
    )
    cycle = [
        ContainerLifecycleState.RUNNING,
        ContainerLifecycleState.STOPPING,
        ContainerLifecycleState.STOPPED,
        ContainerLifecycleState.BUILDING,
    ]
    # We've done 1 transition, need 104 more for 105 total
    for i in range(104):
        state = cycle[i % len(cycle)]
        tracker = transition_state(tracker, state, reason=f"t1-{i}")
    set_lifecycle_tracker(tracker)
    context.tracker = tracker


@when("I attempt to get tracker with empty resource_id")
def step_get_tracker_empty(context: Context) -> None:
    context.get_error = None
    try:
        get_lifecycle_tracker("")
    except ValueError as exc:
        context.get_error = exc


@when("I attempt to set tracker with wrong type")
def step_set_tracker_wrong_type(context: Context) -> None:
    context.set_error = None
    try:
        set_lifecycle_tracker(cast(ContainerLifecycleTracker, "not-a-tracker"))
    except TypeError as exc:
        context.set_error = exc


@when("I run terminal tracker eviction")
def step_run_eviction(context: Context) -> None:
    context.evicted_count = evict_terminal_trackers()


@when('I get tracker for new resource "{resource_id}"')
def step_get_new_tracker(context: Context, resource_id: str) -> None:
    context.auto_tracker = get_lifecycle_tracker(resource_id)


@when("I clear the lifecycle registry")
def step_clear_registry(context: Context) -> None:
    clear_lifecycle_registry()


# ── Then steps ───────────────────────────────────────────────


@then('the lifecycle enum should contain "{value}"')
def step_check_lifecycle_enum_value(context: Context, value: str) -> None:
    assert value in context.enum_values, f"'{value}' not in {context.enum_values}"


@then("the transition should succeed")
def step_transition_ok(context: Context) -> None:
    assert context.transition_error is None, (
        f"Unexpected error: {context.transition_error}"
    )


@then('the tracker state should be "{state}"')
def step_check_tracker_state(context: Context, state: str) -> None:
    assert context.tracker.current_state == ContainerLifecycleState(state)


@then("the transition should raise ValueError")
def step_transition_error(context: Context) -> None:
    assert context.transition_error is not None
    assert isinstance(context.transition_error, ValueError)


@then("the tracker should have {count:d} transition in history")
def step_check_history_count(context: Context, count: int) -> None:
    assert len(context.tracker.transitions) == count


@then('the retrieved tracker state should be "{state}"')
def step_check_retrieved_state(context: Context, state: str) -> None:
    assert context.retrieved_tracker.current_state == ContainerLifecycleState(state)


@then('the active list should contain "{resource_id}"')
def step_check_active_contains(context: Context, resource_id: str) -> None:
    assert resource_id in context.active_list


@then('the active list should not contain "{resource_id}"')
def step_check_active_not_contains(context: Context, resource_id: str) -> None:
    assert resource_id not in context.active_list


@then("it should raise TypeError")
def step_check_type_error(context: Context) -> None:
    assert context.type_error is not None
    assert isinstance(context.type_error, TypeError)


@then("the tracker should have at most 100 transitions")
def step_check_max_transitions(context: Context) -> None:
    assert len(context.tracker.transitions) <= 100, (
        f"Expected at most 100 transitions, got {len(context.tracker.transitions)}"
    )


@then("the get tracker should raise ValueError")
def step_check_get_error(context: Context) -> None:
    assert context.get_error is not None
    assert isinstance(context.get_error, ValueError)


@then("the set tracker should raise TypeError")
def step_check_set_error(context: Context) -> None:
    assert context.set_error is not None
    assert isinstance(context.set_error, TypeError)


@then("the registry should have at most {max_count:d} terminal trackers")
def step_check_registry_cap(context: Context, max_count: int) -> None:
    with _registry_lock:
        terminal = [
            rid
            for rid, t in _lifecycle_registry.items()
            if t.current_state
            in (ContainerLifecycleState.STOPPED, ContainerLifecycleState.FAILED)
        ]
    assert len(terminal) <= max_count, (
        f"Expected at most {max_count} terminal trackers, got {len(terminal)}"
    )


@then("the eviction count should be {count:d}")
def step_check_eviction_count(context: Context, count: int) -> None:
    assert context.evicted_count == count, (
        f"Expected eviction count {count}, got {context.evicted_count}"
    )


@then('the auto-created tracker state should be "{state}"')
def step_check_auto_tracker_state(context: Context, state: str) -> None:
    assert context.auto_tracker.current_state == ContainerLifecycleState(state)


@then('the auto-created tracker resource_id should be "{resource_id}"')
def step_check_auto_tracker_id(context: Context, resource_id: str) -> None:
    assert context.auto_tracker.resource_id == resource_id


@then("the registry should be empty")
def step_check_registry_empty(context: Context) -> None:
    with _registry_lock:
        assert len(_lifecycle_registry) == 0, (
            f"Expected empty registry, got {len(_lifecycle_registry)} entries"
        )


# ── JSON output parsing steps ────────────────────────────────


@when('I parse devcontainer up output with containerId "{ctr_id}" and workspace "{ws}"')
def step_parse_json(context: Context, ctr_id: str, ws: str) -> None:
    stdout = json.dumps(
        {
            "outcome": "success",
            "containerId": ctr_id,
            "remoteWorkspaceFolder": ws,
        }
    )
    context.parsed_ctr_id, context.parsed_ws = _parse_devcontainer_up_output(stdout)


@when("I parse devcontainer up output with invalid JSON")
def step_parse_invalid_json(context: Context) -> None:
    context.parsed_ctr_id, context.parsed_ws = _parse_devcontainer_up_output(
        "not valid json {{"
    )


@when("I parse devcontainer up output with log lines before JSON")
def step_parse_multiline_json(context: Context) -> None:
    stdout = (
        "[2025-01-01T00:00:00Z] Starting container...\n"
        "Some log line\n"
        + json.dumps(
            {
                "outcome": "success",
                "containerId": "aabbccddee0099887766",
                "remoteWorkspaceFolder": "/workspaces/multi",
            }
        )
    )
    context.parsed_ctr_id, context.parsed_ws = _parse_devcontainer_up_output(stdout)


@when('I parse devcontainer up output with invalid container ID "{bad_id}"')
def step_parse_invalid_container_id(context: Context, bad_id: str) -> None:
    stdout = json.dumps(
        {
            "outcome": "success",
            "containerId": bad_id,
            "remoteWorkspaceFolder": "/workspaces/project",
        }
    )
    context.parsed_ctr_id, context.parsed_ws = _parse_devcontainer_up_output(stdout)


@when('I parse devcontainer up output with relative workspace "{ws}"')
def step_parse_relative_ws(context: Context, ws: str) -> None:
    stdout = json.dumps(
        {
            "outcome": "success",
            "containerId": "aabbccddee0011223344",
            "remoteWorkspaceFolder": ws,
        }
    )
    context.parsed_ctr_id, context.parsed_ws = _parse_devcontainer_up_output(stdout)


@then('the parsed container ID should be "{ctr_id}"')
def step_check_parsed_ctr_id(context: Context, ctr_id: str) -> None:
    assert context.parsed_ctr_id == ctr_id


@then("the parsed container ID should be None")
def step_check_parsed_ctr_id_none(context: Context) -> None:
    assert context.parsed_ctr_id is None


@then('the parsed workspace path should be "{ws}"')
def step_check_parsed_ws(context: Context, ws: str) -> None:
    assert context.parsed_ws == ws


@then("the parsed workspace path should be None")
def step_check_parsed_ws_none(context: Context) -> None:
    assert context.parsed_ws is None


@then("the parsed container ID should be empty")
def step_check_empty_container_id(context: Context) -> None:
    assert context.parsed_ctr_id is None, (
        f"Expected None container ID, got '{context.parsed_ctr_id}'"
    )


@then("the runner should not have received a devcontainer up call")
def step_check_no_up_call(context: Context) -> None:
    assert len(context.mock_runner.up_calls) == 0, (
        f"Expected 0 devcontainer up calls, got {len(context.mock_runner.up_calls)}"
    )
