"""Step definitions for ACMS context list and add CLI commands.

Uses Typer's CliRunner to invoke the real CLI commands with mocked services,
providing genuine confidence in the CLI implementation.
"""

from __future__ import annotations

import json as _json
import tempfile
from pathlib import Path
from typing import TYPE_CHECKING, Any
from unittest.mock import MagicMock, patch

from behave import given, register_type, then, when
from parse import with_pattern
from typer.testing import CliRunner

from cleveragents.cli.commands.context import app as context_app

if TYPE_CHECKING:
    from behave.runner import Context

_runner = CliRunner()


# ---------------------------------------------------------------------------
# Custom parse types to avoid AmbiguousStep conflicts
# ---------------------------------------------------------------------------


@with_pattern(r"[^\"]+")
def parse_filepath(text: str) -> str:
    """Parse a file path (no quotes allowed inside)."""
    return text


@with_pattern(r"[^\"]+")
def parse_tag(text: str) -> str:
    """Parse a tag value (no quotes allowed inside)."""
    return text


@with_pattern(r"[^\"]+")
def parse_policy(text: str) -> str:
    """Parse a policy value (no quotes allowed inside)."""
    return text


register_type(FilePath=parse_filepath)
register_type(Tag=parse_tag)
register_type(Policy=parse_policy)


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


def _make_mock_container(
    context_files: list[Any] | None = None,
    added_files: list[Path] | None = None,
    already_in_context: list[Path] | None = None,
) -> MagicMock:
    """Build a mock DI container with pre-configured services."""
    mock_container = MagicMock()

    # Project service
    mock_project_service = MagicMock()
    mock_project = MagicMock()
    mock_project.id = 1
    mock_project.name = "test-project"
    mock_project_service.get_current_project.return_value = mock_project
    mock_container.project_service.return_value = mock_project_service

    # Context service
    mock_context_service = MagicMock()
    mock_context_service.list_context.return_value = context_files or []
    mock_context_service.add_to_context.return_value = (
        added_files or [],
        already_in_context or [],
    )
    mock_container.context_service.return_value = mock_context_service

    return mock_container


def _invoke_context_list(
    extra_args: list[str] | None = None,
    context_files: list[Any] | None = None,
) -> Any:
    """Invoke the context list command via CliRunner with mocked container."""
    mock_container = _make_mock_container(context_files=context_files)
    args = ["list"] + (extra_args or [])
    with patch(
        "cleveragents.application.container.get_container",
        return_value=mock_container,
    ):
        return _runner.invoke(context_app, args)


def _invoke_context_add(
    paths: list[str],
    extra_args: list[str] | None = None,
    added_files: list[Path] | None = None,
    already_in_context: list[Path] | None = None,
) -> Any:
    """Invoke the context add command via CliRunner with mocked container."""
    mock_container = _make_mock_container(
        added_files=added_files,
        already_in_context=already_in_context,
    )
    args = ["add"] + paths + (extra_args or [])
    with patch(
        "cleveragents.application.container.get_container",
        return_value=mock_container,
    ):
        return _runner.invoke(context_app, args)


def _build_mock_entries(context_entries: list[dict[str, Any]]) -> list[dict[str, Any]]:
    """Convert context entry dicts to dict-based entries for the CLI.

    Returns dicts so that _normalize_context_entry uses the dict branch,
    which accesses keys like 'path', 'type', 'size', 'added_at', 'content'.
    """
    result = []
    for entry in context_entries:
        item: dict[str, Any] = {
            "path": entry["path"],
            "type": entry["tier"],
            "size": entry["size"],
            "added_at": entry["last_accessed"],
            "content": "",
        }
        if "tag" in entry:
            item["tag"] = entry["tag"]
        if "policy" in entry:
            item["policy"] = entry["policy"]
        result.append(item)
    return result


# ---------------------------------------------------------------------------
# Background / Given steps
# ---------------------------------------------------------------------------


@given("a temporary project for ACMS context testing")
def step_create_temp_project(context: Context) -> None:
    """Create a temporary project for testing."""
    context.temp_dir = tempfile.mkdtemp()
    context.project_dir = Path(context.temp_dir)
    context.context_entries: list[dict[str, Any]] = []
    context.added_files: list[str] = []
    context.last_command_output = ""
    context.last_command_exit_code = 0
    context.context_files: list[str] = []
    context.file_metadata: dict[str, dict[str, Any]] = {}


