"""Step definitions for CLI global options: --data-dir, --config-path, -v.

Tests for issue #6785 — spec-required global options that were missing from
main_callback() and caused fatal "No such option" crashes.

All step patterns use the prefix "global options" to avoid any ambiguity
with the many generic CLI step definitions in other step files.
"""

from __future__ import annotations

import os
import re
import shutil
import tempfile
from pathlib import Path
from typing import Any
from unittest.mock import MagicMock, patch

from behave import given, then, when
from behave.runner import Context
from typer.testing import CliRunner

from cleveragents.cli.formatting import OutputFormat
from cleveragents.cli.main import app


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

# Regex to strip ANSI escape sequences (CSI sequences and OSC sequences).
# Used to make output assertions environment-agnostic — Rich may emit ANSI
# colour/style codes when the CI environment does not set TERM=dumb.
_ANSI_ESCAPE_RE = re.compile(
    r"\x1b(?:"
    r"\[[0-9;]*[a-zA-Z]"  # CSI sequences (SGR, cursor, etc.)
    r"|"
    r"\][^\x07]*\x07"  # OSC sequences (hyperlink, title, etc.)
    r")"
)


def _strip_ansi(text: str) -> str:
    """Remove ANSI escape sequences from *text*."""
    return _ANSI_ESCAPE_RE.sub("", text)


def _invoke(args: list[str]) -> Any:
    """Invoke the Typer app with the given args and return the result."""
    runner = CliRunner()
    return runner.invoke(app, args, catch_exceptions=True)


def _combined_output(result: Any) -> str:
    """Return combined output from a CliRunner result, with ANSI codes stripped.

    In non-TTY environments (CI), Typer/Rich may split output between
    stdout and stderr.  Check both explicitly so that ``--help`` output
    assertions work regardless of the terminal detection.
    """
    stdout = _strip_ansi((getattr(result, "stdout", "") or "").strip())
    stderr = _strip_ansi((getattr(result, "stderr", "") or "").strip())
    combined = (
        (stdout + "\n" + stderr).strip() if (stdout and stderr) else (stdout or stderr)
    )
    return combined or _strip_ansi((result.output or "").strip())


# ---------------------------------------------------------------------------
# Given steps
# ---------------------------------------------------------------------------


@given("global options test env is clean")
def step_global_opts_env_clean(context: Context) -> None:
    """Ensure test-specific env vars are not polluted from previous tests.

    Registers a cleanup handler that restores the original env var values after
    the scenario ends.  The global ``after_scenario`` hook in
    ``features/environment.py`` runs all ``context._cleanup_handlers``.
    """
    orig_data_dir = os.environ.get("CLEVERAGENTS_DATA_DIR")
    orig_config_path = os.environ.get("CLEVERAGENTS_CONFIG_PATH")
    # Clean up any leftover from previous scenarios
    for var in ("CLEVERAGENTS_DATA_DIR", "CLEVERAGENTS_CONFIG_PATH"):
        os.environ.pop(var, None)

    # Register restoration handler so that env vars set by main_callback()
    # during CLI invocation do not pollute subsequent scenarios.
    def _restore() -> None:
        for key, val in [
            ("CLEVERAGENTS_DATA_DIR", orig_data_dir),
            ("CLEVERAGENTS_CONFIG_PATH", orig_config_path),
        ]:
            if val is not None:
                os.environ[key] = val
            else:
                os.environ.pop(key, None)

    if not hasattr(context, "_cleanup_handlers"):
        context._cleanup_handlers = []
    context._cleanup_handlers.append(_restore)


@given("a temp data dir is prepared")
def step_temp_data_dir_prepared(context: Context) -> None:
    """Create a real temporary directory for --data-dir testing.

    Registers a cleanup handler so the directory is removed after the scenario.
    """
    tmp_dir = tempfile.mkdtemp(prefix="ca_test_data_dir_")
    context.temp_data_dir = tmp_dir
    if not hasattr(context, "_cleanup_handlers"):
        context._cleanup_handlers = []
    context._cleanup_handlers.append(lambda: shutil.rmtree(tmp_dir, ignore_errors=True))


