"""
Tests for the _commit_analysis pre-commit hook shared module.

Covers: classify_file, suggest_test_path, _parse_numstat,
CommitAnalysis dataclass, and analyze_commit (with mocked git).
"""

import sys
from pathlib import Path
from unittest.mock import patch


# Add the pre-commit hooks directory to path
HOOKS_DIR = Path(__file__).parent.parent.parent / ".pre-commit-hooks"
sys.path.insert(0, str(HOOKS_DIR))

from _commit_analysis import (  # noqa: E402
    CommitAnalysis,
    StagedFileInfo,
    _parse_numstat,
    analyze_commit,
    classify_file,
    suggest_test_path,
)


# =========================================================================
# classify_file tests
# =========================================================================


class TestClassifyFile:
    """Tests for classify_file path classification."""

    # --- Source files ---

    def test_source_file_under_src(self):
        assert classify_file("src/local_deep_research/web/api.py") == "source"

    def test_source_file_deep_nesting(self):
        assert (
            classify_file(
                "src/local_deep_research/web_search_engines/engines/search_engine_google.py"
            )
            == "source"
        )

    def test_source_file_direct_under_src(self):
        assert classify_file("src/local_deep_research/main.py") == "source"

    # --- Test files ---

    def test_test_file_under_tests_dir(self):
        assert classify_file("tests/web/test_api.py") == "test"

    def test_test_file_by_prefix(self):
        assert classify_file("src/test_something.py") == "test"

    def test_test_file_by_suffix(self):
        assert classify_file("src/something_test.py") == "test"

    def test_test_file_nested_in_tests(self):
        assert classify_file("tests/security/test_url_validator.py") == "test"

    # --- Doc files ---

    def test_markdown_file(self):
        assert classify_file("README.md") == "doc"

    def test_markdown_file_in_docs(self):
        assert classify_file("docs/setup.md") == "doc"

    def test_markdown_file_nested(self):
        assert classify_file("src/local_deep_research/CHANGELOG.md") == "doc"

    # --- Excluded directories ---

    def test_migrations_dir(self):
        assert (
            classify_file("src/local_deep_research/migrations/001.py")
            == "migration"
        )

    def test_alembic_dir(self):
        assert classify_file("alembic/versions/abc123.py") == "migration"

    def test_config_dir(self):
        assert (
            classify_file("src/local_deep_research/config/settings.py")
            == "config"
        )

    def test_settings_dir(self):
        assert classify_file("settings/defaults.py") == "config"

    def test_scripts_dir(self):
        assert classify_file("scripts/dev/restart_server.py") == "script"

    def test_pre_commit_hooks_dir(self):
        assert classify_file(".pre-commit-hooks/check-env-vars.py") == "hook"

    def test_examples_dir(self):
        assert classify_file("examples/basic_usage.py") == "other"

    def test_docs_dir_python_file(self):
        assert classify_file("docs/conf.py") == "doc"

    # --- Special basenames ---

    def test_init_py(self):
        assert classify_file("src/local_deep_research/__init__.py") == "init"

    def test_conftest_py(self):
        assert classify_file("tests/conftest.py") == "conftest"

    # --- Non-Python files ---

    def test_json_file(self):
        assert (
            classify_file(
                "src/local_deep_research/defaults/default_settings.json"
            )
            == "other"
        )

    def test_yaml_file(self):
        assert classify_file(".pre-commit-config.yaml") == "other"

    def test_html_file(self):
        assert (
            classify_file("src/local_deep_research/web/templates/index.html")
            == "other"
        )

    def test_javascript_file(self):
        assert (
            classify_file("src/local_deep_research/web/static/app.js")
            == "other"
        )

    # --- Edge cases ---

    def test_top_level_python_file(self):
        assert classify_file("setup.py") == "other"

    def test_empty_path_components(self):
        """Files not under src/ and not matching other patterns are 'other'."""
        assert classify_file("lib/helper.py") == "other"

    def test_case_insensitive_excluded_dirs(self):
        """Excluded dirs match case-insensitively."""
        assert (
            classify_file("src/local_deep_research/Config/app.py") == "config"
        )

    def test_test_prefix_not_in_tests_dir(self):
        """test_*.py files are classified as test even outside tests/."""
        assert classify_file("src/test_helpers.py") == "test"


