"""Step definitions for CLI consistency and UX polish feature."""

from __future__ import annotations

import json
from io import StringIO
from unittest.mock import patch

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

# ------------------------------------------------------------------
# Helpers
# ------------------------------------------------------------------


def _call_cli_error(
    context: Context,
    message: str,
    hint: str | None = None,
    exit_code: int | None = None,
) -> None:
    """Invoke cli_error with a captured stderr console."""
    from rich.console import Console

    from cleveragents.cli.errors import cli_error

    buf = StringIO()
    mock_console = Console(file=buf, stderr=True, no_color=True)
    kwargs: dict[str, object] = {}
    if hint is not None:
        kwargs["hint"] = hint
    if exit_code is not None:
        kwargs["exit_code"] = exit_code
    with patch("cleveragents.cli.errors._get_err_console", return_value=mock_console):
        try:
            cli_error(message, **kwargs)  # type: ignore[arg-type]
        except SystemExit as exc:
            context.exit_code = exc.code
    context.stderr_capture = buf


def _call_cli_warning(
    context: Context,
    message: str,
    hint: str | None = None,
) -> None:
    """Invoke cli_warning with a captured stderr console."""
    from rich.console import Console

    from cleveragents.cli.errors import cli_warning

    buf = StringIO()
    mock_console = Console(file=buf, stderr=True, no_color=True)
    with patch("cleveragents.cli.errors._get_err_console", return_value=mock_console):
        if hint is not None:
            cli_warning(message, hint=hint)
        else:
            cli_warning(message)
    context.stderr_capture = buf


def _call_cli_not_found(
    context: Context,
    resource_type: str,
    name: str,
) -> None:
    """Invoke cli_not_found with a captured stderr console."""
    from rich.console import Console

    from cleveragents.cli.errors import cli_not_found

    buf = StringIO()
    mock_console = Console(file=buf, stderr=True, no_color=True)
    with patch("cleveragents.cli.errors._get_err_console", return_value=mock_console):
        try:
            cli_not_found(resource_type, name)
        except SystemExit as exc:
            context.exit_code = exc.code
    context.stderr_capture = buf


# ------------------------------------------------------------------
# Constants module
# ------------------------------------------------------------------


@given("the CLI constants module is imported")
def step_import_constants(context: Context) -> None:
    from cleveragents.cli.constants import (
        DEFAULT_FORMAT,
        EXIT_CONFLICT,
        EXIT_ERROR,
        EXIT_NOT_FOUND,
        EXIT_SUCCESS,
        EXIT_USAGE,
        FORMAT_HELP,
        FORMAT_JSON,
        FORMAT_TABLE,
        FORMAT_TEXT,
        VALID_FORMATS,
    )

    context.const = {
        "EXIT_SUCCESS": EXIT_SUCCESS,
        "EXIT_ERROR": EXIT_ERROR,
        "EXIT_USAGE": EXIT_USAGE,
        "EXIT_NOT_FOUND": EXIT_NOT_FOUND,
        "EXIT_CONFLICT": EXIT_CONFLICT,
        "FORMAT_HELP": FORMAT_HELP,
        "DEFAULT_FORMAT": DEFAULT_FORMAT,
        "VALID_FORMATS": VALID_FORMATS,
        "FORMAT_TEXT": FORMAT_TEXT,
        "FORMAT_JSON": FORMAT_JSON,
        "FORMAT_TABLE": FORMAT_TABLE,
    }


@then("EXIT_SUCCESS should equal {value:d}")
def step_exit_success_equals(context: Context, value: int) -> None:
    assert context.const["EXIT_SUCCESS"] == value


@then("EXIT_ERROR should equal {value:d}")
def step_exit_error_equals(context: Context, value: int) -> None:
    assert context.const["EXIT_ERROR"] == value


@then("EXIT_USAGE should equal {value:d}")
def step_exit_usage_equals(context: Context, value: int) -> None:
    assert context.const["EXIT_USAGE"] == value


@then("EXIT_NOT_FOUND should equal {value:d}")
def step_exit_not_found_equals(context: Context, value: int) -> None:
    assert context.const["EXIT_NOT_FOUND"] == value


@then("EXIT_CONFLICT should equal {value:d}")
def step_exit_conflict_equals(context: Context, value: int) -> None:
    assert context.const["EXIT_CONFLICT"] == value


@then('FORMAT_HELP should mention "{text}"')
def step_format_help_mentions(context: Context, text: str) -> None:
    assert text in context.const["FORMAT_HELP"], (
        f"Expected '{text}' in FORMAT_HELP: {context.const['FORMAT_HELP']}"
    )


@then('DEFAULT_FORMAT should equal "{value}"')
def step_default_format_equals(context: Context, value: str) -> None:
    assert context.const["DEFAULT_FORMAT"] == value