@given("a temp config file is prepared")
def step_temp_config_file_prepared(context: Context) -> None:
    """Create a real temporary TOML config file for --config-path testing.

    Registers a cleanup handler so the file is removed after the scenario.
    """
    with tempfile.NamedTemporaryFile(
        suffix=".toml", prefix="ca_test_config_", delete=False
    ) as tmp_file:
        tmp_file.write(b"# test config\n")
        tmp_path = tmp_file.name
    context.temp_config_file = tmp_path
    if not hasattr(context, "_cleanup_handlers"):
        context._cleanup_handlers = []
    context._cleanup_handlers.append(lambda: os.unlink(tmp_path))


@given("a temp file path is prepared")
def step_temp_file_path_prepared(context: Context) -> None:
    """Create a temporary FILE (not directory) to trigger --data-dir validation.

    Registers a cleanup handler so the file is removed after the scenario.
    """
    with tempfile.NamedTemporaryFile(prefix="ca_test_file_", delete=False) as tmp_file:
        tmp_path = tmp_file.name
    context.temp_file_path = tmp_path
    if not hasattr(context, "_cleanup_handlers"):
        context._cleanup_handlers = []
    context._cleanup_handlers.append(lambda: os.unlink(tmp_path))


# ---------------------------------------------------------------------------
# When steps - CLI invocation
# ---------------------------------------------------------------------------


@when('I run the global options CLI with data-dir flag and "{subcommand}"')
def step_run_global_opts_data_dir(context: Context, subcommand: str) -> None:
    """Run the CLI with --data-dir pointing at the temp directory."""
    args = ["--data-dir", context.temp_data_dir, subcommand]
    result = _invoke(args)
    context.global_opts_result = result
    context.global_opts_exit_code = result.exit_code
    context.global_opts_output = _combined_output(result)


@when('I run the global options CLI with config-path flag and "{subcommand}"')
def step_run_global_opts_config_path(context: Context, subcommand: str) -> None:
    """Run the CLI with --config-path pointing at the temp config file."""
    args = ["--config-path", context.temp_config_file, subcommand]
    result = _invoke(args)
    context.global_opts_result = result
    context.global_opts_exit_code = result.exit_code
    context.global_opts_output = _combined_output(result)


@when('I run the global options CLI with both path flags and "{subcommand}"')
def step_run_global_opts_both(context: Context, subcommand: str) -> None:
    """Run the CLI with both --data-dir and --config-path."""
    args = [
        "--data-dir",
        context.temp_data_dir,
        "--config-path",
        context.temp_config_file,
        subcommand,
    ]
    result = _invoke(args)
    context.global_opts_result = result
    context.global_opts_exit_code = result.exit_code
    context.global_opts_output = _combined_output(result)


@when('I run the global options CLI with data-dir as temp file and "{subcommand}"')
def step_run_global_opts_data_dir_file(context: Context, subcommand: str) -> None:
    """Run CLI with --data-dir pointing to a FILE (triggers validation error)."""
    args = ["--data-dir", context.temp_file_path, subcommand]
    result = _invoke(args)
    context.global_opts_result = result
    context.global_opts_exit_code = result.exit_code
    context.global_opts_output = _combined_output(result)


@when('I run the global options CLI with invalid config-path and "{subcommand}"')
def step_run_global_opts_invalid_config_path(context: Context, subcommand: str) -> None:
    """Run CLI with --config-path pointing to a non-existent file."""
    args = ["--config-path", "/tmp/no_such_file_xyz_99999.toml", subcommand]
    result = _invoke(args)
    context.global_opts_result = result
    context.global_opts_exit_code = result.exit_code
    context.global_opts_output = _combined_output(result)


@when('I run the global options CLI with args "{args}"')
def step_run_global_opts_with_args(context: Context, args: str) -> None:
    """Run the CLI with space-separated args string."""
    result = _invoke(args.split())
    context.global_opts_result = result
    context.global_opts_exit_code = result.exit_code
    context.global_opts_output = _combined_output(result)


# ---------------------------------------------------------------------------
# When steps - direct callback testing for verbosity
# ---------------------------------------------------------------------------


