from __future__ import annotations

from tests.conftest import make_file_info
from unfamiliar.analysis.graph.clustering import (
    _compute_primary_path,
    _infer_category_hints,
    _merge_small_clusters,
    detect_modules,
)
from unfamiliar.analysis.graph.import_graph import ImportGraph
from unfamiliar.constants import InvestigationCategory


class TestComputePrimaryPath:
    """Tests for _compute_primary_path private helper."""

    def test_empty_list_returns_empty_string(self):
        """Empty list returns empty string."""
        assert _compute_primary_path([]) == ""

    def test_single_file_returns_that_file(self):
        """Single file is its own primary path."""
        assert _compute_primary_path(["src/utils.py"]) == "src/utils.py"

    def test_same_directory_returns_directory(self):
        """Files in same dir returns the directory path"""
        files = ["src/utils.py", "src/helpers.py", "src/main.py"]
        assert _compute_primary_path(files) == "src"

    def test_different_top_level_dirs_returns_first_file(self):
        """Files in different top-level dirs returns first file."""
        files = ["src/utils.py", "tests/utils.py", "src/main.py"]
        assert _compute_primary_path(files) == "src/utils.py"


class TestInferCategoryHints:
    """Tests for _infer_category_hints private helper."""

    def test_file_returns_testing_category(self):
        """A file with 'test_' in the path should return TESTING"""
        files = ["tests/test_utils.py"]
        result = _infer_category_hints(files)
        assert InvestigationCategory.TESTING in result

    def test_multiple_patterns_match_returns_sorted_list(self):
        """Files matching multiple patterns return all matching categories sorted"""
        files = ["tests/test_auth_service.py"]
        result = _infer_category_hints(files)
        assert result == [
            InvestigationCategory.DOMAIN_BUSINESS_LOGIC,
            InvestigationCategory.TESTING,
        ]

    def test_no_patterns_match_returns_empty_list(self):
        """Files not matching any patterns return empty list"""
        files = ["example.py"]
        result = _infer_category_hints(files)
        assert result == []


class TestMergeSmallClusters:
    """Tests for _merge_small_clusters private helper."""

    def test_small_cluster_merges_into_large_neighbor(self):
        """A small cluster merges into its connected large cluster"""
        # Large cluster with 5 files, small cluster with 1
        clusters = {
            "src/": {"src/a.py", "src/b.py", "src/c.py", "src/d.py", "src/e.py"},
            "utils.py": {"utils.py"},
        }

        # The small file is connected to files in the large cluster
        neighbors = {
            "utils.py": {"src/a.py", "src/b.py"},
            "src/a.py": {"utils.py"},
            "src/b.py": set(),
            "src/c.py": set(),
            "src/d.py": set(),
            "src/e.py": set(),
        }

        result = _merge_small_clusters(clusters, neighbors, min_size=3)

        assert "utils.py" not in result
        assert "utils.py" in result["src/"]
        assert len(result["src/"]) == 6  # 5 original, + 1 merged

    def test_small_cluster_not_merged_if_no_neighbors(self):
        """A small cluster is not merged if it has no neighbors"""
        clusters = {
            "src/": {"src/a.py", "src/b.py", "src/c.py"},
            "orphan.py": {"orphan.py"},
        }

        neighbors = {
            "orphan.py": set(),
            "src/a.py": {"src/b.py"},
            "src/b.py": {"src/a.py"},
            "src/c.py": set(),
        }

        result = _merge_small_clusters(clusters, neighbors, min_size=3)
        assert "orphan.py" in result
        assert len(result) == 2

    def test_multiple_small_clusters_all_merge(self):
        """Multiple small clusters merge into the same large cluster"""
        clusters = {
            "src/": {"src/a.py", "src/b.py", "src/c.py"},
            "utils.py": {"utils.py"},
            "helpers.py": {"helpers.py"},
        }

        neighbors = {
            "utils.py": {"src/a.py"},
            "helpers.py": {"src/c.py"},
            "src/a.py": {"utils.py", "helpers.py"},
            "src/b.py": set(),
            "src/c.py": {"helpers.py"},
        }

        result = _merge_small_clusters(clusters, neighbors, min_size=3)

        assert "utils.py" not in result
        assert "helpers.py" not in result
        assert len(result) == 1
        assert len(result["src/"]) == 5


class TestDetectModules:
    """Tests for detect_modules function."""

    def test_empty_graph_returns_empty_list(self):
        """An empty ImportGraph returns no modules."""
        graph = ImportGraph(files={}, edges={}, in_degree={})
        result = detect_modules(graph)
        assert result == []

    def test_connected_graph_returns_modules(self):
        """A graph with connected files returns at least one DetectedModule."""
        file_a = make_file_info("src/a.py")
        file_b = make_file_info("src/b.py")
        file_c = make_file_info("src/c.py")

        graph = ImportGraph(
            files={
                "src/a.py": file_a,
                "src/b.py": file_b,
                "src/c.py": file_c,
            },
            edges={
                "src/a.py": {"src/b.py"},
                "src/b.py": {"src/c.py"},
            },
            in_degree={"src/a.py": 0, "src/b.py": 1, "src/c.py": 1},
        )

        result = detect_modules(graph)

        assert len(result) == 1
        assert all(m.file_count > 0 for m in result)

    def test_edge_to_external_file_ignored(self):
        """Edges pointing to files not in graph.files are safely ignored."""
        file_a = make_file_info("src/a.py")

        graph = ImportGraph(
            files={"src/a.py": file_a},
            edges={"src/a.py": {"external/requests/__init__.py"}},
            in_degree={"src/a.py": 0},
        )

        result = detect_modules(graph)

        assert len(result) == 1
        assert result[0].files == ["src/a.py"]
