"""Step definitions for plan_cli_v3_only.feature.

Tests that verify only V3 commands are available and legacy commands are removed.
"""

from __future__ import annotations

from datetime import UTC, datetime, timedelta

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

from cleveragents.core.exceptions import ValidationError


def _get_plan_app():
    """Lazy import of plan app to avoid CLI registration at import time."""
    from cleveragents.cli.commands.plan import app as plan_app

    return plan_app


def _get_format_relative_time():
    """Lazy import of _format_relative_time."""
    from cleveragents.cli.commands.plan import _format_relative_time

    return _format_relative_time


def _get_validate_plan_ulid():
    """Lazy import of _validate_plan_ulid."""
    from cleveragents.cli.commands.plan import _validate_plan_ulid

    return _validate_plan_ulid


def _get_validate_namespaced_actor():
    """Lazy import of validate_namespaced_actor."""
    from cleveragents.cli.commands.plan import validate_namespaced_actor

    return validate_namespaced_actor


def _get_get_decision_label():
    """Lazy import of _get_decision_label."""
    from cleveragents.cli.commands.plan import _get_decision_label

    return _get_decision_label


@given("CLI environment is ready for plan command tests")
def step_setup_cli_env(context: Context) -> None:
    """Initialize CLI test environment."""
    context.runner = CliRunner()
    context.help_output = None
    context.last_exit_code = None


@when('I invoke "{command}" on plan app')
def step_invoke_cli_command(context: Context, command: str) -> None:
    """Execute a CLI command and capture output."""
    plan_app = _get_plan_app()
    # Handle empty command by passing empty list (no args)
    parts = command.split() if command else []
    result = context.runner.invoke(plan_app, parts)
    context.help_output = result.output
    context.last_exit_code = result.exit_code


@when('I invoke "" on plan app')
def step_invoke_empty_command(context: Context) -> None:
    """Execute an empty CLI command (no arguments)."""
    plan_app = _get_plan_app()
    result = context.runner.invoke(plan_app, [])
    context.help_output = result.output
    context.last_exit_code = result.exit_code


@then('the help contains "{text}"')
def step_help_contains(context: Context, text: str) -> None:
    """Verify that help output contains specific text."""
    assert context.help_output is not None, "No help output available"
    assert text in context.help_output, (
        f"Expected '{text}' in output.\nOutput:\n{context.help_output}"
    )


@then("the command fails with exit code non-zero")
def step_command_fails(context: Context) -> None:
    """Verify that the command failed (non-zero exit code)."""
    assert context.last_exit_code is not None, "No command result available"
    assert context.last_exit_code != 0, (
        f"Expected non-zero exit code, got {context.last_exit_code}\n"
        f"Output:\n{context.help_output}"
    )


@then('the help shows "{text}"')
def step_help_shows(context: Context, text: str) -> None:
    """Verify that help contains specific text."""
    assert context.help_output is not None, "No help output available"
    assert text in context.help_output, (
        f"Expected '{text}' in output.\nOutput:\n{context.help_output}"
    )


@when('I invoke "{command}" on plan app again')
def step_invoke_cli_command_again(context: Context, command: str) -> None:
    """Execute a CLI command a second time to test state consistency."""
    plan_app = _get_plan_app()
    parts = command.split()
    result = context.runner.invoke(plan_app, parts)
    # Store the second output for comparison
    if not hasattr(context, "previous_help_output"):
        context.previous_help_output = None
    context.previous_help_output = context.help_output
    context.help_output = result.output
    context.last_exit_code = result.exit_code

    # Also store for "all invocations" comparison
    if not hasattr(context, "invocation_outputs"):
        context.invocation_outputs = []
    if context.help_output:
        context.invocation_outputs.append(context.help_output)


