"""Step definitions for Repository Indexing CLI feature tests."""

from __future__ import annotations

import contextlib
import json
import tempfile
from io import StringIO
from pathlib import Path
from typing import Any
from unittest.mock import patch

from behave import given, then, when  # type: ignore[import-untyped]
from behave.runner import Context  # type: ignore[import-untyped]
from rich.console import Console
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

from cleveragents.application.services.repo_indexing_service import (
    RepoIndexingService,
)
from cleveragents.application.services.resource_registry_service import (
    ResourceRegistryService,
)
from cleveragents.infrastructure.database.models import Base


def _setup_services(
    context: Context,
) -> tuple[ResourceRegistryService, RepoIndexingService]:
    """Return the cached services created by the Background step."""
    return context._repo_cli_resource_svc, context._repo_cli_indexing_svc


def _capture_repo_command(
    context: Context,
    func: Any,
    *args: Any,
    **kwargs: Any,
) -> None:
    """Run a CLI function capturing output and success status.

    Captures both Rich console output (via *buf*) **and** direct
    ``sys.stdout`` writes (used by ``format_output`` for JSON/YAML
    to avoid Rich line-wrapping).
    """
    buf = StringIO()
    stdout_buf = StringIO()
    console = Console(
        file=buf, width=200, no_color=True, highlight=False, force_terminal=False
    )

    import cleveragents.cli.commands.repo as repo_mod

    orig_console = repo_mod.console
    repo_mod.console = console

    failed = False
    try:
        with contextlib.redirect_stdout(stdout_buf):
            func(*args, **kwargs)
    except SystemExit:
        failed = True
    except Exception:
        failed = True
    finally:
        repo_mod.console = orig_console

    # Combine Rich console output with any direct stdout writes
    context.repo_cli_output = buf.getvalue() + stdout_buf.getvalue()
    context.repo_cli_failed = failed


def _run_with_mock(
    context: Context,
    func: Any,
    *args: Any,
    **kwargs: Any,
) -> None:
    """Run a CLI function with mocked container providing test services."""
    resource_svc, indexing_svc = _setup_services(context)

    with patch("cleveragents.cli.commands.repo.get_container") as mock_container:
        container = mock_container.return_value
        container.resource_registry_service.return_value = resource_svc
        container.repo_indexing_service.return_value = indexing_svc
        _capture_repo_command(context, func, *args, **kwargs)


@given("a fresh in-memory repo indexing environment")
def step_fresh_repo_env(context: Context) -> None:
    """Set up fresh in-memory services for repo indexing tests."""
    # Create fresh in-memory database for each scenario.  We cannot
    # ``delattr`` on behave Context (it only works at the current level),
    # so we unconditionally set new services each time.
    engine = create_engine("sqlite:///:memory:", echo=False)
    Base.metadata.create_all(engine)
    factory = sessionmaker(bind=engine, expire_on_commit=False)
    context._repo_cli_engine = engine
    context._repo_cli_factory = factory
    context._repo_cli_resource_svc = ResourceRegistryService(session_factory=factory)
    context._repo_cli_resource_svc.bootstrap_builtin_types()
    context._repo_cli_indexing_svc = RepoIndexingService(session_factory=factory)
    context.repo_cli_output = ""
    context.repo_cli_failed = False
    context._repo_cli_temp_dirs = {}  # dict[str, str]


@given('a registered git-checkout resource "{name}" with a temp directory')
def step_register_resource_with_tempdir(context: Context, name: str) -> None:
    """Register a git-checkout resource backed by a real temporary directory."""
    resource_svc, _indexing_svc = _setup_services(context)

    tmp_dir = tempfile.mkdtemp()
    context._repo_cli_temp_dirs[name] = tmp_dir

    sample = Path(tmp_dir) / "README.md"
    sample.write_text("# Test Repository\n\nSample content for indexing.\n")

    resource_svc.register_resource(
        type_name="git-checkout",
        name=name,
        location=tmp_dir,
        properties={"path": tmp_dir},
    )


@given('I have already indexed "{name}"')
def step_already_indexed(context: Context, name: str) -> None:
    """Perform an initial index on the resource."""
    resource_svc, indexing_svc = _setup_services(context)
    resource = resource_svc.show_resource(name)
    root_path = resource.location or ""
    if not root_path and resource.properties:
        path_val = resource.properties.get("path")
        root_path = str(path_val) if path_val is not None else ""
    indexing_svc.index_resource(resource.resource_id, root_path)


@when('I run repo index on resource "{name}"')
def step_run_repo_index(context: Context, name: str) -> None:
    """Run the repo index command (incremental)."""
    from cleveragents.cli.commands.repo import index_command

    _run_with_mock(context, index_command, name, full=False, fmt="text")