@then('VALID_FORMATS should contain "{value}"')
def step_valid_formats_contains(context: Context, value: str) -> None:
    assert value in context.const["VALID_FORMATS"]


@then('FORMAT_TEXT should equal "{value}"')
def step_format_text_equals(context: Context, value: str) -> None:
    assert context.const["FORMAT_TEXT"] == value


@then('FORMAT_JSON should equal "{value}"')
def step_format_json_equals(context: Context, value: str) -> None:
    assert context.const["FORMAT_JSON"] == value


@then('FORMAT_TABLE should equal "{value}"')
def step_format_table_equals(context: Context, value: str) -> None:
    assert context.const["FORMAT_TABLE"] == value


# ------------------------------------------------------------------
# Error formatting  (parameterised steps matching feature scenarios)
# ------------------------------------------------------------------


@given("the CLI errors module is imported")
def step_import_errors(context: Context) -> None:
    context.stderr_capture = StringIO()
    context.exit_code = None


@when('I call cli_error with only message "{message}"')
def step_call_cli_error_message(context: Context, message: str) -> None:
    _call_cli_error(context, message)


@when('I call cli_error with message "{message}" and hint "{hint}"')
def step_call_cli_error_message_hint(context: Context, message: str, hint: str) -> None:
    _call_cli_error(context, message, hint=hint)


@when('I call cli_error with message "{message}" and exit code {code:d}')
def step_call_cli_error_custom_code(context: Context, message: str, code: int) -> None:
    _call_cli_error(context, message, exit_code=code)


@when('I call cli_warning with only message "{message}"')
def step_call_cli_warning_message(context: Context, message: str) -> None:
    _call_cli_warning(context, message)


@when('I call cli_warning with message "{message}" and hint "{hint}"')
def step_call_cli_warning_with_hint(context: Context, message: str, hint: str) -> None:
    _call_cli_warning(context, message, hint=hint)


@when('I call cli_not_found with type "{resource_type}" and name "{name}"')
def step_call_cli_not_found(context: Context, resource_type: str, name: str) -> None:
    _call_cli_not_found(context, resource_type, name)


@then("the process should exit with code {code:d}")
def step_exit_code_check(context: Context, code: int) -> None:
    assert context.exit_code == code, (
        f"Expected exit code {code}, got {context.exit_code}"
    )


@then('the stderr output should contain "{text}"')
def step_stderr_contains(context: Context, text: str) -> None:
    output = context.stderr_capture.getvalue()
    assert text in output, f"Expected '{text}' in stderr: {output!r}"


# ------------------------------------------------------------------
# CLI runner tests
# ------------------------------------------------------------------
# NOTE: The following steps are defined in cli_steps.py and
# garbage_collection_cli_steps.py respectively:
#   - @given("a CLI test runner")
#   - @when('I run the CLI with "{args}"')
#   - @then("the CLI exit code should be {code:d}")
#   - @then('the CLI output should contain "{text}"')
#   - @then("the CLI output should be valid JSON")
#
# Behave loads all step files, so those shared definitions are
# available here without re-declaration.  We only need to register
# the ``a CLI test runner`` and ``I run the CLI with`` steps because
# cli_steps.py uses a different phrasing ("I run the *CleverAgents*
# CLI with").


@given("a CLI consistency test runner")
def step_cli_consistency_runner(context: Context) -> None:
    from typer.testing import CliRunner

    context.cli_runner = CliRunner()


@when('I invoke the CLI with "{args}"')
def step_invoke_cli(context: Context, args: str) -> None:
    from typer.testing import CliRunner

    from cleveragents.cli.main import app, ensure_cli_commands_registered

    ensure_cli_commands_registered()
    if not hasattr(context, "cli_runner"):
        context.cli_runner = CliRunner()
    runner = context.cli_runner
    arg_list = args.split()
    context.cli_result = runner.invoke(app, arg_list)


@then("the invoked CLI exit code should be {code:d}")
def step_invoked_cli_exit_code(context: Context, code: int) -> None:
    actual = context.cli_result.exit_code
    assert actual == code, (
        f"Expected exit code {code}, got {actual}.\nstdout: {context.cli_result.output}"
    )


@then('the invoked CLI output should contain "{text}"')
def step_invoked_cli_output_contains(context: Context, text: str) -> None:
    output = context.cli_result.output
    assert text in output, f"Expected '{text}' in output: {output!r}"


@then("the invoked CLI output should be valid JSON")
def step_invoked_cli_output_valid_json(context: Context) -> None:
    output = context.cli_result.output.strip()
    try:
        json.loads(output)
    except json.JSONDecodeError as exc:
        raise AssertionError(
            f"Output is not valid JSON: {exc}\nOutput: {output!r}"
        ) from exc
