"""Step definitions for CLI plan and context command testing."""

import contextlib
import json
import os
import shlex
import shutil
import tempfile
from pathlib import Path

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

from cleveragents.application.container import get_container, reset_container


def _run_cleveragents_inprocess(context, args):
    """Run a cleveragents CLI command in-process using the ``main()`` entry point.

    This avoids the ~6 s overhead per ``subprocess.run`` call (Python startup,
    module imports, Alembic migration check) by reusing the already-imported
    modules in the test process.

    The function temporarily changes the working directory to ``context.test_dir``
    (if set) so that project discovery works correctly, and widens the virtual
    terminal so Rich doesn't wrap output.
    """
    import contextlib as _contextlib
    import io
    import re

    from cleveragents.cli.main import main as cli_main

    prev_cwd = os.getcwd()
    prev_columns = os.environ.get("COLUMNS")
    os.environ["COLUMNS"] = "300"

    if hasattr(context, "test_dir") and context.test_dir:
        os.chdir(context.test_dir)

    stdout_buf = io.StringIO()
    stderr_buf = io.StringIO()

    try:
        with (
            _contextlib.redirect_stdout(stdout_buf),
            _contextlib.redirect_stderr(stderr_buf),
        ):
            exit_code = cli_main(list(args))
    except SystemExit as exc:
        exit_code = exc.code if exc.code is not None else 0
    finally:
        os.chdir(prev_cwd)
        if prev_columns is None:
            os.environ.pop("COLUMNS", None)
        else:
            os.environ["COLUMNS"] = prev_columns

    stdout_text = stdout_buf.getvalue()
    stderr_text = stderr_buf.getvalue()

    # Strip ANSI escape codes that Rich may emit even to StringIO
    _ansi_re = re.compile(r"\x1b\[[0-9;]*[a-zA-Z]")
    stdout_text = _ansi_re.sub("", stdout_text)
    stderr_text = _ansi_re.sub("", stderr_text)

    output = stdout_text
    if stderr_text:
        output += stderr_text

    context.command_output = output
    context.command_error = stderr_text
    context.command_exit_code = exit_code if isinstance(exit_code, int) else 1
    context.exit_code = context.command_exit_code


@given("I have a clean test environment")
def step_clean_test_environment(context: Context) -> None:
    """Create a clean test environment."""
    # Create a temporary directory for testing
    context.test_dir = tempfile.mkdtemp(prefix="cleveragents_test_")
    context.temp_dir = (
        context.test_dir
    )  # Alias for compatibility with other step definitions
    context.original_cwd = os.getcwd()
    os.chdir(context.test_dir)

    # Reset container for clean state
    reset_container()

    # Set up mock AI provider for testing
    os.environ["CLEVERAGENTS_TESTING_USE_MOCK_AI"] = "true"
    # Auto-approve migrations during tests to avoid interactive prompts
    os.environ["CLEVERAGENTS_AUTO_APPLY_MIGRATIONS"] = "true"

    # Use a file-based SQLite DB scoped to the temp directory so subprocess CLI
    # calls share the same state.
    db_path = Path(context.test_dir) / ".cleveragents" / "db.sqlite"
    os.environ["CLEVERAGENTS_DATABASE_URL"] = f"sqlite:///{db_path}"
    os.environ["CLEVERAGENTS_TEST_DATABASE_URL"] = f"sqlite:///{db_path}"


@given("I have set up the test configuration")
def step_setup_test_config(context):
    """Set up test configuration."""
    # Configuration is handled by environment variables
    pass


@given("I am in an empty directory")
def step_in_empty_directory(context):
    """Ensure we're in an empty directory."""
    assert len(os.listdir(".")) == 0, "Directory is not empty"


@given("I have an initialized project")
def step_initialized_project(context):
    """Initialize a project for testing."""
    from cleveragents.cli.commands.project import init_command

    # Use the CLI command directly
    init_command("test-project", Path.cwd())

    # Verify project was initialized
    assert (Path.cwd() / ".cleveragents").exists()
    context.project_dir = Path.cwd() / ".cleveragents"

    # Ensure an actor is available for mock provider flows
    container = get_container()
    actor_service = container.actor_service()
    with contextlib.suppress(Exception):
        actor_service.upsert_actor(
            name="local/mock-default",
            provider="MockProvider",
            model="mock-gpt-4",
            set_default=True,
            is_built_in=True,
            unsafe=True,
        )


@given('I have a file "{filename}" with content "{content}"')
def step_create_file_with_content(context, filename, content):
    """Create a file with specified content."""
    Path(filename).write_text(content)
    assert Path(filename).exists()