@when("I call main_callback with verbosity {count:d}")
def step_call_main_callback_verbosity(context: Context, count: int) -> None:
    """Invoke main_callback directly, capturing the log level configured."""
    from cleveragents.cli.main import main_callback

    context.global_opts_captured_log_level: str | None = None
    context.global_opts_captured_ctx_obj: dict[str, Any] = {}

    def _capture_log_level(
        *, env: str = "development", log_level: str = "INFO"
    ) -> None:
        context.global_opts_captured_log_level = log_level

    mock_ctx = MagicMock()
    mock_ctx.obj = {}

    with patch(
        "cleveragents.config.logging.configure_structlog",
        side_effect=_capture_log_level,
    ):
        main_callback(
            ctx=mock_ctx,
            version=None,  # type: ignore[arg-type]
            show_secrets=False,
            fmt=OutputFormat.RICH,
            data_dir=None,
            config_path=None,
            verbose=count,
        )

    context.global_opts_captured_ctx_obj = mock_ctx.obj


# ---------------------------------------------------------------------------
# Then steps - exit codes
# ---------------------------------------------------------------------------


@then("the global options CLI should succeed")
def step_global_opts_cli_succeed(context: Context) -> None:
    actual = context.global_opts_exit_code
    output = context.global_opts_output
    assert actual == 0, f"Expected exit code 0, got {actual}.\nOutput:\n{output}"


@then("the global options CLI should fail")
def step_global_opts_cli_fail(context: Context) -> None:
    actual = context.global_opts_exit_code
    assert actual != 0, (
        f"Expected non-zero exit code, got {actual}.\nOutput:\n{context.global_opts_output}"
    )


# ---------------------------------------------------------------------------
# Then steps - env var assertions
# ---------------------------------------------------------------------------


@then("the CLEVERAGENTS_DATA_DIR env var should match the temp data dir")
def step_env_data_dir_matches(context: Context) -> None:
    """Verify that --data-dir set CLEVERAGENTS_DATA_DIR to the temp directory."""
    env_val = os.environ.get("CLEVERAGENTS_DATA_DIR")
    expected = str(Path(context.temp_data_dir).resolve())
    assert env_val is not None, (
        "CLEVERAGENTS_DATA_DIR was not set after --data-dir invocation"
    )
    assert str(Path(env_val).resolve()) == expected, (
        f"CLEVERAGENTS_DATA_DIR={env_val!r} does not match expected {expected!r}"
    )


@then("the CLEVERAGENTS_CONFIG_PATH env var should match the temp config file")
def step_env_config_path_matches(context: Context) -> None:
    """Verify that --config-path set CLEVERAGENTS_CONFIG_PATH to the temp file."""
    env_val = os.environ.get("CLEVERAGENTS_CONFIG_PATH")
    expected = str(Path(context.temp_config_file).resolve())
    assert env_val is not None, (
        "CLEVERAGENTS_CONFIG_PATH was not set after --config-path invocation"
    )
    assert str(Path(env_val).resolve()) == expected, (
        f"CLEVERAGENTS_CONFIG_PATH={env_val!r} does not match expected {expected!r}"
    )


# ---------------------------------------------------------------------------
# Then steps - ctx.obj assertions
# ---------------------------------------------------------------------------


@then('the global options ctx obj "{key}" should be {value:d}')
def step_global_opts_ctx_obj_int(context: Context, key: str, value: int) -> None:
    """Verify ctx.obj[key] has the expected integer value."""
    obj = context.global_opts_captured_ctx_obj
    assert key in obj, f"ctx.obj missing key '{key}'. ctx.obj={obj}"
    actual = obj[key]
    assert actual == value, f"ctx.obj['{key}']={actual!r}, expected {value!r}"


# ---------------------------------------------------------------------------
# Then steps - log level assertions
# ---------------------------------------------------------------------------


@then('the global options log level should be "{level}"')
def step_global_opts_log_level(context: Context, level: str) -> None:
    """Verify that configure_structlog was called with the expected log level."""
    actual = context.global_opts_captured_log_level
    assert actual is not None, "configure_structlog was not called"
    assert actual.upper() == level.upper(), (
        f"Expected log level {level!r}, got {actual!r}"
    )


# ---------------------------------------------------------------------------
# Then steps - output assertions
# ---------------------------------------------------------------------------


@then('the global options output should contain "{text}"')
def step_global_opts_output_contains(context: Context, text: str) -> None:
    output = context.global_opts_output
    assert text in output, f"Expected '{text}' in output:\n{output}"
