"""End-to-end tests proving that codebase-aware decomposition reduces redundant tasks.

Validates the full enrichment pipeline:
  1. Scanner detects existing files in a project directory
  2. Validator classifies tasks referencing existing code as pre_completed
  3. Validator classifies tasks for genuinely new work as pending
  4. Decomposer injects codebase context into the LLM prompt
  5. Combined scanner + validator correctly separates redundant from new tasks
"""

from __future__ import annotations

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

import pytest

from loom.scanner import CodebaseSummary, FileSymbols, scan_codebase
from loom.skills.decomposer import (
    CODEBASE_CONTEXT_SENTINEL,
    build_codebase_context,
)
from loom.validator import (
    ValidationStatus,
    ValidatorConfig,
    classify_match,
    match_task_to_codebase,
)


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


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


def _write_python_file(base: Path, rel_path: str, content: str) -> Path:
    """Write a Python file into a tmp_path project tree."""
    full = base / rel_path
    full.parent.mkdir(parents=True, exist_ok=True)
    full.write_text(content, encoding="utf-8")
    return full


# ── Test class ───────────────────────────────────────────────────────


class TestEnrichmentReducesRedundancy:
    """Proves that scanner + validator + decomposer integration eliminates
    redundant tasks by detecting existing code and classifying tasks accordingly.
    """

    def test_scanner_detects_existing_files(self, tmp_path: Path) -> None:
        """scan_codebase() finds files in the project and includes them in
        the CodebaseSummary.
        """
        # Create a small project with a known file
        _write_python_file(
            tmp_path,
            "loom/__main__.py",
            '"""Entry point."""\n\ndef main():\n    print("hello")\n',
        )
        _write_python_file(
            tmp_path,
            "loom/cli.py",
            '"""CLI module."""\n\nimport click\n\ndef cli():\n    pass\n',
        )

        summary = scan_codebase(tmp_path)

        # The summary must contain both files
        file_paths = [fs.path for fs in summary.files]
        assert "loom/__main__.py" in file_paths, (
            f"Expected loom/__main__.py in scanned files, got: {file_paths}"
        )
        assert "loom/cli.py" in file_paths, (
            f"Expected loom/cli.py in scanned files, got: {file_paths}"
        )

        # Verify symbols were extracted
        main_entry = next(fs for fs in summary.files if fs.path == "loom/__main__.py")
        assert "main" in main_entry.functions

    def test_validator_matches_existing_file_task(self) -> None:
        """A task referencing an existing file path gets confidence >= 0.85
        (the pre_completed threshold), proving the validator flags it as
        already implemented.
        """
        summary = _make_summary(
            ("loom/__main__.py", ["main"], []),
            ("loom/cli.py", ["cli"], ["CLI"]),
        )

        # Task that references an existing file by exact path
        task_dict = {
            "title": "Create loom/__main__.py entry point",
            "context": {"files": ["loom/__main__.py"]},
        }

        result = match_task_to_codebase(task_dict, summary)
        status = classify_match(result)

        assert result.confidence >= 0.85, (
            f"Expected confidence >= 0.85 for existing file task, got {result.confidence} "
            f"(rule={result.rule_name}, artifact={result.matched_artifact})"
        )
        assert status == ValidationStatus.PRE_COMPLETED

    def test_validator_skips_genuinely_new_task(self) -> None:
        """A task for a file/feature NOT in the codebase gets confidence < 0.65
        (the pending threshold), proving genuinely new tasks are not flagged.
        """
        summary = _make_summary(
            ("loom/__main__.py", ["main"], []),
            ("loom/cli.py", ["cli"], ["CLI"]),
        )

        # Task about something completely absent from the codebase
        task_dict = {
            "title": "Write Kubernetes deployment manifests",
            "context": {"description": "Create k8s YAML for production deployment"},
        }

        result = match_task_to_codebase(task_dict, summary)
        status = classify_match(result)

        assert result.confidence < 0.65, (
            f"Expected confidence < 0.65 for new task, got {result.confidence} "
            f"(rule={result.rule_name}, artifact={result.matched_artifact})"
        )
        assert status == ValidationStatus.PENDING

    @patch("loom.skills.decomposer.run_skill")
    @patch("loom.skills.decomposer.load_skill")
    @pytest.mark.asyncio
    async def test_enriched_decompose_context_includes_codebase(
        self,
        mock_load: MagicMock,
        mock_run: MagicMock,
        tmp_path: Path,
    ) -> None:
        """When decompose() is called with a project_dir pointing to a real
        directory containing Python files, the prompt sent to the LLM includes
        the codebase context sentinel and file information.
        """
        from loom.skills.decomposer import decompose

        # Create a small project in tmp_path
        _write_python_file(
            tmp_path,
            "loom/__main__.py",
            '"""Entry point."""\n\ndef main():\n    print("hello")\n',
        )

        # Capture the context argument passed to run_skill
        captured_inputs: list[dict] = []

        async def _capture_run_skill(skill, inputs, config, pool, project_id):
            captured_inputs.append(dict(inputs))
            return {
                "tasks": [
                    {"title": "New feature X", "type": "task", "done_when": "tests pass"},
                ]
            }

        mock_run.side_effect = _capture_run_skill

        await decompose(
            goal="Add new features",
            project_id="proj-test",
            config=MagicMock(),
            pool=MagicMock(),
            redis=MagicMock(),
            scan_root=tmp_path,
            enrich=False,
            confirm=True,
        )

        # The decompose_project skill should have been called
        assert len(captured_inputs) >= 1, "run_skill was never called"

        # The context passed to the skill must contain the codebase sentinel
        context_value = captured_inputs[0].get("context", "")
        assert CODEBASE_CONTEXT_SENTINEL in context_value, (
            f"Expected codebase context sentinel in prompt context, got: "
            f"{context_value[:200]}..."
        )

        # The context should mention the scanned file
        assert "loom/__main__.py" in context_value or "__main__" in context_value, (
            f"Expected scanned file reference in prompt context, got: "
            f"{context_value[:300]}..."
        )

    def test_end_to_end_redundancy_detection(self, tmp_path: Path) -> None:
        """Full pipeline: scan a real directory, then classify multiple tasks.

        Tasks referencing existing files/symbols should get high confidence
        (pre_completed), while tasks for new work should get low confidence
        (pending).
        """
        # Build a realistic mini-project
        _write_python_file(
            tmp_path,
            "loom/graph/store.py",
            (
                '"""Graph store."""\n\n'
                "async def create_task(pool, task):\n"
                "    pass\n\n"
                "async def get_task(pool, task_id):\n"
                "    pass\n\n"
                "class TaskStore:\n"
                "    pass\n"
            ),
        )
        _write_python_file(
            tmp_path,
            "loom/graph/cache.py",
            (
                '"""Graph cache."""\n\n'
                "async def sync_task(redis, task):\n"
                "    pass\n\n"
                "async def get_cached_task(redis, task_id):\n"
                "    pass\n"
            ),
        )
        _write_python_file(
            tmp_path,
            "loom/bus/publisher.py",
            (
                '"""Event publisher."""\n\n'
                "async def publish_event(redis, project_id, event_type, payload=None):\n"
                "    pass\n"
            ),
        )

        # Scan the project
        summary = scan_codebase(tmp_path)

        # Verify the scan found our files
        scanned_paths = {fs.path for fs in summary.files}
        assert "loom/graph/store.py" in scanned_paths
        assert "loom/graph/cache.py" in scanned_paths
        assert "loom/bus/publisher.py" in scanned_paths

        config = ValidatorConfig(high_threshold=0.85, uncertain_threshold=0.65)

        # --- Redundant tasks: reference existing code ---

        redundant_tasks = [
            {
                "title": "Implement create_task function in loom/graph/store.py",
                "context": {"files": ["loom/graph/store.py"]},
            },
            {
                "title": "Create loom/graph/cache.py with sync_task",
                "context": {"description": "Build the cache layer"},
            },
            {
                "title": "Build publish_event in loom/bus/publisher.py",
                "context": {},
            },
        ]

        for task_dict in redundant_tasks:
            result = match_task_to_codebase(task_dict, summary, config=config)
            status = classify_match(result, config=config)
            assert status in (ValidationStatus.PRE_COMPLETED, ValidationStatus.UNCERTAIN), (
                f"Task '{task_dict['title']}' should be pre_completed or uncertain, "
                f"got {status.value} (confidence={result.confidence}, "
                f"rule={result.rule_name}, artifact={result.matched_artifact})"
            )

        # --- Genuinely new tasks: no match in codebase ---

        new_tasks = [
            {
                "title": "Write Kubernetes deployment manifests",
                "context": {"description": "Create k8s YAML for production"},
            },
            {
                "title": "Design REST API documentation site",
                "context": {"description": "Set up Swagger/OpenAPI docs"},
            },
            {
                "title": "Implement Stripe payment integration",
                "context": {},
            },
        ]

        for task_dict in new_tasks:
            result = match_task_to_codebase(task_dict, summary, config=config)
            status = classify_match(result, config=config)
            assert status == ValidationStatus.PENDING, (
                f"Task '{task_dict['title']}' should be pending (new work), "
                f"got {status.value} (confidence={result.confidence}, "
                f"rule={result.rule_name}, artifact={result.matched_artifact})"
            )