@given("the ACMS context service is initialized")
def step_init_acms_service(context: Context) -> None:
    """Initialize the ACMS context service (no-op: mocked in each step)."""
    context.acms_initialized = True


@given("context entries exist with:")
def step_create_context_entries(context: Context) -> None:
    """Create context entries from table."""
    for row in context.table:
        entry = {
            "path": row["path"],
            "tier": row["tier"],
            "size": int(row["size"]),
            "last_accessed": row["last_accessed"],
        }
        context.context_entries.append(entry)


@given('a file "{file_path}" exists with --tag "{tag}" and --policy "{policy}"')
def step_create_file_with_tag_and_policy(
    context: Context, file_path: str, tag: str, policy: str
) -> None:
    """Create a test file with associated metadata."""
    full_path = context.project_dir / file_path
    full_path.parent.mkdir(parents=True, exist_ok=True)
    full_path.write_text(f"# Test file: {file_path}\n")
    context.file_metadata[file_path] = {"tag": tag, "policy": policy}


@given('a file "{file_path}" exists')
def step_create_file(context: Context, file_path: str) -> None:
    """Create a test file."""
    full_path = context.project_dir / file_path
    full_path.parent.mkdir(parents=True, exist_ok=True)
    full_path.write_text(f"# Test file: {file_path}\n")


@given('a directory "{dir_path}" exists with files:')
def step_create_directory_with_files(context: Context, dir_path: str) -> None:
    """Create a directory with multiple files."""
    dir_full_path = context.project_dir / dir_path
    dir_full_path.mkdir(parents=True, exist_ok=True)

    for row in context.table:
        file_path = dir_full_path / row["file"]
        file_path.parent.mkdir(parents=True, exist_ok=True)
        file_path.write_text(f"# Test file: {row['file']}\n")


@given("files exist:")
def step_create_multiple_files(context: Context) -> None:
    """Create multiple test files."""
    for row in context.table:
        file_path = context.project_dir / row["file"]
        file_path.parent.mkdir(parents=True, exist_ok=True)
        file_path.write_text(f"# Test file: {row['file']}\n")


@given('"{file_path}" is already in context')
def step_file_in_context(context: Context, file_path: str) -> None:
    """Mark a file as already in context."""
    context.context_files.append(file_path)


# ---------------------------------------------------------------------------
# When steps — context list
# ---------------------------------------------------------------------------


@when("I run context list")
def step_run_context_list(context: Context) -> None:
    """Run context list command via CliRunner."""
    entries = getattr(context, "context_entries", [])
    mock_entries = _build_mock_entries(entries)
    result = _invoke_context_list(context_files=mock_entries)
    context.last_command_output = result.output
    context.last_command_exit_code = result.exit_code
    # Also set context.result for compatibility with cli_commands_coverage tests
    context.result = result


@when('I run context list with format "{format_type}"')
def step_run_context_list_with_format(context: Context, format_type: str) -> None:
    """Run context list with specific format via CliRunner."""
    mock_entries = _build_mock_entries(context.context_entries)
    result = _invoke_context_list(
        extra_args=["--format", format_type],
        context_files=mock_entries,
    )
    context.last_command_output = result.output
    context.last_command_exit_code = result.exit_code


@when("I run context list with --help")
def step_run_context_list_help(context: Context) -> None:
    """Run context list with --help via CliRunner."""
    result = _runner.invoke(context_app, ["list", "--help"])
    context.last_command_output = result.output
    context.last_command_exit_code = result.exit_code


# ---------------------------------------------------------------------------
# When steps — context add
# ---------------------------------------------------------------------------


def _register_added_file(
    context: Context,
    file_path: str,
    tag: str | None = None,
    policy: str | None = None,
) -> None:
    """Register a file as added to context and update context_entries for list."""
    context.added_files.append(file_path)
    full_path = context.project_dir / file_path
    size = full_path.stat().st_size if full_path.exists() else 0
    entry: dict[str, Any] = {
        "path": file_path,
        "tier": policy or "hot",
        "size": size,
        "last_accessed": "2026-04-15T10:00:00Z",
    }
    if tag is not None:
        entry["tag"] = tag
    if policy is not None:
        entry["policy"] = policy
    context.context_entries.append(entry)