@then("both invocations should have consistent help output")
def step_help_output_consistent(context: Context) -> None:
    """Verify help output is consistent across multiple invocations."""
    assert context.help_output is not None, "Current help output not available"
    assert context.previous_help_output is not None, (
        "Previous help output not available"
    )
    assert context.help_output == context.previous_help_output, (
        "Help output changed between invocations"
    )


@then("the context should not leak state between invocations")
def step_context_no_state_leak(context: Context) -> None:
    """Verify that context state is properly managed."""
    plan_app = _get_plan_app()
    assert hasattr(context, "runner"), "CLI runner not initialized"
    assert context.runner is not None, "CLI runner is None"
    # Verify runner is still functional
    result = context.runner.invoke(plan_app, ["--help"])
    assert result is not None, "Runner returned None result"


@then("the output contains help information")
def step_output_contains_help(context: Context) -> None:
    """Verify output contains help information."""
    assert context.help_output is not None, "No output available"
    # Help output should contain common CLI help indicators
    help_indicators = [
        "Usage:",
        "usage:",
        "Options:",
        "options:",
        "Commands:",
        "commands:",
    ]
    assert any(indicator in context.help_output for indicator in help_indicators), (
        f"Help information not found in output:\n{context.help_output}"
    )


@then("the help output should contain complete command list")
def step_help_output_complete(context: Context) -> None:
    """Verify help output contains all v3 commands."""
    assert context.help_output is not None, "No help output available"
    v3_commands = [
        "use",
        "list",
        "status",
        "execute",
        "apply",
        "cancel",
        "diff",
        "artifacts",
        "errors",
    ]
    found_commands = []
    for cmd in v3_commands:
        if cmd in context.help_output:
            found_commands.append(cmd)
    assert len(found_commands) > 0, (
        f"No v3 commands found in help output. Looked for: {v3_commands}"
    )


@then("the help output should not contain deprecated command references")
def step_help_no_deprecated_refs(context: Context) -> None:
    """Verify help output doesn't reference deprecated commands."""
    assert context.help_output is not None, "No help output available"
    deprecated_commands = ["tell", "build", "new", "current", "cd", "continue"]
    for cmd in deprecated_commands:
        # The word might appear in explanatory text, but not as a command
        lines = context.help_output.split("\n")
        for line in lines:
            # Skip lines that are just explaining what tell/build are (in the help message)
            if line.strip().startswith(cmd):
                # This would indicate the command is still registered
                raise AssertionError(
                    f"Deprecated command '{cmd}' appears to be registered in help output"
                )


@then("the CLI runner should be in a valid state for next command")
def step_cli_runner_valid(context: Context) -> None:
    """Verify CLI runner is in valid state after failed command."""
    plan_app = _get_plan_app()
    assert hasattr(context, "runner"), "CLI runner not initialized"
    assert context.runner is not None, "CLI runner is None"
    # Try a simple command to verify runner is still functional
    result = context.runner.invoke(plan_app, ["--help"])
    assert result is not None, "Runner should still be functional"
    assert result.exit_code == 0, "Basic help command should succeed"


@then("the help output should be available")
def step_help_output_available(context: Context) -> None:
    """Verify that help output is available after commands."""
    assert context.help_output is not None, "Help output should be available"
    assert len(context.help_output) > 0, "Help output should not be empty"


@then("all invocations should have identical output")
def step_all_invocations_identical(context: Context) -> None:
    """Verify that multiple invocations produce identical output."""
    assert hasattr(context, "invocation_outputs"), "No invocation outputs stored"
    if not hasattr(context, "invocation_outputs"):
        context.invocation_outputs = []

    # Store current output
    if context.help_output:
        if not hasattr(context, "invocation_outputs"):
            context.invocation_outputs = []
        context.invocation_outputs.append(context.help_output)

    # Check all stored outputs are identical
    assert len(context.invocation_outputs) > 0, "No invocation outputs to compare"
    first_output = context.invocation_outputs[0]
    for i, output in enumerate(context.invocation_outputs[1:], 1):
        assert output == first_output, (
            f"Invocation {i} output differs from first invocation"
        )


