"""Step definitions for container_clone_into.feature.

Tests the CloneIntoHandler module: URL validation, argument validation,
successful clone, failure handling, and container-instance resource type
CLI argument registration.
"""

from __future__ import annotations

from typing import Any
from unittest.mock import MagicMock, patch

from behave import given, then, when  # type: ignore[import-untyped]
from behave.runner import Context  # type: ignore[import-untyped]

from cleveragents.application.services._resource_registry_data import (
    BUILTIN_TYPES,
)
from cleveragents.domain.models.core.container_lifecycle import (
    ContainerLifecycleState,
    ContainerLifecycleTracker,
)
from cleveragents.resource.handlers.clone_into import (
    CloneIntoError,
    clone_repo_into_container,
    validate_clone_into_url,
)
from cleveragents.resource.handlers.devcontainer import (
    DevcontainerHandler,
    clear_lifecycle_registry,
    set_lifecycle_tracker,
)
from cleveragents.domain.models.core.resource import Resource

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


@when('I validate clone-into URL "{url}"')
def step_validate_url(context: Context, url: str) -> None:
    """Validate a clone-into URL."""
    context.clone_url_error = None
    try:
        validate_clone_into_url(url)
        context.clone_url_valid = True
    except ValueError as exc:
        context.clone_url_valid = False
        context.clone_url_error = exc


@when("I validate clone-into URL with whitespace only")
def step_validate_whitespace_url(context: Context) -> None:
    """Validate a whitespace-only clone-into URL."""
    context.clone_url_error = None
    try:
        validate_clone_into_url("   ")
        context.clone_url_valid = True
    except ValueError as exc:
        context.clone_url_valid = False
        context.clone_url_error = exc


@when("I call clone_repo_into_container with empty container_id")
def step_clone_empty_container_id(context: Context) -> None:
    """Call clone_repo_into_container with an empty container_id."""
    context.clone_error = None
    try:
        clone_repo_into_container("", "https://github.com/org/repo.git")
    except ValueError as exc:
        context.clone_error = exc


@when("I call clone_repo_into_container with empty repo_url")
def step_clone_empty_repo_url(context: Context) -> None:
    """Call clone_repo_into_container with an empty repo_url."""
    context.clone_error = None
    try:
        clone_repo_into_container("abc123def456", "")
    except ValueError as exc:
        context.clone_error = exc


@when('I clone "{repo_url}" into container "{container_id}"')
def step_clone_into_container(
    context: Context, repo_url: str, container_id: str
) -> None:
    """Clone a repository into a container."""
    context.clone_error = None
    context.clone_result = None
    try:
        context.clone_result = clone_repo_into_container(container_id, repo_url)
    except (CloneIntoError, ValueError) as exc:
        context.clone_error = exc


@when(
    'I clone repo "{repo_url}" into container "{container_id}" with target "{target_dir}"'
)
def step_clone_into_container_at(
    context: Context, repo_url: str, container_id: str, target_dir: str
) -> None:
    """Clone a repository into a container at a specific target directory."""
    context.clone_error = None
    context.clone_result = None
    try:
        context.clone_result = clone_repo_into_container(
            container_id, repo_url, target_dir
        )
    except (CloneIntoError, ValueError) as exc:
        context.clone_error = exc


@when('I look up the "{type_name}" resource type spec')
def step_look_up_resource_type_spec(context: Context, type_name: str) -> None:
    """Look up a resource type spec from BUILTIN_TYPES."""
    context.resource_type_spec = None
    for entry in BUILTIN_TYPES:
        if entry.get("name") == type_name:
            context.resource_type_spec = entry
            break


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


@given("a mock docker exec that succeeds for clone")
def step_mock_docker_exec_success(context: Context) -> None:
    """Patch subprocess.run to simulate a successful docker exec git clone."""
    mock_result = MagicMock()
    mock_result.returncode = 0
    mock_result.stdout = ""
    mock_result.stderr = ""

    patcher = patch(
        "cleveragents.resource.handlers.clone_into.subprocess.run",
        return_value=mock_result,
    )
    patcher.start()
    context.add_cleanup(patcher.stop)


@given('a mock docker exec that fails for clone with stderr "{stderr_msg}"')
def step_mock_docker_exec_failure(context: Context, stderr_msg: str) -> None:
    """Patch subprocess.run to simulate a failed docker exec git clone."""
    mock_result = MagicMock()
    mock_result.returncode = 1
    mock_result.stdout = ""
    mock_result.stderr = stderr_msg

    patcher = patch(
        "cleveragents.resource.handlers.clone_into.subprocess.run",
        return_value=mock_result,
    )
    patcher.start()
    context.add_cleanup(patcher.stop)


