"""Tests for loom.roundcheck — pytest output parsing and round summary.

Phase 7, Stream F: Comprehensive round summary tests.
"""

from __future__ import annotations

from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from loom.roundcheck import build_round_summary, parse_pytest_output


# ── parse_pytest_output ───────────────────────────────────────────────


class TestParsePytestOutput:
    def test_all_passed(self):
        output = "===== 42 passed in 3.21s ====="
        result = parse_pytest_output(output)
        assert result["passed"] == 42
        assert result["failed"] == 0
        assert result["errors"] == 0
        assert result["total"] == 42

    def test_mixed_results(self):
        output = "===== 10 passed, 3 failed, 1 error in 5.67s ====="
        result = parse_pytest_output(output)
        assert result["passed"] == 10
        assert result["failed"] == 3
        assert result["errors"] == 1
        assert result["total"] == 14

    def test_only_failures(self):
        output = "===== 5 failed in 1.23s ====="
        result = parse_pytest_output(output)
        assert result["passed"] == 0
        assert result["failed"] == 5
        assert result["total"] == 5

    def test_only_two_failed(self):
        output = "=== 2 failed in 0.5s ==="
        result = parse_pytest_output(output)
        assert result == {"passed": 0, "failed": 2, "errors": 0, "total": 2}

    def test_empty_output(self):
        result = parse_pytest_output("")
        assert result["passed"] == 0
        assert result["failed"] == 0
        assert result["errors"] == 0
        assert result["total"] == 0

    def test_multiline_output(self):
        output = """
FAILED tests/test_foo.py::test_bar - AssertionError
FAILED tests/test_foo.py::test_baz - ValueError
===== 8 passed, 2 failed in 2.34s =====
"""
        result = parse_pytest_output(output)
        assert result["passed"] == 8
        assert result["failed"] == 2
        assert result["total"] == 10

    def test_multiline_with_individual_lines(self):
        output = (
            "tests/test_foo.py::test_one PASSED\n"
            "tests/test_foo.py::test_two FAILED\n"
            "\n"
            "=== 1 passed, 1 failed in 0.12s ==="
        )
        result = parse_pytest_output(output)
        assert result == {"passed": 1, "failed": 1, "errors": 0, "total": 2}

    def test_passed_and_errors(self):
        output = "=== 5 passed, 2 errors in 1.00s ==="
        result = parse_pytest_output(output)
        assert result == {"passed": 5, "failed": 0, "errors": 2, "total": 7}

    def test_with_warnings(self):
        output = "===== 15 passed, 2 warnings in 1.00s ====="
        result = parse_pytest_output(output)
        assert result["passed"] == 15
        assert result["failed"] == 0
        assert result["total"] == 15  # warnings not counted

    def test_no_summary_line(self):
        """Output without a pytest summary line should return zeros."""
        output = "some random output\nno summary here\n"
        result = parse_pytest_output(output)
        assert result["total"] == 0

    def test_with_skipped(self):
        """Skipped tests are not parsed but should not break parsing."""
        output = "=== 10 passed, 1 skipped in 2.00s ==="
        result = parse_pytest_output(output)
        assert result["passed"] == 10
        assert result["failed"] == 0
        assert result["total"] == 10  # skipped not counted

    def test_large_numbers(self):
        output = "=== 658 passed, 0 failed in 45.67s ==="
        result = parse_pytest_output(output)
        assert result["passed"] == 658
        assert result["failed"] == 0
        assert result["total"] == 658

    def test_errors_plural(self):
        """'errors' (plural) should be parsed correctly."""
        output = "=== 3 passed, 5 errors in 1.00s ==="
        result = parse_pytest_output(output)
        assert result["errors"] == 5

    def test_single_error(self):
        """'1 error' (singular) should be parsed correctly."""
        output = "=== 3 passed, 1 error in 1.00s ==="
        result = parse_pytest_output(output)
        assert result["errors"] == 1


# ── build_round_summary ──────────────────────────────────────────────