@then("the context state should remain clean")
def step_context_state_clean(context: Context) -> None:
    """Verify that context state remains clean and unmodified."""
    assert hasattr(context, "runner"), "CLI runner not initialized"
    assert context.runner is not None, "CLI runner is None"
    # Verify no orphaned resources
    assert hasattr(context, "help_output"), "Help output tracking missing"
    assert hasattr(context, "last_exit_code"), "Exit code tracking missing"


@when("I save the help output for comparison")
def step_save_help_output(context: Context) -> None:
    """Save current help output for later comparison."""
    assert context.help_output is not None, "No help output to save"
    if not hasattr(context, "saved_outputs"):
        context.saved_outputs = {}
    context.saved_outputs["original_help"] = context.help_output


@then("the help output should match the saved output")
def step_help_matches_saved(context: Context) -> None:
    """Verify help output matches previously saved output."""
    assert hasattr(context, "saved_outputs"), "No saved outputs to compare"
    assert "original_help" in context.saved_outputs, "Original help output not saved"
    assert context.help_output == context.saved_outputs["original_help"], (
        "Help output changed after failed command invocation"
    )


# Utility function test steps


@when('I validate plan ID "{plan_id}" as ULID')
@when('I validate plan ID "" as ULID')
def step_validate_plan_ulid_impl(context: Context, plan_id: str = "") -> None:
    """Validate a plan ID using the ULID validator."""
    _validate_plan_ulid = _get_validate_plan_ulid()
    context.validation_error = None
    context.validation_result = None
    try:
        result = _validate_plan_ulid(plan_id)
        context.validation_result = result
    except ValidationError as e:
        context.validation_error = str(e)


@then("validation should fail with legacy workflow message")
def step_validation_failed_legacy(context: Context) -> None:
    """Verify validation failed with legacy workflow message."""
    assert context.validation_error is not None, "Expected validation to fail"
    assert "v3 plan lifecycle" in context.validation_error.lower(), (
        f"Expected legacy workflow message. Got: {context.validation_error}"
    )


@then("validation should succeed with original plan ID returned")
def step_validation_succeeded_plan_id_impl(context: Context) -> None:
    """Verify validation succeeded and returned the original plan ID."""
    assert context.validation_error is None, (
        f"Validation should succeed. Got error: {context.validation_error}"
    )
    assert context.validation_result is not None, "Expected a validation result"


@when('I validate actor name "{actor_name}" as namespaced')
@when('I validate actor name "" as namespaced')
def step_validate_actor_impl(context: Context, actor_name: str = "") -> None:
    """Validate an actor name using the namespaced actor validator."""
    context.validation_error = None
    context.validation_result = None
    validate_namespaced_actor = _get_validate_namespaced_actor()
    try:
        result = validate_namespaced_actor(actor_name, "--actor")
        context.validation_result = result
    except ValidationError as e:
        context.validation_error = str(e)


@then("validation should fail with actor format message")
def step_validation_failed_actor_impl(context: Context) -> None:
    """Verify validation failed with actor format message."""
    assert context.validation_error is not None, "Expected validation to fail"
    assert "namespace/name" in context.validation_error.lower(), (
        f"Expected actor format message. Got: {context.validation_error}"
    )


@then("validation should succeed with original actor name returned")
def step_validation_succeeded_actor_impl(context: Context) -> None:
    """Verify validation succeeded and returned the original actor name."""
    assert context.validation_error is None, (
        f"Validation should succeed. Got error: {context.validation_error}"
    )
    assert context.validation_result is not None, "Expected a validation result"


@when("I format current timestamp as relative time")
def step_format_current_timestamp_impl(context: Context) -> None:
    """Format the current timestamp."""
    _format_relative_time = _get_format_relative_time()
    now = datetime.now(UTC)
    context.formatted_time = _format_relative_time(now)


