"""Step definitions for cleanup CLI uncovered branches.

Covers missed lines and branches in
``cleveragents.cli.commands.cleanup``:

* L49-50  - inner ``OSError`` on ``p.stat()`` inside ``_detect_active_plan_ids``
* L52-58  - outer ``OSError`` from ``_get_sandbox_dirs`` -> warning + empty frozenset
* L44->42  - empty sandbox dir list (loop end)
* L87->94  - ``scan()`` with no stale items -> "No stale resources found."
* L128-131 - ``purge --dry-run`` with stale items -> "Would clean:" listing
* L139->143 - ``purge`` confirmation prompt accepted -> proceeds
"""

from __future__ import annotations

from pathlib import Path
from unittest.mock import MagicMock, patch

from behave import given, then, when
from behave.runner import Context

from cleveragents.application.services.cleanup_service import (
    CleanupReport,
    StaleItem,
)

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


def _empty_report() -> CleanupReport:
    """Return a ``CleanupReport`` with no stale items (dry-run)."""
    return CleanupReport(dry_run=True)


def _report_with_stale_items() -> CleanupReport:
    """Return a dry-run ``CleanupReport`` containing stale items."""
    report = CleanupReport(dry_run=True)
    report.sandboxes.scanned = 1
    report.sandboxes.removed = 1
    report.stale_items = [
        StaleItem(
            resource_type="sandbox",
            path="/tmp/fake-sandbox",
            age_description="3 days old",
            plan_id="plan-001",
        ),
    ]
    return report


def _purge_report() -> CleanupReport:
    """Return a non-dry-run ``CleanupReport`` for purge results."""
    report = CleanupReport(dry_run=False)
    report.sandboxes.scanned = 1
    report.sandboxes.removed = 1
    return report


# ---------------------------------------------------------------------------
# Givens
# ---------------------------------------------------------------------------


@given("cleanup cli branch mock service returns empty report")
def step_mock_service_empty_report(context: Context) -> None:
    mock_svc = MagicMock()
    mock_svc.scan.return_value = _empty_report()
    context._cleanup_branch_mock_svc = mock_svc


@given("cleanup cli branch mock service returns report with stale items")
def step_mock_service_stale_report(context: Context) -> None:
    mock_svc = MagicMock()
    mock_svc.scan.return_value = _report_with_stale_items()
    context._cleanup_branch_mock_svc = mock_svc


@given("cleanup cli branch mock service returns purge report")
def step_mock_service_purge_report(context: Context) -> None:
    mock_svc = MagicMock()
    mock_svc.scan.return_value = _empty_report()
    mock_svc.purge.return_value = _purge_report()
    context._cleanup_branch_mock_svc = mock_svc


@given("cleanup cli branch a service whose sandbox dir stat raises OSError")
def step_service_stat_oserror(context: Context) -> None:
    """Build a mock service where one sandbox path raises OSError on stat().

    ``extract_plan_id_from_sandbox`` returns a truthy plan ID so the code
    enters the ``try: mtime = p.stat()`` block and hits the inner ``except
    OSError: pass`` (L49-50).
    """
    bad_path = MagicMock(spec=Path)
    bad_path.stat.side_effect = OSError("permission denied")

    mock_svc = MagicMock()
    mock_svc._get_sandbox_dirs.return_value = [bad_path]
    mock_svc.extract_plan_id_from_sandbox.return_value = "plan-abc"
    context._cleanup_branch_mock_svc = mock_svc


@given("cleanup cli branch a service whose get sandbox dirs raises OSError")
def step_service_get_sandbox_dirs_oserror(context: Context) -> None:
    mock_svc = MagicMock()
    mock_svc._get_sandbox_dirs.side_effect = OSError("cannot list /tmp")
    context._cleanup_branch_mock_svc = mock_svc


@given("cleanup cli branch a service with no sandbox dirs")
def step_service_empty_sandbox_dirs(context: Context) -> None:
    mock_svc = MagicMock()
    mock_svc._get_sandbox_dirs.return_value = []
    context._cleanup_branch_mock_svc = mock_svc


# ---------------------------------------------------------------------------
# Whens
# ---------------------------------------------------------------------------


@when("cleanup cli branch I invoke scan via main app")
def step_invoke_scan(context: Context) -> None:
    from typer.testing import CliRunner

    from cleveragents.cli.main import app

    runner = CliRunner()
    with patch(
        "cleveragents.cli.commands.cleanup._get_cleanup_service",
        return_value=context._cleanup_branch_mock_svc,
    ):
        context._cleanup_branch_result = runner.invoke(app, ["cleanup", "scan"])


@when("cleanup cli branch I invoke purge dry-run via main app")
def step_invoke_purge_dry_run(context: Context) -> None:
    from typer.testing import CliRunner

    from cleveragents.cli.main import app

    runner = CliRunner()
    with patch(
        "cleveragents.cli.commands.cleanup._get_cleanup_service",
        return_value=context._cleanup_branch_mock_svc,
    ):
        context._cleanup_branch_result = runner.invoke(
            app, ["cleanup", "purge", "--dry-run"]
        )


@when("cleanup cli branch I invoke purge with confirmation y via main app")
def step_invoke_purge_confirm_y(context: Context) -> None:
    from typer.testing import CliRunner

    from cleveragents.cli.main import app

    runner = CliRunner()
    with patch(
        "cleveragents.cli.commands.cleanup._get_cleanup_service",
        return_value=context._cleanup_branch_mock_svc,
    ):
        context._cleanup_branch_result = runner.invoke(
            app, ["cleanup", "purge"], input="y\n"
        )


@when("cleanup cli branch I call detect active plan ids")
def step_call_detect_active_plan_ids(context: Context) -> None:
    from cleveragents.cli.commands.cleanup import _detect_active_plan_ids

    # Capture stderr output for the warning assertion
    context._cleanup_branch_warnings: list[str] = []

    mock_err_console = MagicMock()

    def capture_print(*args, **kwargs):
        context._cleanup_branch_warnings.append(str(args))

    mock_err_console.print = capture_print

    with patch(
        "cleveragents.cli.commands.cleanup.get_err_console",
        return_value=mock_err_console,
    ):
        context._cleanup_branch_detect_result = _detect_active_plan_ids(
            context._cleanup_branch_mock_svc,
        )


# ---------------------------------------------------------------------------
# Thens
# ---------------------------------------------------------------------------


@then("cleanup cli branch exit code is {code:d}")
def step_check_exit_code(context: Context, code: int) -> None:
    result = context._cleanup_branch_result
    assert result.exit_code == code, (
        f"Expected exit code {code}, got {result.exit_code}.\n"
        f"Output: {result.output}\n"
        f"Exception: {result.exception!r}"
    )


@then('cleanup cli branch output contains "{text}"')
def step_output_contains(context: Context, text: str) -> None:
    output = context._cleanup_branch_result.output
    assert text in output, f"Expected '{text}' in output, got:\n{output}"


@then("cleanup cli branch the result is an empty frozenset")
def step_result_empty_frozenset(context: Context) -> None:
    result = context._cleanup_branch_detect_result
    assert isinstance(result, frozenset), f"Expected frozenset, got {type(result)}"
    assert len(result) == 0, f"Expected empty frozenset, got {result}"


@then("cleanup cli branch a warning was printed about active plans")
def step_warning_printed(context: Context) -> None:
    warnings = context._cleanup_branch_warnings
    combined = " ".join(warnings)
    assert "active plans" in combined.lower() or "Warning" in combined, (
        f"Expected warning about active plans, got: {combined}"
    )
