"""Tests for validator integration in loom.skills.decomposer.

Covers:
  - _validate_tasks() annotates task contexts with validation status
  - DecompositionResult.validation_summary counts
  - Validation skipped when codebase_summary is None
  - Graceful failure when validator raises an exception
"""

from __future__ import annotations

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

import pytest

from loom.scanner import CodebaseSummary, FileSymbols
from loom.skills.decomposer import (
    DecompositionResult,
    TaskDefinition,
    _validate_tasks,
)


# ── Helpers ──────────────────────────────────────────────────────────


def _make_summary(*file_specs: tuple[str, list[str], list[str]]) -> CodebaseSummary:
    files = [
        FileSymbols(path=path, functions=funcs, classes=classes)
        for path, funcs, classes in file_specs
    ]
    return CodebaseSummary(root="/tmp/test-project", files=files)


SAMPLE_SUMMARY = _make_summary(
    ("loom/graph/store.py", ["create_task", "get_task", "update_task"], ["TaskStore"]),
    ("loom/graph/cache.py", ["sync_task", "get_cached_task"], ["CacheManager"]),
)


# ── Tests: _validate_tasks ───────────────────────────────────────────


class TestValidateTasks:
    """_validate_tasks annotates task contexts and returns summary counts."""

    def test_basic_validation(self) -> None:
        task_defs = [
            TaskDefinition(
                title="Implement create_task function",
                context={},
            ),
            TaskDefinition(
                title="Deploy to production",
                context={},
            ),
        ]

        result_defs, summary = _validate_tasks(task_defs, SAMPLE_SUMMARY)

        # First task should match existing code (create_task symbol)
        assert result_defs[0].context["validation_status"] in ("pending", "uncertain", "pre_completed")
        assert "matched_artifact" in result_defs[0].context
        assert "match_rationale" in result_defs[0].context

        # Second task should not match (no deployment code)
        assert result_defs[1].context["validation_status"] == "pending"

        # Summary should have counts
        assert isinstance(summary, dict)
        total = summary["pending"] + summary["uncertain"] + summary["pre_completed"]
        assert total == 2  # Two non-epic tasks

    def test_epics_skipped(self) -> None:
        task_defs = [
            TaskDefinition(title="Infrastructure Epic", type="epic"),
            TaskDefinition(title="Deploy server", context={}),
        ]

        result_defs, summary = _validate_tasks(task_defs, SAMPLE_SUMMARY)

        # Epic should not have validation keys
        assert "validation_status" not in result_defs[0].context
        # Only the non-epic task counted
        total = summary["pending"] + summary["uncertain"] + summary["pre_completed"]
        assert total == 1

    def test_high_confidence_match(self) -> None:
        """A task mentioning an exact file path should get pre_completed or uncertain."""
        task_defs = [
            TaskDefinition(
                title="Update loom/graph/store.py to add batching",
                context={},
            ),
        ]

        result_defs, summary = _validate_tasks(task_defs, SAMPLE_SUMMARY)

        assert result_defs[0].context["validation_status"] in ("pre_completed", "uncertain")
        assert result_defs[0].context["matched_artifact"] != ""

    def test_no_match_gets_pending(self) -> None:
        task_defs = [
            TaskDefinition(
                title="Write API documentation for deployment guide",
                context={},
            ),
        ]

        result_defs, summary = _validate_tasks(task_defs, SAMPLE_SUMMARY)

        assert result_defs[0].context["validation_status"] == "pending"
        assert summary["pending"] == 1
        assert summary["pre_completed"] == 0

    def test_empty_task_list(self) -> None:
        result_defs, summary = _validate_tasks([], SAMPLE_SUMMARY)

        assert result_defs == []
        assert summary == {"pending": 0, "uncertain": 0, "pre_completed": 0}

    def test_context_preserved(self) -> None:
        """Existing context fields are preserved alongside validation keys."""
        task_defs = [
            TaskDefinition(
                title="Fix deployment",
                context={"description": "Fix the deploy script", "priority_reason": "urgent"},
            ),
        ]

        result_defs, summary = _validate_tasks(task_defs, SAMPLE_SUMMARY)

        assert result_defs[0].context["description"] == "Fix the deploy script"
        assert result_defs[0].context["priority_reason"] == "urgent"
        assert "validation_status" in result_defs[0].context


