"""Step definitions for CLI main shortcut coverage scenarios."""

from __future__ import annotations

import ast
from typing import Any
from unittest.mock import patch

import typer
from behave import then, when
from typer.testing import CliRunner

from cleveragents.cli.main import app, main

# Map shortcut names to their underlying implementation targets.
SHORTCUT_TARGETS = {
    "tell": "cleveragents.cli.commands.plan.tell",
    "build": "cleveragents.cli.commands.plan.build",
    "apply": "cleveragents.cli.commands.plan.lifecycle_apply_plan",
    "context-load": "cleveragents.cli.commands.context.context_add",
    "context-add": "cleveragents.cli.commands.context.context_add",
}


@when('I execute the CLI shortcut "{shortcut}" with argument list {arg_literal}')
def step_execute_cli_shortcut(context: Any, shortcut: str, arg_literal: str) -> None:
    """Invoke a CLI shortcut while patching the underlying implementation."""

    if shortcut not in SHORTCUT_TARGETS:
        raise AssertionError(f"Unhandled CLI shortcut: {shortcut}")

    try:
        parsed_args = ast.literal_eval(arg_literal)
    except Exception as exc:  # pragma: no cover - defensive guard
        raise AssertionError(
            f"Argument list must be a valid Python list literal: {arg_literal}"
        ) from exc

    if not isinstance(parsed_args, list):
        raise AssertionError(f"Argument list must evaluate to a list: {arg_literal}")

    runner = CliRunner()
    target = SHORTCUT_TARGETS[shortcut]

    with patch(target) as mock_target:
        result = runner.invoke(app, [shortcut, *parsed_args])

    if not mock_target.called:
        raise AssertionError(f"Expected shortcut '{shortcut}' to call {target}")

    # Record results for subsequent assertions.
    context.shortcut_name = shortcut
    context.shortcut_result = result
    if (
        mock_target.call_args
    ):  # pragma: no branch - call_args always present given the assertion above
        context.shortcut_call_args = list(mock_target.call_args.args)
        context.shortcut_call_kwargs = dict(mock_target.call_args.kwargs)
    else:  # pragma: no cover - defensive guard
        context.shortcut_call_args = []
        context.shortcut_call_kwargs = {}


@then("the shortcut should exit with code {expected:d}")
def step_shortcut_exit_code(context: Any, expected: int) -> None:
    """Ensure the CLI shortcut returned the expected exit code."""

    result = getattr(context, "shortcut_result", None)
    if result is None:
        raise AssertionError("Shortcut result was not captured")
    assert result.exit_code == expected, (
        f"Unexpected exit code for {context.shortcut_name}: "
        f"expected {expected}, got {result.exit_code}"
    )


@then("the shortcut should forward keyword arguments {expected_literal}")
def step_shortcut_forward_kwargs(context: Any, expected_literal: str) -> None:
    """Verify the underlying command received the correct keyword arguments."""

    try:
        expected_kwargs = ast.literal_eval(expected_literal)
    except Exception as exc:  # pragma: no cover - defensive guard
        raise AssertionError(
            f"Expected keyword arguments must be a valid Python dict literal: {expected_literal}"
        ) from exc

    if not isinstance(expected_kwargs, dict):
        raise AssertionError(
            f"Expected keyword arguments must evaluate to a dict: {expected_literal}"
        )

    actual_kwargs = getattr(context, "shortcut_call_kwargs", None)
    assert actual_kwargs == expected_kwargs, (
        f"Forwarded kwargs mismatch for {context.shortcut_name}: "
        f"expected {expected_kwargs}, got {actual_kwargs}"
    )


@when('I execute main with stubbed return "{mode}" and value {value}')
def step_execute_main_with_stub(context: Any, mode: str, value: str) -> None:
    """Run main() with the Typer app patched to return specific results."""

    try:
        numeric_value = int(value)
    except ValueError as exc:
        raise AssertionError(
            f"Value parameter must be an integer literal, got: {value}"
        ) from exc

    call_tracker: dict[str, Any] = {"called": False}

    def stubbed_app(args: list[str], standalone_mode: bool = False) -> Any:
        call_tracker["called"] = True
        call_tracker["args"] = list(args)
        call_tracker["standalone_mode"] = standalone_mode

        if mode == "exit":
            return typer.Exit(numeric_value)
        if mode == "abort":
            return typer.Abort()
        if mode == "integer":
            return numeric_value
        if mode == "none":
            return None
        if mode == "raise_abort":
            raise typer.Abort()
        raise AssertionError(f"Unhandled stubbed mode: {mode}")

    with patch("cleveragents.cli.main.app", new=stubbed_app):
        context.main_result = main(["--help"])

    context.stub_tracker = call_tracker

    if not call_tracker.get("called"):
        raise AssertionError("Patched Typer app was not invoked by main()")


@then("the Typer app should have received {arg_literal} with standalone mode disabled")
def step_verify_stub_invocation(context: Any, arg_literal: str) -> None:
    """Ensure the patched Typer app was called with expected arguments."""

    try:
        expected_args = ast.literal_eval(arg_literal)
    except Exception as exc:  # pragma: no cover - defensive guard
        raise AssertionError(
            f"Expected argument list must be a valid Python literal: {arg_literal}"
        ) from exc

    if not isinstance(expected_args, list):
        raise AssertionError(
            f"Expected argument list must evaluate to a list: {arg_literal}"
        )

    tracker = getattr(context, "stub_tracker", None)
    if tracker is None:
        raise AssertionError("No stub tracker data found on context")

    actual_args = tracker.get("args")
    assert actual_args == expected_args, (
        f"Patched Typer app received args {actual_args}, expected {expected_args}"
    )

    standalone_mode = tracker.get("standalone_mode")
    assert standalone_mode is False, (
        "Expected standalone_mode to be False for main() invocation"
    )