@given('I have a file "{filename}"')
def step_create_file(context, filename):
    """Create an empty file."""
    Path(filename).touch()
    assert Path(filename).exists()


@given("I have an initialized project with a plan")
def step_initialized_project_with_plan(context):
    """Initialize project and create a plan."""
    step_initialized_project(context)

    from cleveragents.cli.commands.plan import tell_command

    # Create a plan
    tell_command("Test instruction")

    # Verify plan was created
    container = get_container()
    plan_service = container.plan_service()
    project_service = container.project_service()
    project = project_service.get_current_project()
    current = plan_service.get_current_plan(project) if project else None
    assert current is not None


@given('the plan has an instruction "{instruction}"')
def step_plan_has_instruction(context, instruction):
    """Set plan instruction."""
    container = get_container()
    plan_service = container.plan_service()
    project_service = container.project_service()

    # Update the current plan's prompt
    project = project_service.get_current_project()
    current = plan_service.get_current_plan(project) if project else None
    if current:
        current.prompt = instruction
        # Save updated plan (would normally use repository)
        plans_file = Path.cwd() / ".cleveragents" / "plans.json"
        if plans_file.exists():
            data = json.loads(plans_file.read_text())
            for plan in data:
                if plan["name"] == current.name:
                    plan["prompt"] = instruction
            plans_file.write_text(json.dumps(data, indent=2))


@given("I have an initialized project with a built plan")
def step_initialized_project_with_built_plan(context):
    """Initialize project with a built plan."""
    step_initialized_project_with_plan(context)

    from cleveragents.cli.commands.plan import build_command

    # Build the plan
    build_command()

    # Verify plan was built
    container = get_container()
    plan_service = container.plan_service()
    project_service = container.project_service()
    project = project_service.get_current_project()
    current = plan_service.get_current_plan(project) if project else None
    assert current is not None
    assert current.status == "built"


@given('the plan has a change to create "{filename}"')
def step_plan_has_create_change(context, filename):
    """Replace existing changes with a change to create the specified file."""
    from datetime import datetime

    from cleveragents.application.container import get_container
    from cleveragents.domain.models.core import Change, OperationType

    # Get the current plan from the database
    container = get_container()
    plan_service = container.plan_service()
    project_service = container.project_service()
    project = project_service.get_current_project()
    current_plan = plan_service.get_current_plan(project) if project else None

    if current_plan and current_plan.id:
        # Clear existing changes and add the specific one we want
        with container.unit_of_work().transaction() as ctx:
            # Clear existing changes for this plan
            ctx.changes.clear_for_plan(current_plan.id)

            # Add the new change
            change = Change(
                id=None,
                plan_id=current_plan.id,
                file_path=filename,
                operation=OperationType.CREATE,
                original_content=None,
                new_content="# Generated file\nprint('Hello from CleverAgents')\n",
                new_path=None,
                applied=False,
                created_at=datetime.now(),
                applied_at=None,
            )
            ctx.changes.add(change)


@given('I have a plan named "{name}"')
def step_have_plan_named(context, name):
    """Create a plan with specified name."""
    from cleveragents.cli.commands.plan import list_command, new_command

    # Check if plan already exists
    existing_plans = list_command()
    if any(p.name == name for p in existing_plans):
        # Plan already exists, don't create it again
        return

    new_command(name)


@given('I have plans named "{names}"')
def step_have_multiple_plans(context, names):
    """Create multiple plans."""
    from cleveragents.cli.commands.plan import list_command

    # Parse the plan names - they can be separated by comma or "and"
    # Handle formats like: "main", "feature-1", "feature-2"
    # or: "main" and "feature-1"
    names = names.replace('"', "")

    # Split by comma first
    if "," in names:
        plan_names = [n.strip() for n in names.split(",")]
    else:
        # Split by "and"
        plan_names = [n.strip() for n in names.split(" and ")]

    # Check existing plans first
    existing_plans = list_command()
    existing_names = [p.name for p in existing_plans]

    for name in plan_names:
        if name not in existing_names:
            step_have_plan_named(context, name)


@given('"{name}" is the current plan')
def step_set_current_plan(context, name):
    """Set a specific plan as current."""
    from cleveragents.cli.commands.plan import cd_command

    cd_command(name)


@given("the plan has previous conversation history")
def step_plan_has_conversation(context):
    """Add conversation history to the plan."""
    # This would normally be stored in the database
    # For now, we'll just note that the plan has history
    pass


