"""Tests for analysis orchestrator.

Tests analyze_repository from analysis/orchestrator.py.
Uses mocking to isolate pipeline stages and avoid subprocess calls.
"""

from __future__ import annotations

from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch

from tests.conftest import make_file_info
from unfamiliar.analysis.models import FileTestInfo, ImportGraph
from unfamiliar.analysis.orchestrator import analyze_repository

if TYPE_CHECKING:
    from pathlib import Path


class TestAnalyzeRepository:
    """Tests for analyze_repository function."""

    def test_empty_repo_returns_minimal_result(self, tmp_path: Path):
        """Empty repository returns AnalysisResult with repo_path only."""
        with patch(
            "unfamiliar.analysis.orchestrator.discover_files",
            return_value=([], 0),
        ):
            result = analyze_repository(tmp_path)

        assert result.repo_path == str(tmp_path)
        assert result.files == []
        assert result.parsed_files == {}
        assert result.entry_points == []
        assert result.languages == {}
        assert result.excluded_count == 0

    def test_populates_all_result_fields(self, tmp_path: Path):
        """Happy path populates all AnalysisResult fields."""
        files = [
            make_file_info("src/main.py"),
            make_file_info("src/utils.py"),
        ]

        with (
            patch(
                "unfamiliar.analysis.orchestrator.discover_files",
                return_value=(files, 0),
            ),
            patch(
                "unfamiliar.analysis.orchestrator.scan_for_secrets",
                return_value=[],
            ),
            patch(
                "unfamiliar.analysis.orchestrator.scan_vulnerabilities",
                return_value=([], [], []),
            ),
        ):
            # Need real files for the adapter to read
            (tmp_path / "src").mkdir()
            (tmp_path / "src" / "main.py").write_text("def main(): pass")
            (tmp_path / "src" / "utils.py").write_text("def helper(): pass")

            result = analyze_repository(tmp_path)

        # Verify all top-level fields are populated
        assert result.repo_path == str(tmp_path)
        assert len(result.files) == 2
        assert len(result.parsed_files) == 2
        assert result.import_graph is not None
        assert isinstance(result.import_graph, ImportGraph)
        assert result.modules is not None  # list or empty list
        assert result.test_info is not None
        assert isinstance(result.test_info, FileTestInfo)
        assert result.scored_files is not None
        assert result.entry_points is not None
        assert result.languages == {"python": 2}
        assert result.security is not None

    def test_languages_distribution(self, tmp_path: Path):
        """Language distribution reflects file languages."""
        files = [
            make_file_info("src/main.py", language="python"),
            make_file_info("src/utils.py", language="python"),
            make_file_info("src/app.ts", language="typescript"),
        ]

        with (
            patch(
                "unfamiliar.analysis.orchestrator.discover_files",
                return_value=(files, 0),
            ),
            patch(
                "unfamiliar.analysis.orchestrator.scan_for_secrets",
                return_value=[],
            ),
            patch(
                "unfamiliar.analysis.orchestrator.scan_vulnerabilities",
                return_value=([], [], []),
            ),
        ):
            (tmp_path / "src").mkdir()
            (tmp_path / "src" / "main.py").write_text("def main(): pass")
            (tmp_path / "src" / "utils.py").write_text("def helper(): pass")
            (tmp_path / "src" / "app.ts").write_text("const x = 1;")

            result = analyze_repository(tmp_path)

        assert result.languages == {"python": 2, "typescript": 1}

    def test_security_result_populated(self, tmp_path: Path):
        """Security scanning results are included."""
        files = [make_file_info("src/main.py")]

        mock_secrets = [MagicMock()]
        mock_vulns = [MagicMock()]

        with (
            patch(
                "unfamiliar.analysis.orchestrator.discover_files",
                return_value=(files, 0),
            ),
            patch(
                "unfamiliar.analysis.orchestrator.scan_for_secrets",
                return_value=mock_secrets,
            ),
            patch(
                "unfamiliar.analysis.orchestrator.scan_vulnerabilities",
                return_value=(mock_vulns, ["requirements.txt"], []),
            ),
        ):
            (tmp_path / "src").mkdir()
            (tmp_path / "src" / "main.py").write_text("def main(): pass")

            result = analyze_repository(tmp_path)

        assert result.security.secrets == mock_secrets
        assert result.security.vulnerabilities == mock_vulns
        assert result.security.manifests_scanned == ["requirements.txt"]
        assert result.security.scan_errors == []

    def test_security_scan_errors_recorded(self, tmp_path: Path):
        """Security scan errors are recorded but don't fail analysis."""
        files = [make_file_info("src/main.py")]

        with (
            patch(
                "unfamiliar.analysis.orchestrator.discover_files",
                return_value=(files, 0),
            ),
            patch(
                "unfamiliar.analysis.orchestrator.scan_for_secrets",
                return_value=[],
            ),
            patch(
                "unfamiliar.analysis.orchestrator.scan_vulnerabilities",
                return_value=([], [], ["pip-audit failed"]),
            ),
        ):
            (tmp_path / "src").mkdir()
            (tmp_path / "src" / "main.py").write_text("def main(): pass")

            result = analyze_repository(tmp_path)

        assert result.security.scan_errors == ["pip-audit failed"]
        # Analysis still succeeds
        assert len(result.files) == 1

    def test_skips_files_without_adapters(self, tmp_path: Path):
        """Files with unknown language are not parsed."""
        files = [
            make_file_info(
                "src/main.py",
            ),
            make_file_info(
                "data.csv",
            ),
        ]

        with (
            patch(
                "unfamiliar.analysis.orchestrator.discover_files",
                return_value=(files, 0),
            ),
            patch(
                "unfamiliar.analysis.orchestrator.scan_for_secrets",
                return_value=[],
            ),
            patch(
                "unfamiliar.analysis.orchestrator.scan_vulnerabilities",
                return_value=([], [], []),
            ),
        ):
            (tmp_path / "src").mkdir()
            (tmp_path / "src" / "main.py").write_text("def main(): pass")
            (tmp_path / "data.csv").write_text("a,b,c")

            result = analyze_repository(tmp_path)

        # Only python file is parsed
        assert "src/main.py" in result.parsed_files
        assert "data.csv" not in result.parsed_files
        # But both are in files list
        assert len(result.files) == 2

    def test_unreadable_files_are_skipped(self, tmp_path: Path):
        """Files that can't be read are skipped gracefully."""
        files = [make_file_info("src/main.py")]

        with (
            patch(
                "unfamiliar.analysis.orchestrator.discover_files",
                return_value=(files, 0),
            ),
            patch(
                "unfamiliar.analysis.orchestrator.scan_for_secrets",
                return_value=[],
            ),
            patch(
                "unfamiliar.analysis.orchestrator.scan_vulnerabilities",
                return_value=([], [], []),
            ),
        ):
            # Don't create the file - simulate unreadable
            (tmp_path / "src").mkdir()
            # main.py doesn't exist, so read will fail

            result = analyze_repository(tmp_path)

        # File is in discovered list but not parsed
        assert len(result.files) == 1
        assert len(result.parsed_files) == 0


class TestPipelineStageOrder:
    """Tests verifying pipeline stages are called in order."""

    def test_stages_called_in_correct_order(self, tmp_path: Path):
        """All pipeline stages are invoked in expected order."""
        files = [make_file_info("src/main.py")]

        call_order = []

        def track_discover(*args, **kwargs):
            call_order.append("discover")
            return (files, 0)

        def track_secrets(*args, **kwargs):
            call_order.append("secrets")
            return []

        def track_vulns(*args, **kwargs):
            call_order.append("vulnerabilities")
            return ([], [], [])

        with (
            patch(
                "unfamiliar.analysis.orchestrator.discover_files",
                side_effect=track_discover,
            ),
            patch(
                "unfamiliar.analysis.orchestrator.scan_for_secrets",
                side_effect=track_secrets,
            ),
            patch(
                "unfamiliar.analysis.orchestrator.scan_vulnerabilities",
                side_effect=track_vulns,
            ),
        ):
            (tmp_path / "src").mkdir()
            (tmp_path / "src" / "main.py").write_text("def main(): pass")

            analyze_repository(tmp_path)

        # Verify order: discover before secrets before vulnerabilities
        assert call_order == ["discover", "secrets", "vulnerabilities"]