# =========================================================================
# suggest_test_path tests
# =========================================================================


class TestSuggestTestPath:
    """Tests for suggest_test_path path generation."""

    def test_standard_source_file(self):
        result = suggest_test_path("src/local_deep_research/web/api.py")
        assert result == "tests/web/test_api.py"

    def test_deeply_nested_source(self):
        result = suggest_test_path(
            "src/local_deep_research/web_search_engines/engines/search_engine_google.py"
        )
        assert (
            result
            == "tests/web_search_engines/engines/test_search_engine_google.py"
        )

    def test_direct_under_package(self):
        result = suggest_test_path("src/local_deep_research/main.py")
        assert result == "tests/test_main.py"

    def test_strips_src_prefix(self):
        """Should strip src/ prefix from path."""
        result = suggest_test_path("src/local_deep_research/utils.py")
        assert result.startswith("tests/")
        assert "src" not in result

    def test_adds_test_prefix_to_filename(self):
        """Should add test_ prefix to the filename."""
        result = suggest_test_path("src/local_deep_research/database/models.py")
        assert result.endswith("test_models.py")


# =========================================================================
# _parse_numstat tests
# =========================================================================


class TestParseNumstat:
    """Tests for _parse_numstat git output parsing."""

    def test_normal_output(self):
        lines = [
            "10\t5\tsrc/local_deep_research/web/api.py",
            "20\t3\ttests/web/test_api.py",
        ]
        result = _parse_numstat(lines)
        assert result == {
            "src/local_deep_research/web/api.py": (10, 5),
            "tests/web/test_api.py": (20, 3),
        }

    def test_binary_file(self):
        """Binary files show '-' for added/removed counts."""
        lines = ["-\t-\timages/logo.png"]
        result = _parse_numstat(lines)
        assert result == {"images/logo.png": (0, 0)}

    def test_empty_input(self):
        assert _parse_numstat([]) == {}

    def test_malformed_line_skipped(self):
        """Lines with wrong number of fields are skipped."""
        lines = [
            "10\t5\tsrc/api.py",
            "bad line",
            "20\t3\ttests/test_api.py",
        ]
        result = _parse_numstat(lines)
        assert len(result) == 2
        assert "src/api.py" in result
        assert "tests/test_api.py" in result

    def test_zero_counts(self):
        lines = ["0\t0\tempty_file.py"]
        result = _parse_numstat(lines)
        assert result == {"empty_file.py": (0, 0)}

    def test_large_counts(self):
        lines = ["9999\t8888\tbig_file.py"]
        result = _parse_numstat(lines)
        assert result == {"big_file.py": (9999, 8888)}

    def test_filepath_with_spaces(self):
        """Tabs separate fields, so filenames with spaces work."""
        lines = ["5\t2\tpath with spaces/file.py"]
        result = _parse_numstat(lines)
        assert "path with spaces/file.py" in result


# =========================================================================
# CommitAnalysis dataclass tests
# =========================================================================


class TestCommitAnalysis:
    """Tests for CommitAnalysis computed properties."""

    def _make_file(
        self,
        path="src/a.py",
        added=10,
        removed=0,
        is_new=False,
        category="source",
    ):
        return StagedFileInfo(
            path=path,
            added=added,
            removed=removed,
            is_new=is_new,
            category=category,
        )

    def test_empty_analysis(self):
        analysis = CommitAnalysis()
        assert analysis.total_source_added == 0
        assert analysis.has_tests is False
        assert analysis.has_docs is False
        assert analysis.new_source_files == []

    def test_total_source_added(self):
        analysis = CommitAnalysis(
            source_files=[
                self._make_file(added=10),
                self._make_file(added=20),
            ]
        )
        assert analysis.total_source_added == 30

    def test_has_tests_true(self):
        analysis = CommitAnalysis(test_files=[self._make_file(category="test")])
        assert analysis.has_tests is True

    def test_has_tests_false(self):
        analysis = CommitAnalysis(source_files=[self._make_file()])
        assert analysis.has_tests is False

    def test_has_docs_true(self):
        analysis = CommitAnalysis(doc_files=[self._make_file(category="doc")])
        assert analysis.has_docs is True

    def test_new_source_files(self):
        analysis = CommitAnalysis(
            source_files=[
                self._make_file(path="a.py", is_new=True),
                self._make_file(path="b.py", is_new=False),
                self._make_file(path="c.py", is_new=True),
            ]
        )
        new = analysis.new_source_files
        assert len(new) == 2
        assert all(f.is_new for f in new)