# ── Tests: DecompositionResult.validation_summary ─────────────────────


class TestDecompositionResultValidationSummary:
    """The validation_summary field on DecompositionResult."""

    def test_default_is_none(self) -> None:
        result = DecompositionResult()
        assert result.validation_summary is None

    def test_with_summary(self) -> None:
        result = DecompositionResult(
            validation_summary={"pending": 5, "uncertain": 2, "pre_completed": 1},
        )
        assert result.validation_summary["pre_completed"] == 1
        assert result.validation_summary["pending"] == 5

    def test_serialization(self) -> None:
        result = DecompositionResult(
            validation_summary={"pending": 3, "uncertain": 1, "pre_completed": 0},
        )
        dumped = result.model_dump(mode="json")
        assert dumped["validation_summary"] == {"pending": 3, "uncertain": 1, "pre_completed": 0}

    def test_serialization_when_none(self) -> None:
        result = DecompositionResult()
        dumped = result.model_dump(mode="json")
        assert dumped["validation_summary"] is None


# ── Tests: Validation skipped when no summary ─────────────────────────


class TestValidationSkippedWithoutSummary:
    """Validation is skipped when codebase_summary is None in decompose()."""

    @patch("loom.skills.decomposer.run_skill")
    @patch("loom.skills.decomposer.load_skill")
    async def test_no_summary_no_validation(
        self, mock_load: MagicMock, mock_run: MagicMock,
    ) -> None:
        from loom.skills.decomposer import decompose

        mock_run.return_value = {
            "tasks": [
                {"title": "Task A", "type": "task", "done_when": "tests pass"},
            ]
        }

        with patch("loom.skills.decomposer._validate_tasks") as mock_validate:
            result = await decompose(
                goal="Build something",
                project_id="proj-123",
                config=MagicMock(),
                pool=MagicMock(),
                redis=MagicMock(),
                codebase_summary=None,
                enrich=False,
                confirm=True,
            )

        mock_validate.assert_not_called()
        assert result.validation_summary is None

    @patch("loom.skills.decomposer.run_skill")
    @patch("loom.skills.decomposer.load_skill")
    async def test_with_summary_runs_validation(
        self, mock_load: MagicMock, mock_run: MagicMock,
    ) -> None:
        from loom.skills.decomposer import decompose

        mock_run.return_value = {
            "tasks": [
                {"title": "Task A", "type": "task", "done_when": "tests pass"},
            ]
        }

        result = await decompose(
            goal="Build something",
            project_id="proj-123",
            config=MagicMock(),
            pool=MagicMock(),
            redis=MagicMock(),
            codebase_summary=SAMPLE_SUMMARY,
            enrich=False,
            confirm=True,
        )

        assert result.validation_summary is not None
        assert isinstance(result.validation_summary, dict)
        assert "pending" in result.validation_summary


# ── Tests: Graceful failure ───────────────────────────────────────────


class TestValidationGracefulFailure:
    """Exceptions in _validate_tasks don't break decompose()."""

    @patch("loom.skills.decomposer._validate_tasks", side_effect=RuntimeError("validator broke"))
    @patch("loom.skills.decomposer.run_skill")
    @patch("loom.skills.decomposer.load_skill")
    async def test_validator_exception_continues(
        self, mock_load: MagicMock, mock_run: MagicMock, mock_validate: MagicMock,
    ) -> None:
        from loom.skills.decomposer import decompose

        mock_run.return_value = {
            "tasks": [
                {"title": "Task A", "type": "task", "done_when": "tests pass"},
            ]
        }

        result = await decompose(
            goal="Build something",
            project_id="proj-123",
            config=MagicMock(),
            pool=MagicMock(),
            redis=MagicMock(),
            codebase_summary=SAMPLE_SUMMARY,
            enrich=False,
            confirm=True,
        )

        # Decompose succeeded despite validator failure
        assert result.total_count == 1
        # _validate_tasks failed so its counts are absent, but the post-decompose
        # pruning pass may still populate a "pruning" sub-key.
        if result.validation_summary is not None:
            assert "pending" not in result.validation_summary
            assert "pruning" in result.validation_summary