@given('I have added files "{files}" to context')
def step_add_files_to_context(context, files):
    """Add multiple files to context."""
    from cleveragents.cli.commands.context import add_command

    # Remove quotes from filenames and split by comma
    file_list = [f.strip().strip('"') for f in files.split(",")]
    for filename in file_list:
        # Create the file if it doesn't exist
        if not Path(filename).exists():
            Path(filename).touch()
        # Add to context
        add_command([filename])


@given('I have added "{filename}" to context')
def step_add_file_to_context(context, filename):
    """Add a single file to context."""
    step_add_files_to_context(context, filename)


@given('I run "{command}"')
def step_given_run_command(context, command):
    """Run a CLI command."""
    step_run_command(context, command)


@when('I run "{command}"')
@when("I run '{command}'")
def step_run_command(context, command):
    """Run a CLI command.

    For ``cleveragents`` / ``agents`` commands we invoke the Typer app
    **in-process** via ``CliRunner`` to avoid the ~6 s Python-startup +
    Alembic-migration overhead that a ``subprocess.run`` call incurs per
    invocation.  Non-cleveragents commands still use ``subprocess``.
    """
    import subprocess

    try:
        # Use shlex to properly handle quoted strings in the command
        command_parts = shlex.split(command)

        # If the command starts with "agents" or "cleveragents", run in-process
        if command_parts[0] in ["agents", "cleveragents"]:
            _run_cleveragents_inprocess(context, command_parts[1:])
        else:
            # Widen terminal so Rich does not wrap long paths mid-word
            env = os.environ.copy()
            env.setdefault("COLUMNS", "300")
            # Run other commands as-is via subprocess
            result = subprocess.run(
                command_parts,
                capture_output=True,
                text=True,
                cwd=context.test_dir if hasattr(context, "test_dir") else None,
                timeout=60,
                env=env,
            )

            output = result.stdout or ""
            if result.stderr:
                output += result.stderr
            context.command_output = output
            context.command_error = result.stderr
            context.command_exit_code = result.returncode
            context.exit_code = result.returncode
    except subprocess.TimeoutExpired:
        context.command_output = ""
        context.command_error = "Command timed out"
        context.command_exit_code = 1
        context.exit_code = 1
    except Exception as e:
        context.command_output = ""
        context.command_error = str(e)
        context.command_exit_code = 1
        context.exit_code = 1


@then("the command should succeed")
def step_command_succeeds(context):
    """Verify command succeeded."""
    assert context.command_exit_code == 0, f"Command failed: {context.command_error}"


@then("the command should fail")
def step_command_fails(context):
    """Verify command failed."""
    assert context.command_exit_code != 0, "Command should have failed but succeeded"


@then("a .cleveragents directory should be created")
def step_cleveragents_dir_created(context):
    """Verify .cleveragents directory exists."""
    assert (Path.cwd() / ".cleveragents").exists()


@then("the database should be initialized in current directory")
def step_database_initialized_current_dir(context):
    """Verify database is initialized in current directory."""
    db_file = Path.cwd() / ".cleveragents" / "db.sqlite"
    assert db_file.exists() or (Path.cwd() / ".cleveragents" / "plans.json").exists()


@then('the project should be named "{name}"')
def step_project_named(context, name):
    """Verify project name."""
    # Check if we can get project info
    container = get_container()
    container.project_service()
    # Project name verification would go here
    pass


@then("the file should be added to context")
def step_file_in_context(context):
    """Verify file was added to context."""
    container = get_container()
    context_service = container.context_service()
    files = context_service.list_files()
    assert len(files) > 0


@then('I should see "{text}" in the output')
def step_see_text_in_output(context, text):
    """Verify text appears in command output."""
    assert text in context.command_output, (
        f"Expected '{text}' in output: {context.command_output}"
    )


@then("a new plan should be created")
def step_new_plan_created(context):
    """Verify a new plan was created."""
    container = get_container()
    plan_service = container.plan_service()
    project_service = container.project_service()
    project = project_service.get_current_project()
    current = plan_service.get_current_plan(project) if project else None
    assert current is not None


@then('the plan should contain the instruction "{instruction}"')
def step_plan_contains_instruction(context, instruction):
    """Verify plan contains instruction."""
    container = get_container()
    plan_service = container.plan_service()
    project_service = container.project_service()
    project = project_service.get_current_project()
    current = plan_service.get_current_plan(project) if project else None
    assert current is not None
    assert instruction in current.prompt