class TestBuildRoundSummary:
    @pytest.mark.asyncio
    async def test_with_tests_passing(self):
        mock_pool = MagicMock()
        mock_status = MagicMock()
        mock_status.model_dump.return_value = {
            "project_id": "abc", "total": 10, "done": 5, "pending": 3,
        }

        test_result = {"passed": 42, "failed": 0, "errors": 0, "total": 42, "returncode": 0}

        with (
            patch("loom.roundcheck._run_pytest", new_callable=AsyncMock, return_value=test_result),
            patch("loom.graph.project.get_project_status", new_callable=AsyncMock, return_value=mock_status),
        ):
            result = await build_round_summary(
                pool=mock_pool, project_id="abc", project_dir="/tmp", run_tests=True,
            )

        assert result["tests"]["passed"] == 42
        assert result["tests"]["returncode"] == 0
        assert result["project"]["total"] == 10
        assert len(result["checklist"]) == 2
        assert result["checklist"][0] == {"step": "Run tests", "status": "pass"}
        assert result["checklist"][1] == {"step": "Project snapshot", "status": "done"}

    @pytest.mark.asyncio
    async def test_with_tests_failing(self):
        mock_pool = MagicMock()
        mock_status = MagicMock()
        mock_status.model_dump.return_value = {"project_id": "abc", "total": 10}

        test_result = {"passed": 8, "failed": 2, "errors": 0, "total": 10, "returncode": 1}

        with (
            patch("loom.roundcheck._run_pytest", new_callable=AsyncMock, return_value=test_result),
            patch("loom.graph.project.get_project_status", new_callable=AsyncMock, return_value=mock_status),
        ):
            result = await build_round_summary(
                pool=mock_pool, project_id="abc", project_dir="/tmp", run_tests=True,
            )

        assert result["tests"]["failed"] == 2
        assert result["checklist"][0] == {"step": "Run tests", "status": "fail"}

    @pytest.mark.asyncio
    async def test_skip_tests(self):
        """run_tests=False should skip test execution."""
        mock_pool = MagicMock()
        mock_status = MagicMock()
        mock_status.model_dump.return_value = {"project_id": "abc", "total": 5}

        with patch("loom.graph.project.get_project_status", new_callable=AsyncMock, return_value=mock_status):
            result = await build_round_summary(
                pool=mock_pool, project_id="abc", project_dir="/tmp", run_tests=False,
            )

        assert result["tests"] is None
        assert any(c["status"] == "skipped" for c in result["checklist"])
        assert result["checklist"][0] == {"step": "Run tests", "status": "skipped"}
        assert result["project"]["total"] == 5

    @pytest.mark.asyncio
    async def test_project_snapshot(self):
        """Should include project status snapshot."""
        mock_pool = MagicMock()
        mock_status = MagicMock()
        mock_status.model_dump.return_value = {
            "project_id": "abc", "total": 20, "done": 15, "pending": 5,
        }

        with patch("loom.graph.project.get_project_status", new_callable=AsyncMock, return_value=mock_status):
            result = await build_round_summary(
                pool=mock_pool, project_id="abc", project_dir="/tmp", run_tests=False,
            )

        assert result["project"] is not None
        assert "total" in result["project"]
        assert result["project"]["total"] == 20

    @pytest.mark.asyncio
    async def test_project_status_error(self):
        mock_pool = MagicMock()
        test_result = {"passed": 5, "failed": 0, "errors": 0, "total": 5, "returncode": 0}

        with (
            patch("loom.roundcheck._run_pytest", new_callable=AsyncMock, return_value=test_result),
            patch("loom.graph.project.get_project_status", new_callable=AsyncMock, side_effect=Exception("DB down")),
        ):
            result = await build_round_summary(
                pool=mock_pool, project_id="abc", project_dir="/tmp", run_tests=True,
            )

        assert result["tests"]["passed"] == 5
        assert result["project"] is None
        assert result["checklist"][1] == {"step": "Project snapshot", "status": "error"}

    @pytest.mark.asyncio
    async def test_checklist_structure(self):
        """Checklist should have expected steps."""
        mock_pool = MagicMock()
        mock_status = MagicMock()
        mock_status.model_dump.return_value = {"project_id": "abc", "total": 5}

        with patch("loom.graph.project.get_project_status", new_callable=AsyncMock, return_value=mock_status):
            summary = await build_round_summary(
                pool=mock_pool, project_id="abc", project_dir=".", run_tests=False,
            )

        steps = [c["step"] for c in summary["checklist"]]
        assert "Run tests" in steps
        assert "Project snapshot" in steps

    @pytest.mark.asyncio
    async def test_test_run_exception(self):
        """Exception during test run should produce error status in checklist."""
        mock_pool = MagicMock()
        mock_status = MagicMock()
        mock_status.model_dump.return_value = {"project_id": "abc", "total": 5}

        with (
            patch("loom.roundcheck._run_pytest", new_callable=AsyncMock, side_effect=Exception("pytest not found")),
            patch("loom.graph.project.get_project_status", new_callable=AsyncMock, return_value=mock_status),
        ):
            result = await build_round_summary(
                pool=mock_pool, project_id="abc", project_dir="/tmp", run_tests=True,
            )

        assert result["tests"] is None
        assert result["checklist"][0] == {"step": "Run tests", "status": "error"}
        # Project snapshot should still succeed
        assert result["project"] is not None

    @pytest.mark.asyncio
    async def test_both_steps_error(self):
        """Both test run and project snapshot failing should produce two error entries."""
        mock_pool = MagicMock()

        with (
            patch("loom.roundcheck._run_pytest", new_callable=AsyncMock, side_effect=Exception("test fail")),
            patch("loom.graph.project.get_project_status", new_callable=AsyncMock, side_effect=Exception("db fail")),
        ):
            result = await build_round_summary(
                pool=mock_pool, project_id="abc", project_dir="/tmp", run_tests=True,
            )

        assert result["tests"] is None
        assert result["project"] is None
        assert len(result["checklist"]) == 2
        assert result["checklist"][0] == {"step": "Run tests", "status": "error"}
        assert result["checklist"][1] == {"step": "Project snapshot", "status": "error"}

    @pytest.mark.asyncio
    async def test_summary_dict_keys(self):
        """Summary should always have tests, project, and checklist keys."""
        mock_pool = MagicMock()
        mock_status = MagicMock()
        mock_status.model_dump.return_value = {"project_id": "abc", "total": 0}

        with patch("loom.graph.project.get_project_status", new_callable=AsyncMock, return_value=mock_status):
            result = await build_round_summary(
                pool=mock_pool, project_id="abc", project_dir="/tmp", run_tests=False,
            )

        assert "tests" in result
        assert "project" in result
        assert "checklist" in result