@when('I run context add "{file_path:FilePath}"')
def step_run_context_add(context: Context, file_path: str) -> None:
    """Run context add command via CliRunner."""
    full_path = context.project_dir / file_path

    if not full_path.exists():
        result = _invoke_context_add(
            paths=[str(full_path)],
            added_files=[],
            already_in_context=[],
        )
        context.last_command_output = result.output
        context.last_command_exit_code = result.exit_code
        return

    if file_path in context.context_files:
        result = _invoke_context_add(
            paths=[str(full_path)],
            added_files=[],
            already_in_context=[full_path],
        )
    else:
        result = _invoke_context_add(
            paths=[str(full_path)],
            added_files=[full_path],
            already_in_context=[],
        )
        _register_added_file(context, file_path)

    context.last_command_output = result.output
    context.last_command_exit_code = result.exit_code


@when('I run context add "{file_path:FilePath}" with --recursive')
def step_run_context_add_recursive(context: Context, file_path: str) -> None:
    """Run context add with recursive flag via CliRunner."""
    full_path = context.project_dir / file_path

    if not full_path.exists():
        result = _invoke_context_add(
            paths=[str(full_path)],
            added_files=[],
            already_in_context=[],
        )
        context.last_command_output = result.output
        context.last_command_exit_code = result.exit_code
        return

    if full_path.is_dir():
        files = [f for f in full_path.rglob("*") if f.is_file()]
        for f in files:
            rel = str(f.relative_to(context.project_dir))
            _register_added_file(context, rel)
        added = files
    else:
        added = [full_path]
        _register_added_file(context, file_path)

    result = _invoke_context_add(
        paths=[str(full_path)],
        extra_args=["--recursive"],
        added_files=added,
        already_in_context=[],
    )
    context.last_command_output = result.output
    context.last_command_exit_code = result.exit_code


@when('I run context add "{file_path:FilePath}" without --recursive')
def step_run_context_add_no_recursive(context: Context, file_path: str) -> None:
    """Run context add without recursive flag via CliRunner."""
    full_path = context.project_dir / file_path

    if not full_path.exists():
        result = _invoke_context_add(
            paths=[str(full_path)],
            added_files=[],
            already_in_context=[],
        )
        context.last_command_output = result.output
        context.last_command_exit_code = result.exit_code
        return

    _register_added_file(context, file_path)
    # Invoke without --recursive flag (default is True, but we don't pass it)
    result = _invoke_context_add(
        paths=[str(full_path)],
        extra_args=[],
        added_files=[full_path],
        already_in_context=[],
    )
    context.last_command_output = result.output
    context.last_command_exit_code = result.exit_code


@when('I run context add "{file_path:FilePath}" with --tag "{tag:Tag}"')
def step_run_context_add_with_tag(context: Context, file_path: str, tag: str) -> None:
    """Run context add with --tag flag via CliRunner."""
    full_path = context.project_dir / file_path

    if not full_path.exists():
        result = _invoke_context_add(
            paths=[str(full_path)],
            added_files=[],
            already_in_context=[],
        )
        context.last_command_output = result.output
        context.last_command_exit_code = result.exit_code
        return

    context.file_metadata[file_path] = {"tag": tag}
    _register_added_file(context, file_path, tag=tag)
    result = _invoke_context_add(
        paths=[str(full_path)],
        extra_args=["--tag", tag],
        added_files=[full_path],
        already_in_context=[],
    )
    context.last_command_output = result.output
    context.last_command_exit_code = result.exit_code


@when('I run context add "{file_path:FilePath}" with --policy "{policy:Policy}"')
def step_run_context_add_with_policy(
    context: Context, file_path: str, policy: str
) -> None:
    """Run context add with --policy flag via CliRunner."""
    full_path = context.project_dir / file_path

    if not full_path.exists():
        result = _invoke_context_add(
            paths=[str(full_path)],
            added_files=[],
            already_in_context=[],
        )
        context.last_command_output = result.output
        context.last_command_exit_code = result.exit_code
        return

    if file_path not in context.file_metadata:
        context.file_metadata[file_path] = {}
    context.file_metadata[file_path]["policy"] = policy
    _register_added_file(context, file_path, policy=policy)
    result = _invoke_context_add(
        paths=[str(full_path)],
        extra_args=["--policy", policy],
        added_files=[full_path],
        already_in_context=[],
    )
    context.last_command_output = result.output
    context.last_command_exit_code = result.exit_code


