"""Step definitions for devcontainer activation, stop, and rebuild.

Covers: lazy activation (success/failure/timeout/orphan), manual stop,
rebuild, concurrent activation and stop, argument validation for
activate/stop/rebuild, container_id clearing, and early-return on
terminal state.

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

from __future__ import annotations

import json
import subprocess
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,
)
from cleveragents.resource.handlers.devcontainer import (
    activate_container,
    get_lifecycle_tracker,
    rebuild_container,
    set_lifecycle_tracker,
    stop_container,
)
from features.mocks.mock_devcontainer_cli import (
    MockDevcontainerRunner,
    MockProcessResult,
)

# ── Given steps ──────────────────────────────────────────────


@given("a mock devcontainer CLI runner")
def step_create_mock_runner(context: Context) -> None:
    context.mock_runner = MockDevcontainerRunner()


@given("the runner configured for successful activation")
def step_configure_success(context: Context) -> None:
    context.mock_runner.set_up_result(
        container_id="aabbccddee0011223344",
        workspace="/workspaces/project",
    )


@given("the runner configured for failed activation")
def step_configure_failure(context: Context) -> None:
    context.mock_runner.set_up_failure(stderr="container build failed")


@given('an active container "{resource_id}" with container ID "{ctr_id}"')
def step_create_active_container(
    context: Context, resource_id: str, ctr_id: str
) -> None:
    tracker = ContainerLifecycleTracker(
        resource_id=resource_id,
        current_state=ContainerLifecycleState.RUNNING,
        container_id=ctr_id,
        workspace_path="/workspaces/project",
    )
    set_lifecycle_tracker(tracker)


@given('a stopped container "{resource_id}"')
def step_create_stopped_container(context: Context, resource_id: str) -> None:
    tracker = ContainerLifecycleTracker(
        resource_id=resource_id,
        current_state=ContainerLifecycleState.STOPPED,
    )
    set_lifecycle_tracker(tracker)


@given('an errored container "{resource_id}"')
def step_create_errored_container(context: Context, resource_id: str) -> None:
    tracker = ContainerLifecycleTracker(
        resource_id=resource_id,
        current_state=ContainerLifecycleState.FAILED,
    )
    set_lifecycle_tracker(tracker)


@given('an active container with no container_id "{resource_id}"')
def step_create_active_no_ctr_id(context: Context, resource_id: str) -> None:
    tracker = ContainerLifecycleTracker(
        resource_id=resource_id,
        current_state=ContainerLifecycleState.RUNNING,
        container_id=None,
        workspace_path="/workspaces/project",
    )
    set_lifecycle_tracker(tracker)


@given("the runner configured for activation with no container_id")
def step_configure_activation_no_ctr_id(context: Context) -> None:
    """Configure devcontainer up to succeed but return no containerId."""
    context.mock_runner.up_result = MockProcessResult(
        returncode=0,
        stdout=json.dumps({"outcome": "success"}),
        stderr="",
    )


@given("the runner configured for failed docker stop")
def step_configure_stop_failure_rc(context: Context) -> None:
    """Configure docker stop to return a non-zero exit code (not raise)."""
    context.mock_runner.set_stop_result(returncode=1)


@given("the runner configured to raise exception on stop")
def step_configure_stop_exception(context: Context) -> None:
    """Replace mock_runner with a wrapper that raises on docker stop."""
    inner = context.mock_runner

    def raising_runner(args: list[str], **kwargs: object) -> object:
        inner.calls.append((list(args), dict(kwargs)))
        if len(args) >= 2 and args[0] == "docker" and args[1] == "stop":
            raise OSError("docker daemon not running")
        return inner(args, **kwargs)

    context.mock_runner = cast(MockDevcontainerRunner, raising_runner)


@given("the runner configured to raise generic exception on up")
def step_configure_up_generic_exception(context: Context) -> None:
    """Replace mock_runner with a wrapper that raises on devcontainer up."""
    inner = context.mock_runner

    def raising_runner(args: list[str], **kwargs: object) -> object:
        inner.calls.append((list(args), dict(kwargs)))
        if len(args) >= 2 and args[0] == "devcontainer" and args[1] == "up":
            raise OSError("network unreachable")
        return inner(args, **kwargs)

    context.mock_runner = cast(MockDevcontainerRunner, raising_runner)


@given("a mock devcontainer CLI runner that times out")
def step_create_timeout_runner(context: Context) -> None:
    """Create a runner that raises subprocess.TimeoutExpired."""

    def _timeout_runner(args: list[str], **kwargs: object) -> None:
        raise subprocess.TimeoutExpired(cmd=args, timeout=300)

    context.mock_runner = _timeout_runner


@given("a mock devcontainer CLI runner that times out with orphan containers")
def step_create_timeout_orphan_runner(context: Context) -> None:
    """Create a runner that times out on devcontainer up but returns
    orphan container IDs on docker ps.
    """
    call_log: list[tuple[list[str], dict[str, object]]] = []

    def _runner(args: list[str], **kwargs: object) -> object:
        call_log.append((list(args), dict(kwargs)))
        if len(args) >= 2 and args[0] == "devcontainer" and args[1] == "up":
            raise subprocess.TimeoutExpired(cmd=args, timeout=300)
        if len(args) >= 2 and args[0] == "docker" and args[1] == "ps":
            return MockProcessResult(returncode=0, stdout="orphan123abc\n", stderr="")
        if len(args) >= 2 and args[0] == "docker" and args[1] == "stop":
            return MockProcessResult(returncode=0)
        return MockProcessResult(returncode=0)

    context.mock_runner = _runner
    context.orphan_call_log = call_log


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


@when('I activate container "{resource_id}" at "{workspace}"')
def step_activate(context: Context, resource_id: str, workspace: str) -> None:
    context.activation_error = None
    try:
        context.activated_tracker = activate_container(
            resource_id,
            workspace,
            run_command=context.mock_runner,
        )
    except (RuntimeError, ValueError) as exc:
        context.activation_error = exc


@when('I attempt to activate container "{resource_id}" at "{workspace}"')
def step_attempt_activate(context: Context, resource_id: str, workspace: str) -> None:
    context.activation_error = None
    try:
        context.activated_tracker = activate_container(
            resource_id,
            workspace,
            run_command=getattr(context, "mock_runner", None),
        )
    except (RuntimeError, ValueError) as exc:
        context.activation_error = exc


@when('I stop container "{resource_id}"')
def step_stop(context: Context, resource_id: str) -> None:
    context.stop_error = None
    try:
        stop_container(
            resource_id,
            run_command=context.mock_runner,
        )
    except (RuntimeError, ValueError) as exc:
        context.stop_error = exc


@when('I attempt to stop container "{resource_id}"')
def step_attempt_stop(context: Context, resource_id: str) -> None:
    context.stop_error = None
    try:
        stop_container(
            resource_id,
            run_command=getattr(context, "mock_runner", None),
        )
    except (RuntimeError, ValueError) as exc:
        context.stop_error = exc


@when('I rebuild container "{resource_id}" at "{workspace}"')
def step_rebuild(context: Context, resource_id: str, workspace: str) -> None:
    context.rebuild_error = None
    try:
        rebuild_container(
            resource_id,
            workspace,
            run_command=context.mock_runner,
        )
    except (RuntimeError, ValueError) as exc:
        context.rebuild_error = exc


@when('I call rebuild_container for "{resource_id}" at "{workspace}"')
def step_rebuild_direct(context: Context, resource_id: str, workspace: str) -> None:
    context.rebuild_error = None
    try:
        rebuild_container(
            resource_id,
            workspace,
            run_command=context.mock_runner,
        )
    except (RuntimeError, ValueError) as exc:
        context.rebuild_error = exc


@when("I call rebuild_container with empty resource_id")
def step_rebuild_empty_resource_id(context: Context) -> None:
    context.rebuild_error = None
    try:
        rebuild_container("", "/workspace")
    except (RuntimeError, ValueError) as exc:
        context.rebuild_error = exc


@when("I call rebuild_container with empty workspace_folder")
def step_rebuild_empty_workspace(context: Context) -> None:
    context.rebuild_error = None
    try:
        rebuild_container("01TESTLIFECYCLE0000000091", "")
    except (RuntimeError, ValueError) as exc:
        context.rebuild_error = exc


@when("I attempt to activate a container with empty resource_id")
def step_activate_empty_resource_id(context: Context) -> None:
    context.activation_error = None
    try:
        activate_container("", "/workspace")
    except (RuntimeError, ValueError) as exc:
        context.activation_error = exc


@when("I attempt to activate a container with empty workspace_folder")
def step_activate_empty_workspace(context: Context) -> None:
    context.activation_error = None
    try:
        activate_container("01TESTLIFECYCLE0000000080", "")
    except (RuntimeError, ValueError) as exc:
        context.activation_error = exc


@when("I attempt to stop a container with empty resource_id")
def step_stop_empty_resource_id(context: Context) -> None:
    context.stop_error = None
    try:
        stop_container("")
    except (RuntimeError, ValueError) as exc:
        context.stop_error = exc


@when("I attempt to activate a container with relative workspace_folder")
def step_activate_relative_workspace(context: Context) -> None:
    context.activation_error = None
    try:
        activate_container("01TESTLIFECYCLE0000000080", "relative/path")
    except (RuntimeError, ValueError) as exc:
        context.activation_error = exc


@when("I attempt to activate the timed-out container")
def step_activate_timeout(context: Context) -> None:
    context.activation_error = None
    try:
        activate_container(
            context.resource_id,
            "/workspace",
            run_command=context.mock_runner,
        )
    except (RuntimeError, ValueError) as exc:
        context.activation_error = exc


@when("I attempt to activate the timed-out container with orphan cleanup")
def step_activate_timeout_orphan(context: Context) -> None:
    context.activation_error = None
    try:
        activate_container(
            context.resource_id,
            "/workspace",
            run_command=context.mock_runner,
        )
    except (RuntimeError, ValueError) as exc:
        context.activation_error = exc


@when('I stop container "{resource_id}" expecting early return')
def step_stop_container_early_return(context: Context, resource_id: str) -> None:
    """Stop a container that is already in a terminal state."""
    context.stop_result = stop_container(
        resource_id,
        run_command=context.mock_runner,
    )


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


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


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


@then('the workspace path should be "{ws_path}"')
def step_check_workspace_path(context: Context, ws_path: str) -> None:
    assert context.activated_tracker.workspace_path == ws_path


@then("the runner should have received a devcontainer up call")
def step_check_up_call(context: Context) -> None:
    assert len(context.mock_runner.up_calls) > 0


@then("the activation should raise RuntimeError")
def step_check_activation_runtime_error(context: Context) -> None:
    assert context.activation_error is not None
    assert isinstance(context.activation_error, RuntimeError)


@then('the container state for "{resource_id}" should be "{state}"')
def step_check_specific_state(context: Context, resource_id: str, state: str) -> None:
    tracker = get_lifecycle_tracker(resource_id)
    assert tracker.current_state == ContainerLifecycleState(state), (
        f"Expected {state}, got {tracker.current_state.value}"
    )


@then("the runner should have received a docker stop call")
def step_check_stop_call(context: Context) -> None:
    assert len(context.mock_runner.stop_calls) > 0


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


@then('the activation should raise ValueError with "{keyword}"')
def step_check_activation_value_error(context: Context, keyword: str) -> None:
    assert context.activation_error is not None
    assert isinstance(context.activation_error, ValueError)
    assert keyword in str(context.activation_error)


@then('the stop should raise ValueError with "{keyword}"')
def step_check_stop_value_error(context: Context, keyword: str) -> None:
    assert context.stop_error is not None
    assert isinstance(context.stop_error, ValueError)
    assert keyword in str(context.stop_error)


@then("the stop should raise RuntimeError")
def step_check_stop_runtime_error(context: Context) -> None:
    assert context.stop_error is not None
    assert isinstance(context.stop_error, RuntimeError)


@then('the rebuild should raise ValueError with "{keyword}"')
def step_check_rebuild_value_error(context: Context, keyword: str) -> None:
    assert context.rebuild_error is not None
    assert isinstance(context.rebuild_error, ValueError)
    assert keyword in str(context.rebuild_error)


@then('the activation should raise RuntimeError with "{text}"')
def step_check_runtime_error(context: Context, text: str) -> None:
    assert context.activation_error is not None, "Expected an error but none was raised"
    assert isinstance(context.activation_error, RuntimeError), (
        f"Expected RuntimeError, got {type(context.activation_error).__name__}"
    )
    assert text in str(context.activation_error), (
        f"Expected '{text}' in error message, got '{context.activation_error}'"
    )


@then('the tracker "{resource_id}" should be in "{state}" state')
def step_check_named_tracker_state(
    context: Context, resource_id: str, state: str
) -> None:
    tracker = get_lifecycle_tracker(resource_id)
    assert tracker.current_state == ContainerLifecycleState(state), (
        f"Expected state '{state}', got '{tracker.current_state.value}'"
    )


@then('the container_id for "{resource_id}" should be None')
def step_check_container_id_none(context: Context, resource_id: str) -> None:
    tracker = get_lifecycle_tracker(resource_id)
    assert tracker.container_id is None, (
        f"Expected container_id None, got '{tracker.container_id}'"
    )


@then('the workspace_path for "{resource_id}" should be None')
def step_check_workspace_path_none(context: Context, resource_id: str) -> None:
    tracker = get_lifecycle_tracker(resource_id)
    assert tracker.workspace_path is None, (
        f"Expected workspace_path None, got '{tracker.workspace_path}'"
    )


@then('the host_workspace_path for "{resource_id}" should be "{path}"')
def step_check_host_workspace_path(
    context: Context, resource_id: str, path: str
) -> None:
    tracker = get_lifecycle_tracker(resource_id)
    assert tracker.host_workspace_path == path, (
        f"Expected host_workspace_path '{path}', got '{tracker.host_workspace_path}'"
    )


@then('the devcontainer up call should include "{flag}"')
def step_check_up_call_includes_flag(context: Context, flag: str) -> None:
    up_calls = context.mock_runner.up_calls
    assert len(up_calls) > 0, "No devcontainer up calls recorded"
    args, _ = up_calls[-1]
    assert flag in args, f"Expected '{flag}' in {args}"


@then('the devcontainer up call should not include "{flag}"')
def step_check_up_call_excludes_flag(context: Context, flag: str) -> None:
    up_calls = context.mock_runner.up_calls
    assert len(up_calls) > 0, "No devcontainer up calls recorded"
    args, _ = up_calls[-1]
    assert flag not in args, f"Did not expect '{flag}' in {args}"


@then("the runner should have attempted orphan container cleanup")
def step_check_orphan_cleanup(context: Context) -> None:
    ps_calls = [
        (a, k)
        for a, k in context.orphan_call_log
        if len(a) >= 2 and a[0] == "docker" and a[1] == "ps"
    ]
    assert len(ps_calls) >= 1, "Expected docker ps call for orphan lookup"
    stop_calls = [
        (a, k)
        for a, k in context.orphan_call_log
        if len(a) >= 2 and a[0] == "docker" and a[1] == "stop"
    ]
    assert len(stop_calls) >= 1, "Expected docker stop call for orphan cleanup (F1 fix)"