@when("I format timestamp from {amount} hours ago as relative time")
def step_format_hours_ago_impl(context: Context, amount: str) -> None:
    """Format a timestamp from N hours ago."""
    _format_relative_time = _get_format_relative_time()
    hours = int(amount)
    past_time = datetime.now(UTC) - timedelta(hours=hours)
    context.formatted_time = _format_relative_time(past_time)


@when("I format timestamp from {amount} days ago as relative time")
def step_format_days_ago_impl(context: Context, amount: str) -> None:
    """Format a timestamp from N days ago."""
    _format_relative_time = _get_format_relative_time()
    days = int(amount)
    past_time = datetime.now(UTC) - timedelta(days=days)
    context.formatted_time = _format_relative_time(past_time)


@then('formatted time should be "{expected}"')
def step_formatted_time_equals_impl(context: Context, expected: str) -> None:
    """Verify formatted time equals expected string."""
    assert hasattr(context, "formatted_time"), "No formatted time available"
    assert context.formatted_time == expected, (
        f"Expected '{expected}', got '{context.formatted_time}'"
    )


@then('formatted time should contain "{text}"')
def step_formatted_time_contains_impl(context: Context, text: str) -> None:
    """Verify formatted time contains text."""
    assert hasattr(context, "formatted_time"), "No formatted time available"
    assert text.lower() in context.formatted_time.lower(), (
        f"Expected '{text}' in '{context.formatted_time}'"
    )


@when('I invoke an invalid plan command "{command}"')
def step_invoke_invalid_command_impl(context: Context, command: str) -> None:
    """Invoke an invalid plan command."""
    plan_app = _get_plan_app()
    parts = command.split()
    result = context.runner.invoke(plan_app, parts)
    context.help_output = result.output
    context.last_exit_code = result.exit_code


@then("the command should fail")
def step_command_failed_impl(context: Context) -> None:
    """Verify the command failed with non-zero exit code."""
    assert context.last_exit_code is not None, "No exit code available"
    assert context.last_exit_code != 0, (
        f"Expected non-zero exit code, got {context.last_exit_code}"
    )


@when('I invoke another invalid command "{command}"')
@then('I invoke another invalid command "{command}"')
def step_invoke_another_invalid_impl(context: Context, command: str) -> None:
    """Invoke another invalid command."""
    plan_app = _get_plan_app()
    parts = command.split()
    result = context.runner.invoke(plan_app, parts)
    context.second_exit_code = result.exit_code
    context.second_output = result.output


@then("that command should also fail")
def step_second_command_failed_impl(context: Context) -> None:
    """Verify second command also failed."""
    assert context.second_exit_code is not None, "No second exit code"
    assert context.second_exit_code != 0, (
        f"Expected second command to fail, got {context.second_exit_code}"
    )


@then("the CLI runner should remain functional")
def step_cli_runner_remains_functional_impl(context: Context) -> None:
    """Verify CLI runner remains functional after multiple errors."""
    plan_app = _get_plan_app()
    # Runner should still execute valid commands
    result = context.runner.invoke(plan_app, ["--help"])
    assert result is not None, "Runner returned None"
    assert result.exit_code == 0, (
        f"Help should work after errors, got {result.exit_code}"
    )
    # Verify output is not corrupted
    assert "Usage:" in result.output or "usage:" in result.output, (
        "Help output should be valid"
    )


# Decision label generation tests
@when('I generate decision label for type "{decision_type}" with ordinal {ordinal}')
def step_generate_decision_label(
    context: Context, decision_type: str, ordinal: str
) -> None:
    """Generate a decision label for a specific decision type."""
    _get_decision_label = _get_get_decision_label()
    ordinal_int = int(ordinal)
    context.decision_label = _get_decision_label(decision_type, ordinal_int)