@when('I run repo full-index on resource "{name}"')
def step_run_repo_full_index(context: Context, name: str) -> None:
    """Run the repo index command with --full flag."""
    from cleveragents.cli.commands.repo import index_command

    _run_with_mock(context, index_command, name, full=True, fmt="text")


@when('I run repo index-json on resource "{name}"')
def step_run_repo_index_json(context: Context, name: str) -> None:
    """Run the repo index command with JSON format."""
    from cleveragents.cli.commands.repo import index_command

    _run_with_mock(context, index_command, name, full=False, fmt="json")


@when('I run repo status on resource "{name}"')
def step_run_repo_status(context: Context, name: str) -> None:
    """Run the repo status command."""
    from cleveragents.cli.commands.repo import status_command

    _run_with_mock(context, status_command, name, fmt="text")


@when('I run repo status-json on resource "{name}"')
def step_run_repo_status_json(context: Context, name: str) -> None:
    """Run the repo status command with JSON format."""
    from cleveragents.cli.commands.repo import status_command

    _run_with_mock(context, status_command, name, fmt="json")


@then("the repo command should succeed")
def step_repo_should_succeed(context: Context) -> None:
    """Assert the repo command did not fail."""
    assert not context.repo_cli_failed, (
        f"Expected success but command failed. Output:\n{context.repo_cli_output}"
    )


@then("the repo command should fail")
def step_repo_should_fail(context: Context) -> None:
    """Assert the repo command failed."""
    assert context.repo_cli_failed, (
        f"Expected failure but command succeeded. Output:\n{context.repo_cli_output}"
    )


@then('the repo output should contain "{text}"')
def step_repo_output_contains(context: Context, text: str) -> None:
    """Assert the repo output contains the given text."""
    assert text in context.repo_cli_output, (
        f"Expected output to contain '{text}'. Got:\n{context.repo_cli_output}"
    )


@then("the repo output should be valid JSON")
def step_repo_output_valid_json(context: Context) -> None:
    """Assert the repo output is valid JSON."""
    output = context.repo_cli_output.strip()
    try:
        context.repo_cli_json = json.loads(output)
    except json.JSONDecodeError as exc:
        raise AssertionError(
            f"Output is not valid JSON: {exc}\nOutput:\n{output}"
        ) from exc


@then('the repo JSON output should contain key "{key}"')
def step_repo_json_has_key(context: Context, key: str) -> None:
    """Assert the parsed JSON output contains a key."""
    assert hasattr(context, "repo_cli_json"), "JSON not parsed; call 'valid JSON' first"
    assert key in context.repo_cli_json, (
        f"Key '{key}' not found in JSON: {context.repo_cli_json}"
    )


@then('the repo JSON output field "{key}" should equal "{value}"')
def step_repo_json_field_equals(context: Context, key: str, value: str) -> None:
    """Assert a JSON field equals the expected value."""
    assert hasattr(context, "repo_cli_json"), "JSON not parsed; call 'valid JSON' first"
    actual = str(context.repo_cli_json.get(key, ""))
    assert actual == value, f"Expected {key}='{value}', got '{actual}'"


@given('a registered resource "{name}" with no location')
def step_register_resource_no_location(context: Context, name: str) -> None:
    """Register a resource with no location or path property."""
    resource_svc, _indexing_svc = _setup_services(context)
    resource_svc.register_resource(
        type_name="git-checkout",
        name=name,
        location=None,
        properties={},
    )


@given('a registered resource "{name}" with path in properties only')
def step_register_resource_properties_path(context: Context, name: str) -> None:
    """Register a resource with path only in properties (no location)."""
    resource_svc, _indexing_svc = _setup_services(context)

    tmp_dir = tempfile.mkdtemp()
    context._repo_cli_temp_dirs[name] = tmp_dir

    sample = Path(tmp_dir) / "README.md"
    sample.write_text("# Properties path test\n")

    resource_svc.register_resource(
        type_name="git-checkout",
        name=name,
        location=None,
        properties={"path": tmp_dir},
    )


@given('a registered resource "{name}" with nonexistent path')
def step_register_resource_bad_path(context: Context, name: str) -> None:
    """Register a resource pointing to a nonexistent directory."""
    resource_svc, _indexing_svc = _setup_services(context)
    resource_svc.register_resource(
        type_name="git-checkout",
        name=name,
        location="/tmp/nonexistent-path-for-indexing-test-12345",
        properties={"path": "/tmp/nonexistent-path-for-indexing-test-12345"},
    )