@when(
    'I run context add "{file_path:FilePath}" with --tag "{tag:Tag}" and --policy "{policy:Policy}"'
)
def step_run_context_add_with_tag_and_policy(
    context: Context, file_path: str, tag: str, policy: str
) -> None:
    """Run context add with both --tag and --policy flags via CliRunner."""
    full_path = context.project_dir / file_path

    if not full_path.exists():
        result = _invoke_context_add(
            paths=[str(full_path)],
            added_files=[],
            already_in_context=[],
        )
        context.last_command_output = result.output
        context.last_command_exit_code = result.exit_code
        return

    context.file_metadata[file_path] = {"tag": tag, "policy": policy}
    _register_added_file(context, file_path, tag=tag, policy=policy)
    result = _invoke_context_add(
        paths=[str(full_path)],
        extra_args=["--tag", tag, "--policy", policy],
        added_files=[full_path],
        already_in_context=[],
    )
    context.last_command_output = result.output
    context.last_command_exit_code = result.exit_code


@when(
    'I run context add "{file_path:FilePath}" "{file_path2:FilePath}" "{file_path3:FilePath}"'
)
def step_run_context_add_multiple(
    context: Context, file_path: str, file_path2: str, file_path3: str
) -> None:
    """Run context add with multiple files via CliRunner."""
    files = [file_path, file_path2, file_path3]
    added: list[Path] = []

    for f in files:
        full_path = context.project_dir / f
        if full_path.exists():
            added.append(full_path)
            _register_added_file(context, f)

    result = _invoke_context_add(
        paths=[str(context.project_dir / f) for f in files],
        added_files=added,
        already_in_context=[],
    )
    context.last_command_output = result.output
    context.last_command_exit_code = result.exit_code


@when("I run context add with --help")
def step_run_context_add_help(context: Context) -> None:
    """Run context add with --help via CliRunner."""
    result = _runner.invoke(context_app, ["add", "--help"])
    context.last_command_output = result.output
    context.last_command_exit_code = result.exit_code


@when('I run context add "{file_path:FilePath}" with format "{format_type}"')
def step_run_context_add_with_format(
    context: Context, file_path: str, format_type: str
) -> None:
    """Run context add with --format flag via CliRunner."""
    full_path = context.project_dir / file_path

    if not full_path.exists():
        result = _invoke_context_add(
            paths=[str(full_path)],
            added_files=[],
            already_in_context=[],
        )
        context.last_command_output = result.output
        context.last_command_exit_code = result.exit_code
        return

    _register_added_file(context, file_path)
    result = _invoke_context_add(
        paths=[str(full_path)],
        extra_args=["--format", format_type],
        added_files=[full_path],
        already_in_context=[],
    )
    context.last_command_output = result.output
    context.last_command_exit_code = result.exit_code


@when("I run context add")
def step_run_context_add_no_args(context: Context) -> None:
    """Run context add without arguments via CliRunner."""
    result = _runner.invoke(context_app, ["add"])
    context.last_command_output = result.output
    context.last_command_exit_code = result.exit_code


# ---------------------------------------------------------------------------
# Then steps
# ---------------------------------------------------------------------------


@then("the context list command should succeed")
def step_context_list_success(context: Context) -> None:
    """Verify context list succeeded."""
    assert context.last_command_exit_code == 0, (
        f"Command failed with exit code {context.last_command_exit_code}.\n"
        f"Output: {context.last_command_output}"
    )


@then("the context list command should fail")
def step_context_list_fail(context: Context) -> None:
    """Verify context list failed."""
    assert context.last_command_exit_code != 0, (
        f"Command should have failed but exit code was {context.last_command_exit_code}"
    )


@then("the context add command should succeed")
def step_context_add_success(context: Context) -> None:
    """Verify context add succeeded."""
    assert context.last_command_exit_code == 0, (
        f"Command failed with exit code {context.last_command_exit_code}.\n"
        f"Output: {context.last_command_output}"
    )


@then("the context add command should fail")
def step_context_add_fail(context: Context) -> None:
    """Verify context add failed."""
    assert context.last_command_exit_code != 0, (
        f"Command should have failed but exit code was {context.last_command_exit_code}"
    )


@then("the output should indicate no files in context")
def step_output_no_files(context: Context) -> None:
    """Verify output indicates no files."""
    assert "No files in context" in context.last_command_output, (
        f"Expected 'No files in context' in output.\nOutput: {context.last_command_output}"
    )


@then("the output should display a table with columns: File Path, Type, Size, Added")
def step_output_table_columns(context: Context) -> None:
    """Verify output contains all four table columns."""
    output = context.last_command_output
    assert "File Path" in output, f"'File Path' not in output.\nOutput: {output}"
    assert "Type" in output, f"'Type' not in output.\nOutput: {output}"
    assert "Size" in output, f"'Size' not in output.\nOutput: {output}"
    assert "Added" in output, f"'Added' not in output.\nOutput: {output}"