@then('the label should be "{expected_label}"')
def step_label_equals(context: Context, expected_label: str) -> None:
    """Verify decision label equals expected value."""
    assert hasattr(context, "decision_label"), "No decision label generated"
    assert context.decision_label == expected_label, (
        f"Expected '{expected_label}', got '{context.decision_label}'"
    )


# Timestamp formatting edge cases
@when("I format timestamp from {amount} minute ago as relative time")
def step_format_singular_minute_ago(context: Context, amount: str) -> None:
    """Format a timestamp from N minute(s) ago."""
    _format_relative_time = _get_format_relative_time()
    minutes = int(amount)
    past_time = datetime.now(UTC) - timedelta(minutes=minutes)
    context.formatted_time = _format_relative_time(past_time)


@when("I format timestamp from {amount} minutes ago as relative time")
def step_format_plural_minutes_ago(context: Context, amount: str) -> None:
    """Format a timestamp from N minutes ago (plural form)."""
    _format_relative_time = _get_format_relative_time()
    minutes = int(amount)
    past_time = datetime.now(UTC) - timedelta(minutes=minutes)
    context.formatted_time = _format_relative_time(past_time)


@when("I format timestamp from {amount} hour ago as relative time")
def step_format_singular_hour_ago(context: Context, amount: str) -> None:
    """Format a timestamp from 1 hour ago."""
    _format_relative_time = _get_format_relative_time()
    hours = int(amount)
    past_time = datetime.now(UTC) - timedelta(hours=hours)
    context.formatted_time = _format_relative_time(past_time)


@when("I format timestamp from {amount} day ago as relative time")
def step_format_singular_day_ago(context: Context, amount: str) -> None:
    """Format a timestamp from 1 day ago."""
    _format_relative_time = _get_format_relative_time()
    days = int(amount)
    past_time = datetime.now(UTC) - timedelta(days=days)
    context.formatted_time = _format_relative_time(past_time)


# Error message content validation
@then('validation error should mention "{text}"')
def step_error_mentions_text(context: Context, text: str) -> None:
    """Verify error message mentions specific text."""
    assert context.validation_error is not None, "No validation error"
    assert text.lower() in context.validation_error.lower(), (
        f"Expected '{text}' in error: {context.validation_error}"
    )


# Multiple validation attempts
@when("I attempt multiple actor validations")
def step_attempt_multiple_actor_validations(context: Context) -> None:
    """Attempt multiple actor validations with different inputs."""
    validate_namespaced_actor = _get_validate_namespaced_actor()
    test_cases = [
        "no-slash",
        "UPPERCASE/name",
        "missing-slash",
        "space in/name",
    ]

    context.validation_attempts = []
    for actor_name in test_cases:
        try:
            validate_namespaced_actor(actor_name, "--actor")
            context.validation_attempts.append({"actor": actor_name, "error": None})
        except ValidationError as e:
            context.validation_attempts.append({"actor": actor_name, "error": str(e)})


@then("all validations should fail appropriately")
def step_all_validations_failed(context: Context) -> None:
    """Verify all validation attempts failed."""
    assert hasattr(context, "validation_attempts"), "No validation attempts stored"
    assert len(context.validation_attempts) > 0, "No validations performed"

    for attempt in context.validation_attempts:
        assert attempt["error"] is not None, (
            f"Expected validation to fail for '{attempt['actor']}'"
        )
        assert "namespace/name" in attempt["error"].lower(), (
            f"Expected helpful error message for '{attempt['actor']}'"
        )


@then("the validation context should remain clean")
def step_validation_context_clean(context: Context) -> None:
    """Verify validation context is clean after multiple attempts."""
    plan_app = _get_plan_app()
    assert hasattr(context, "validation_attempts"), "Validation context missing"
    assert hasattr(context, "runner"), "CLI runner not initialized"
    assert context.runner is not None, "CLI runner is None"

    # Verify runner is still functional
    result = context.runner.invoke(plan_app, ["--help"])
    assert result is not None, "Runner should return result"
    assert result.exit_code == 0, "Help should succeed"
