"""Tests for the ``loom status`` CLI command output formatting.

These tests use Click's CliRunner — no Docker, Postgres, or Redis required.
The ``_show_status`` async function is tested by mocking asyncpg and
project status queries.
"""

from __future__ import annotations

import re
from pathlib import Path
from unittest.mock import AsyncMock, patch

import pytest
import yaml
from click.testing import CliRunner

from loom.cli import cli


def _strip_ansi(text: str) -> str:
    """Remove ANSI escape codes from text for assertion matching."""
    return re.sub(r"\x1b\[[0-9;]*m", "", text)


@pytest.fixture
def runner() -> CliRunner:
    return CliRunner()


def _make_config_dir(base: Path) -> Path:
    """Create a .loom/config.yaml inside *base* and return *base*."""
    loom_dir = base / ".loom"
    loom_dir.mkdir(parents=True, exist_ok=True)
    config = {
        "loom": {
            "project_name": "status-test",
            "project_id": "00000000-0000-0000-0000-000000000000",
        },
        "database": {
            "url": "postgresql://loom:loom@localhost:5432/loom",
        },
        "redis": {
            "url": "redis://localhost:6379",
        },
        "logging": {
            "level": "INFO",
        },
    }
    with open(loom_dir / "config.yaml", "w") as f:
        yaml.dump(config, f, default_flow_style=False, sort_keys=False)
    return base


class TestStatusOutput:
    """Tests for the ``loom status`` command output."""

    def test_empty_state(self, runner: CliRunner) -> None:
        """No tasks shows 0/0 tasks complete (0%)."""
        from loom.graph.task import ProjectStatus

        mock_ps = ProjectStatus(
            project_id="00000000-0000-0000-0000-000000000000",
            project_name="status-test",
            total=0,
            pending=0,
            claimed=0,
            done=0,
            failed=0,
            blocked=0,
            epic=0,
            ready_count=0,
        )

        mock_pool = AsyncMock()
        mock_pool.close = AsyncMock()

        with runner.isolated_filesystem() as td:
            _make_config_dir(Path(td))
            with (
                patch("asyncpg.create_pool", new_callable=AsyncMock, return_value=mock_pool),
                patch("loom.graph.project.get_project_status", new_callable=AsyncMock, return_value=mock_ps),
                patch("loom.graph.project.get_project", new_callable=AsyncMock, return_value={"created_at": None}),
            ):
                result = runner.invoke(cli, ["status"])

        assert result.exit_code == 0, f"output: {result.output}\nexception: {result.exception}"
        plain = _strip_ansi(result.output)
        assert "0/0 tasks complete" in plain
        assert "0%" in plain

    def test_partial_completion(self, runner: CliRunner) -> None:
        """Some tasks done shows correct count and percentage."""
        from loom.graph.task import ProjectStatus

        mock_ps = ProjectStatus(
            project_id="00000000-0000-0000-0000-000000000000",
            project_name="status-test",
            total=12,
            pending=3,
            claimed=2,
            done=5,
            failed=0,
            blocked=0,
            epic=2,
            ready_count=3,
        )

        mock_pool = AsyncMock()
        mock_pool.close = AsyncMock()

        with runner.isolated_filesystem() as td:
            _make_config_dir(Path(td))
            with (
                patch("asyncpg.create_pool", new_callable=AsyncMock, return_value=mock_pool),
                patch("loom.graph.project.get_project_status", new_callable=AsyncMock, return_value=mock_ps),
                patch("loom.graph.project.get_project", new_callable=AsyncMock, return_value={"created_at": None}),
            ):
                result = runner.invoke(cli, ["status"])

        assert result.exit_code == 0, f"output: {result.output}\nexception: {result.exception}"
        plain = _strip_ansi(result.output)
        # actionable = total - epic = 12 - 2 = 10, completed = 5
        assert "5/10 tasks complete" in plain
        assert "50%" in plain

    def test_full_completion(self, runner: CliRunner) -> None:
        """All tasks done shows 100%."""
        from loom.graph.task import ProjectStatus

        mock_ps = ProjectStatus(
            project_id="00000000-0000-0000-0000-000000000000",
            project_name="status-test",
            total=8,
            pending=0,
            claimed=0,
            done=6,
            failed=0,
            blocked=0,
            epic=2,
            ready_count=0,
        )

        mock_pool = AsyncMock()
        mock_pool.close = AsyncMock()

        with runner.isolated_filesystem() as td:
            _make_config_dir(Path(td))
            with (
                patch("asyncpg.create_pool", new_callable=AsyncMock, return_value=mock_pool),
                patch("loom.graph.project.get_project_status", new_callable=AsyncMock, return_value=mock_ps),
                patch("loom.graph.project.get_project", new_callable=AsyncMock, return_value={"created_at": None}),
            ):
                result = runner.invoke(cli, ["status"])

        assert result.exit_code == 0, f"output: {result.output}\nexception: {result.exception}"
        plain = _strip_ansi(result.output)
        # actionable = 8 - 2 = 6, completed = 6
        assert "6/6 tasks complete" in plain
        assert "100%" in plain

    def test_error_state(self, runner: CliRunner) -> None:
        """Failed tasks are reflected in the output."""
        from loom.graph.task import ProjectStatus

        mock_ps = ProjectStatus(
            project_id="00000000-0000-0000-0000-000000000000",
            project_name="status-test",
            total=10,
            pending=2,
            claimed=1,
            done=3,
            failed=2,
            blocked=1,
            epic=1,
            ready_count=2,
            blocked_tasks=["loom-blocked1"],
        )

        mock_pool = AsyncMock()
        mock_pool.close = AsyncMock()

        with runner.isolated_filesystem() as td:
            _make_config_dir(Path(td))
            with (
                patch("asyncpg.create_pool", new_callable=AsyncMock, return_value=mock_pool),
                patch("loom.graph.project.get_project_status", new_callable=AsyncMock, return_value=mock_ps),
                patch("loom.graph.project.get_project", new_callable=AsyncMock, return_value={"created_at": None}),
            ):
                result = runner.invoke(cli, ["status"])

        assert result.exit_code == 0, f"output: {result.output}\nexception: {result.exception}"
        plain = _strip_ansi(result.output)
        # Failed count should appear in the status table
        assert "2" in plain  # failed count
        # Blocked task ID should appear
        assert "loom-blocked1" in plain

    def test_status_db_connection_error(self, runner: CliRunner) -> None:
        """When DB query fails, status shows error message gracefully."""
        mock_pool = AsyncMock()
        mock_pool.close = AsyncMock()

        with runner.isolated_filesystem() as td:
            _make_config_dir(Path(td))
            with (
                patch("asyncpg.create_pool", new_callable=AsyncMock, return_value=mock_pool),
                patch(
                    "loom.graph.project.get_project_status",
                    new_callable=AsyncMock,
                    side_effect=Exception("Connection refused"),
                ),
            ):
                result = runner.invoke(cli, ["status"])

        assert result.exit_code == 0, f"output: {result.output}\nexception: {result.exception}"
        plain = _strip_ansi(result.output)
        assert "could not load project status" in plain.lower()