@then('the table should contain "{text}"')
def step_table_contains(context: Context, text: str) -> None:
    """Verify table contains text."""
    assert text in context.last_command_output, (
        f"'{text}' not in output.\nOutput: {context.last_command_output}"
    )


@then("the JSON output should contain an array of context entries")
def step_json_contains_entries(context: Context) -> None:
    """Verify JSON contains entries array."""
    assert "entries" in context.json_output or isinstance(context.json_output, list), (
        f"JSON does not contain 'entries'.\nJSON: {context.json_output}"
    )


@then("each entry should have keys: path, tier, size, last_accessed")
def step_json_entry_keys(context: Context) -> None:
    """Verify JSON entries have all required keys."""
    entries = context.json_output.get("entries", context.json_output)
    if entries:
        for entry in entries:
            assert "path" in entry, f"'path' missing from entry: {entry}"
            assert "tier" in entry, f"'tier' missing from entry: {entry}"
            assert "size" in entry, f"'size' missing from entry: {entry}"
            assert "last_accessed" in entry, (
                f"'last_accessed' missing from entry: {entry}"
            )


@then("the output should display a table")
def step_output_is_table(context: Context) -> None:
    """Verify output is a table."""
    assert (
        "|" in context.last_command_output or "File Path" in context.last_command_output
    ), f"Output does not appear to be a table.\nOutput: {context.last_command_output}"


@then("the output should indicate file was added")
def step_output_file_added(context: Context) -> None:
    """Verify output indicates file was added."""
    assert (
        "Added" in context.last_command_output or "✓" in context.last_command_output
    ), (
        f"Output does not indicate file was added.\nOutput: {context.last_command_output}"
    )


@then('the context should contain "{file_path}"')
def step_context_contains_file(context: Context, file_path: str) -> None:
    """Verify context contains file."""
    assert file_path in context.added_files, (
        f"File '{file_path}' not in context.\nAdded files: {context.added_files}"
    )


@then('the context entry for "{file_path}" should have tag "{tag}"')
def step_context_entry_has_tag(context: Context, file_path: str, tag: str) -> None:
    """Verify context entry has tag."""
    assert file_path in context.file_metadata, (
        f"No metadata for '{file_path}'.\nMetadata: {context.file_metadata}"
    )
    assert context.file_metadata[file_path].get("tag") == tag, (
        f"Tag mismatch for '{file_path}': "
        f"expected '{tag}', got '{context.file_metadata[file_path].get('tag')}'"
    )


@then('the context entry for "{file_path}" should have policy "{policy}"')
def step_context_entry_has_policy(
    context: Context, file_path: str, policy: str
) -> None:
    """Verify context entry has policy."""
    assert file_path in context.file_metadata, (
        f"No metadata for '{file_path}'.\nMetadata: {context.file_metadata}"
    )
    assert context.file_metadata[file_path].get("policy") == policy, (
        f"Policy mismatch for '{file_path}': "
        f"expected '{policy}', got '{context.file_metadata[file_path].get('policy')}'"
    )


@then("the output should indicate file was already in context")
def step_output_file_already_in_context(context: Context) -> None:
    """Verify output indicates file was already in context."""
    assert "already in context" in context.last_command_output.lower(), (
        f"Output does not indicate file was already in context.\n"
        f"Output: {context.last_command_output}"
    )


@then("the output should indicate {count:d} files were added")
def step_output_files_added_count(context: Context, count: int) -> None:
    """Verify output indicates correct number of files added."""
    assert str(count) in context.last_command_output, (
        f"Count '{count}' not in output.\nOutput: {context.last_command_output}"
    )


@then("the JSON output should contain entries with:")
def step_json_entries_with_values(context: Context) -> None:
    """Verify JSON entries contain specific values."""
    if not hasattr(context, "json_output"):
        raw = getattr(context, "last_command_output", "")
        try:
            context.json_output = _json.loads(raw)
        except (_json.JSONDecodeError, ValueError) as e:
            raise AssertionError(f"Output is not valid JSON: {e}\nOutput: {raw}") from e
    entries = context.json_output.get("entries", context.json_output)
    for row in context.table:
        field = row["field"]
        value = row["value"]
        found = any(entry.get(field) == value for entry in entries)
        assert found, f"No entry found with {field}={value}.\nEntries: {entries}"