# =========================================================================
# analyze_commit integration tests (with mocked git)
# =========================================================================


class TestAnalyzeCommit:
    """Tests for analyze_commit with mocked git commands."""

    def _mock_run_git(self, name_status_lines, numstat_lines):
        """Create a mock for _run_git returning different output per command."""

        def side_effect(args):
            if "--name-status" in args:
                return name_status_lines
            if "--numstat" in args:
                return numstat_lines
            return []

        return side_effect

    def test_basic_source_and_test_commit(self):
        mock = self._mock_run_git(
            name_status_lines=[
                "M\tsrc/local_deep_research/web/api.py",
                "A\ttests/web/test_api.py",
            ],
            numstat_lines=[
                "15\t3\tsrc/local_deep_research/web/api.py",
                "50\t0\ttests/web/test_api.py",
            ],
        )

        with patch("_commit_analysis._run_git", side_effect=mock):
            analysis = analyze_commit()

        assert len(analysis.source_files) == 1
        assert len(analysis.test_files) == 1
        assert (
            analysis.source_files[0].path
            == "src/local_deep_research/web/api.py"
        )
        assert analysis.source_files[0].is_new is False
        assert analysis.test_files[0].is_new is True
        assert analysis.has_tests is True

    def test_new_file_detection(self):
        mock = self._mock_run_git(
            name_status_lines=["A\tsrc/local_deep_research/new_module.py"],
            numstat_lines=["100\t0\tsrc/local_deep_research/new_module.py"],
        )

        with patch("_commit_analysis._run_git", side_effect=mock):
            analysis = analyze_commit()

        assert len(analysis.source_files) == 1
        assert analysis.source_files[0].is_new is True
        assert analysis.source_files[0].added == 100

    def test_doc_files_classified(self):
        mock = self._mock_run_git(
            name_status_lines=["A\tREADME.md"],
            numstat_lines=["20\t0\tREADME.md"],
        )

        with patch("_commit_analysis._run_git", side_effect=mock):
            analysis = analyze_commit()

        assert len(analysis.doc_files) == 1
        assert analysis.has_docs is True

    def test_excluded_files_not_in_any_list(self):
        """Config, migration, and other excluded files don't appear in analysis lists."""
        mock = self._mock_run_git(
            name_status_lines=[
                "M\tsrc/local_deep_research/config/settings.py",
                "M\t.pre-commit-config.yaml",
            ],
            numstat_lines=[
                "5\t2\tsrc/local_deep_research/config/settings.py",
                "3\t1\t.pre-commit-config.yaml",
            ],
        )

        with patch("_commit_analysis._run_git", side_effect=mock):
            analysis = analyze_commit()

        assert len(analysis.source_files) == 0
        assert len(analysis.test_files) == 0
        assert len(analysis.doc_files) == 0

    def test_empty_commit(self):
        mock = self._mock_run_git(
            name_status_lines=[],
            numstat_lines=[],
        )

        with patch("_commit_analysis._run_git", side_effect=mock):
            analysis = analyze_commit()

        assert len(analysis.source_files) == 0
        assert analysis.total_source_added == 0
        assert analysis.has_tests is False
        assert analysis.has_docs is False

    def test_mixed_commit_with_all_types(self):
        mock = self._mock_run_git(
            name_status_lines=[
                "A\tsrc/local_deep_research/feature.py",
                "A\ttests/test_feature.py",
                "M\tREADME.md",
                "M\tscripts/dev/helper.py",
            ],
            numstat_lines=[
                "80\t0\tsrc/local_deep_research/feature.py",
                "40\t0\ttests/test_feature.py",
                "10\t5\tREADME.md",
                "3\t1\tscripts/dev/helper.py",
            ],
        )

        with patch("_commit_analysis._run_git", side_effect=mock):
            analysis = analyze_commit()

        assert len(analysis.source_files) == 1
        assert len(analysis.test_files) == 1
        assert len(analysis.doc_files) == 1
        assert analysis.total_source_added == 80
        assert analysis.has_tests is True
        assert analysis.has_docs is True
        assert analysis.source_files[0].is_new is True