@given(
    'a CloneIntoError with repo "{repo_url}" container "{container_id}" stderr "{stderr}"'
)
def step_create_clone_into_error(
    context: Context, repo_url: str, container_id: str, stderr: str
) -> None:
    """Create a CloneIntoError instance for attribute testing."""
    context.clone_into_error = CloneIntoError(
        repo_url=repo_url,
        container_id=container_id,
        stderr=stderr,
    )


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


@then("the clone-into URL should be valid")
def step_url_should_be_valid(context: Context) -> None:
    assert context.clone_url_valid is True, (
        "Expected URL to be valid but validate_clone_into_url returned False"
    )


@then("the clone-into URL should be invalid")
def step_url_should_be_invalid(context: Context) -> None:
    assert context.clone_url_valid is False, (
        "Expected URL to be invalid but validate_clone_into_url returned True"
    )


@then('it should raise ValueError mentioning "{field}"')
def step_should_raise_value_error(context: Context, field: str) -> None:
    assert context.clone_error is not None, (
        "Expected a ValueError but no error was raised"
    )
    assert isinstance(context.clone_error, ValueError), (
        f"Expected ValueError, got {type(context.clone_error).__name__}"
    )
    assert field.lower() in str(context.clone_error).lower(), (
        f"Expected '{field}' in error message, got: {context.clone_error!r}"
    )


@then("the clone should succeed")
def step_clone_should_succeed(context: Context) -> None:
    assert context.clone_error is None, (
        f"Expected clone to succeed but got error: {context.clone_error!r}"
    )
    assert context.clone_result is not None, "Expected a clone result but got None"


@then('the clone target should be "{expected_target}"')
def step_clone_target(context: Context, expected_target: str) -> None:
    assert context.clone_result == expected_target, (
        f"Expected clone target '{expected_target}', got '{context.clone_result}'"
    )


@then("the clone should raise CloneIntoError")
def step_clone_should_raise(context: Context) -> None:
    assert context.clone_error is not None, (
        "Expected CloneIntoError but no error was raised"
    )
    assert isinstance(context.clone_error, CloneIntoError), (
        f"Expected CloneIntoError, got {type(context.clone_error).__name__}"
    )


@then('the CloneIntoError should mention "{text}"')
def step_clone_error_mentions(context: Context, text: str) -> None:
    error_str = str(context.clone_error)
    assert text.lower() in error_str.lower(), (
        f"Expected '{text}' in CloneIntoError message, got: {error_str!r}"
    )


@then('the CloneIntoError repo_url should be "{expected}"')
def step_clone_error_repo_url(context: Context, expected: str) -> None:
    assert context.clone_into_error.repo_url == expected, (
        f"Expected repo_url '{expected}', got '{context.clone_into_error.repo_url}'"
    )


@then('the CloneIntoError container_id should be "{expected}"')
def step_clone_error_container_id(context: Context, expected: str) -> None:
    assert context.clone_into_error.container_id == expected, (
        f"Expected container_id '{expected}', "
        f"got '{context.clone_into_error.container_id}'"
    )


@then('the CloneIntoError stderr should be "{expected}"')
def step_clone_error_stderr(context: Context, expected: str) -> None:
    assert context.clone_into_error.stderr == expected, (
        f"Expected stderr '{expected}', got '{context.clone_into_error.stderr}'"
    )


@then('the spec should have a CLI argument named "{arg_name}"')
def step_spec_has_cli_arg(context: Context, arg_name: str) -> None:
    spec = context.resource_type_spec
    assert spec is not None, "No resource type spec found"
    cli_args: list[dict[str, Any]] = spec.get("cli_args", [])
    arg_names = [arg.get("name") for arg in cli_args]
    assert arg_name in arg_names, f"Expected CLI argument '{arg_name}' in {arg_names}"


@then('the clone-into argument should be of type "{expected_type}"')
def step_clone_into_arg_type(context: Context, expected_type: str) -> None:
    spec = context.resource_type_spec
    assert spec is not None, "No resource type spec found"
    cli_args: list[dict[str, Any]] = spec.get("cli_args", [])
    clone_into_arg = next(
        (arg for arg in cli_args if arg.get("name") == "clone-into"), None
    )
    assert clone_into_arg is not None, "clone-into argument not found"
    assert clone_into_arg.get("type") == expected_type, (
        f"Expected type '{expected_type}', got '{clone_into_arg.get('type')}'"
    )