@then("the plan should have generated changes")
def step_plan_has_changes(context):
    """Verify plan has generated changes."""
    changes_file = Path.cwd() / ".cleveragents" / "changes.json"
    if changes_file.exists():
        changes = json.loads(changes_file.read_text())
        assert len(changes) > 0


@then("the changes should be stored in the database")
def step_changes_in_database(context):
    """Verify changes are stored."""
    # Changes are stored in JSON for now (or could be in database)
    changes_file = Path.cwd() / ".cleveragents" / "changes.json"
    # Also check if build was successful even without changes file
    # (changes might be in the database)
    if not changes_file.exists():
        # For testing, we consider it successful if the .cleveragents directory exists
        assert (Path.cwd() / ".cleveragents").exists()
    else:
        assert changes_file.exists()


@then('the file "{filename}" should exist')
def step_file_exists(context, filename):
    """Verify file exists."""
    assert Path(filename).exists(), f"File {filename} does not exist"


@then("the changes should be marked as applied")
def step_changes_marked_applied(context):
    """Verify changes are marked as applied."""
    changes_file = Path.cwd() / ".cleveragents" / "changes.json"
    if changes_file.exists():
        changes = json.loads(changes_file.read_text())
        assert any(c.get("applied") for c in changes)


@then('a plan named "{name}" should be created')
def step_plan_named_created(context, name):
    """Verify plan with name was created."""
    container = get_container()
    plan_service = container.plan_service()
    project_service = container.project_service()
    project = project_service.get_current_project()
    plans = plan_service.list_plans(project) if project else []
    assert any(p.name == name for p in plans)


@then('"{name}" should be the current plan')
def step_verify_current_plan(context, name):
    """Verify current plan name."""
    container = get_container()
    plan_service = container.plan_service()
    project_service = container.project_service()
    project = project_service.get_current_project()
    current = plan_service.get_current_plan(project) if project else None
    assert current is not None
    assert current.name == name


@then("the plan should have the additional instruction")
def step_plan_has_additional(context):
    """Verify plan has additional instruction."""
    container = get_container()
    plan_service = container.plan_service()
    project_service = container.project_service()
    project = project_service.get_current_project()
    current = plan_service.get_current_plan(project) if project else None
    assert current is not None
    # Would check conversation history here


@then('"{filename}" should not be in context')
def step_file_not_in_context(context, filename):
    """Verify file is not in context."""
    from pathlib import Path

    container = get_container()
    context_service = container.context_service()
    files = context_service.list_files()
    # Files are Context objects with a path attribute
    file_names = [Path(f.path if hasattr(f, "path") else str(f)).name for f in files]
    assert filename not in file_names


@then('"{filename}" should still be in context')
def step_file_still_in_context(context, filename):
    """Verify file is still in context."""
    from pathlib import Path

    container = get_container()
    context_service = container.context_service()
    files = context_service.list_files()
    # Files are Context objects with a path attribute
    file_names = [Path(f.path if hasattr(f, "path") else str(f)).name for f in files]
    print(f"DEBUG: Looking for {filename} in {file_names}")  # Debug output
    assert filename in file_names, f"{filename} not found in {file_names}"


@then("the context should be empty")
def step_context_empty(context):
    """Verify context is empty."""
    container = get_container()
    context_service = container.context_service()
    files = context_service.list_files()
    assert len(files) == 0


@then('the error message should mention "{text}"')
def step_error_mentions(context, text):
    """Verify error message contains text."""
    # Check both stdout and stderr for error messages
    combined_output = (context.command_output or "") + (context.command_error or "")
    assert text.lower() in combined_output.lower(), (
        f"Expected '{text}' not found in: {combined_output}"
    )


@then('the output should mention "{text}"')
def step_output_mentions(context, text):
    """Verify output mentions text."""
    output = context.command_output or context.command_error
    assert text.lower() in output.lower()


@then('it should be equivalent to "{command}"')
def step_equivalent_command(context, command):
    """Verify command equivalence."""
    # This is more of a conceptual check
    # The shortcuts should work the same as the full commands
    pass


# Cleanup
def after_scenario(context, scenario):
    """Clean up after each scenario."""
    if hasattr(context, "test_dir"):
        # Change back to original directory
        if hasattr(context, "original_cwd"):
            os.chdir(context.original_cwd)
        # Remove test directory
        shutil.rmtree(context.test_dir, ignore_errors=True)

    # Clean up environment
    if "CLEVERAGENTS_TESTING_USE_MOCK_AI" in os.environ:
        del os.environ["CLEVERAGENTS_TESTING_USE_MOCK_AI"]

    # Reset container
    reset_container()