@then("the clone-into argument should not be required")
def step_clone_into_not_required(context: Context) -> None:
    spec = context.resource_type_spec
    assert spec is not None, "No resource type spec found"
    cli_args: list[dict[str, Any]] = spec.get("cli_args", [])
    clone_into_arg = next(
        (arg for arg in cli_args if arg.get("name") == "clone-into"), None
    )
    assert clone_into_arg is not None, "clone-into argument not found"
    assert clone_into_arg.get("required") is False, (
        f"Expected clone-into to be optional (required=False), "
        f"got required={clone_into_arg.get('required')}"
    )


# -- End-to-end handler wiring steps --


@given(
    'a devcontainer-instance resource with clone_into "{clone_url}" and tracker container_id "{container_id}"'
)
def step_devcontainer_resource_with_clone_into(
    context: Context, clone_url: str, container_id: str
) -> None:
    """Set up a devcontainer-instance resource with clone_into property and tracker."""
    # Clear any existing lifecycle state
    clear_lifecycle_registry()

    # Create a resource with clone_into in properties
    # resource_id must be a valid ULID (Crockford Base32, 26 chars, no I/L/O/U)
    # classification must be 'physical' or 'virtual'
    context.handler_resource = Resource(
        resource_id="01ARZ3NDEKTSV4RRFFQ69G5FAV",
        name="test-devcontainer",
        resource_type_name="devcontainer-instance",
        classification="physical",
        description="Test devcontainer for clone-into wiring",
        location="/tmp/test-workspace",
        properties={"clone_into": clone_url},
    )

    # Set up the lifecycle tracker in DISCOVERED state with container_id pre-set
    # (simulates the state after activate_container() has run and set container_id)
    tracker = ContainerLifecycleTracker(
        resource_id="01ARZ3NDEKTSV4RRFFQ69G5FAV",
        current_state=ContainerLifecycleState.DISCOVERED,
        container_id=container_id,
    )
    set_lifecycle_tracker(tracker)

    context.handler_clone_url = clone_url
    context.handler_container_id = container_id

    # Store cleanup reference
    context.add_cleanup(clear_lifecycle_registry)


@given("clone_repo_into_container is mocked to succeed")
def step_mock_clone_repo_into_container(context: Context) -> None:
    """Mock clone_repo_into_container to capture calls without running docker."""
    mock_clone = MagicMock(return_value="/workspace")
    patcher = patch(
        "cleveragents.resource.handlers.devcontainer.clone_repo_into_container",
        mock_clone,
    )
    patcher.start()
    context.mock_clone_repo = mock_clone
    context.add_cleanup(patcher.stop)


@when("DevcontainerHandler.resolve() is called for the resource")
def step_call_devcontainer_handler_resolve(context: Context) -> None:
    """Call DevcontainerHandler.resolve() with the test resource."""
    handler = DevcontainerHandler()
    mock_sandbox_manager = MagicMock()
    mock_bound_resource = MagicMock()

    # Patch activate_container to be a no-op (tracker already has container_id)
    # Patch BaseResourceHandler.resolve to return a mock BoundResource
    with (
        patch("cleveragents.resource.handlers.devcontainer.activate_container"),
        patch(
            "cleveragents.resource.handlers._base.BaseResourceHandler.resolve",
            return_value=mock_bound_resource,
        ),
    ):
        context.handler_resolve_error = None
        context.handler_resolve_result = None
        try:
            context.handler_resolve_result = handler.resolve(
                resource=context.handler_resource,
                plan_id="test-plan-001",
                slot_name="test-slot",
                sandbox_manager=mock_sandbox_manager,
            )
        except Exception as exc:
            context.handler_resolve_error = exc


@then(
    'clone_repo_into_container should have been called with container_id "{expected_container_id}" and url "{expected_url}"'
)
def step_assert_clone_called(
    context: Context, expected_container_id: str, expected_url: str
) -> None:
    """Assert that clone_repo_into_container was called with the expected arguments."""
    assert context.handler_resolve_error is None, (
        f"Expected resolve() to succeed but got error: {context.handler_resolve_error!r}"
    )
    mock_clone = context.mock_clone_repo
    assert mock_clone.called, (
        "Expected clone_repo_into_container to have been called, but it was not"
    )
    call_args = mock_clone.call_args
    actual_container_id = (
        call_args[0][0] if call_args[0] else call_args[1].get("container_id")
    )
    actual_url = (
        call_args[0][1] if len(call_args[0]) > 1 else call_args[1].get("repo_url")
    )
    assert actual_container_id == expected_container_id, (
        f"Expected container_id '{expected_container_id}', got '{actual_container_id}'"
    )
    assert actual_url == expected_url, (
        f"Expected url '{expected_url}', got '{actual_url}'"
    )
