from __future__ import annotations

import os
import stat
import sys
from collections.abc import Generator
from pathlib import Path

import pygit2
import pytest

from pygittools.validation import check_export_ok, is_valid_pathname, is_valid_project, is_valid_ref_format


class TestValidation:
    @pytest.fixture(scope="class")
    def project_env(self, tmp_path_factory: pytest.TempPathFactory) -> Generator[dict[str, str], None, None]:
        projectroot: Path = tmp_path_factory.mktemp("validation-projects")
        valid_dir: Path = projectroot / "valid"
        invalid_dir: Path = projectroot / "invalid"
        unreachable_dir: Path = projectroot / "unreachable"

        valid_dir.mkdir()
        invalid_dir.mkdir()
        unreachable_dir.mkdir()

        pygit2.init_repository(str(valid_dir), bare=True)
        (valid_dir / "git-daemon-export-ok").write_text("", encoding="utf-8")

        pygit2.init_repository(str(unreachable_dir), bare=True)
        (unreachable_dir / "git-daemon-export-ok").write_text("", encoding="utf-8")
        original_mode: int = stat.S_IMODE(unreachable_dir.stat().st_mode)
        os.chmod(unreachable_dir, 0)

        try:
            yield {
                "projectroot": str(projectroot),
                "valid_name": "valid",
                "invalid_name": "invalid",
                "unreachable_name": "unreachable",
                "valid_dir": str(valid_dir),
                "invalid_dir": str(invalid_dir),
                "unreachable_dir": str(unreachable_dir),
                "export_ok": "git-daemon-export-ok",
            }
        finally:
            os.chmod(unreachable_dir, original_mode)

    class TestPathname:
        def test_is_valid_pathname_success(self) -> None:
            assert is_valid_pathname("group/project.git") is True

        @pytest.mark.parametrize("input_path", [None, "a//b", "a/./b", "a/../b", "a\0b"])
        def test_is_valid_pathname_failure_modes(self, input_path: str | None) -> None:
            assert is_valid_pathname(input_path) is False

    class TestRefFormat:
        def test_is_valid_ref_format_success(self) -> None:
            assert is_valid_ref_format("refs/heads/main") is True

        @pytest.mark.parametrize(
            "input_ref",
            [None, ".bad", "refs/heads/../x", "refs/heads/x.lock", "refs/heads/x ", "refs/heads/x?y"],
        )
        def test_is_valid_ref_format_failure_modes(self, input_ref: str | None) -> None:
            assert is_valid_ref_format(input_ref) is False

    class TestCheckExportOk:
        def test_check_export_ok_success(self, project_env: dict[str, str]) -> None:
            assert check_export_ok(project_env["valid_dir"], project_env["export_ok"]) is True

        def test_check_export_ok_fails_for_missing_directory(self, project_env: dict[str, str]) -> None:
            missing_dir: str = str(Path(project_env["projectroot"]) / "missing")
            assert check_export_ok(missing_dir, project_env["export_ok"]) is False

        def test_check_export_ok_fails_for_non_repo_directory(self, project_env: dict[str, str]) -> None:
            assert check_export_ok(project_env["invalid_dir"], project_env["export_ok"]) is False

        def test_check_export_ok_fails_for_missing_export_file(self, project_env: dict[str, str]) -> None:
            assert check_export_ok(project_env["valid_dir"], "missing-export-ok") is False

        def test_check_export_ok_fails_when_hook_rejects(self, project_env: dict[str, str]) -> None:
            assert (
                check_export_ok(
                    project_env["valid_dir"],
                    project_env["export_ok"],
                    export_auth_hook=lambda _path: False,
                )
                is False
            )

        @pytest.mark.skipif(sys.platform == "win32", reason="chmod-based unreachable directory check is not portable")
        def test_check_export_ok_fails_for_unreachable_repo(self, project_env: dict[str, str]) -> None:
            assert check_export_ok(project_env["unreachable_dir"], project_env["export_ok"]) is False

    class TestValidProject:
        def test_is_valid_project_success(self, project_env: dict[str, str]) -> None:
            assert (
                is_valid_project(
                    project=project_env["valid_name"],
                    projectroot=project_env["projectroot"],
                    export_ok=project_env["export_ok"],
                    strict_export=False,
                    project_in_list=lambda _project: True,
                )
                is True
            )

        def test_is_valid_project_fails_for_none(self, project_env: dict[str, str]) -> None:
            assert (
                is_valid_project(
                    project=None,
                    projectroot=project_env["projectroot"],
                    export_ok=project_env["export_ok"],
                    strict_export=False,
                    project_in_list=lambda _project: True,
                )
                is False
            )

        def test_is_valid_project_fails_for_invalid_pathname(self, project_env: dict[str, str]) -> None:
            assert (
                is_valid_project(
                    project="bad/../name",
                    projectroot=project_env["projectroot"],
                    export_ok=project_env["export_ok"],
                    strict_export=False,
                    project_in_list=lambda _project: True,
                )
                is False
            )

        def test_is_valid_project_fails_for_missing_directory(self, project_env: dict[str, str]) -> None:
            assert (
                is_valid_project(
                    project="missing",
                    projectroot=project_env["projectroot"],
                    export_ok=project_env["export_ok"],
                    strict_export=False,
                    project_in_list=lambda _project: True,
                )
                is False
            )

        def test_is_valid_project_fails_when_export_check_fails(self, project_env: dict[str, str]) -> None:
            assert (
                is_valid_project(
                    project=project_env["invalid_name"],
                    projectroot=project_env["projectroot"],
                    export_ok=project_env["export_ok"],
                    strict_export=False,
                    project_in_list=lambda _project: True,
                )
                is False
            )

        def test_is_valid_project_fails_strict_export_when_not_listed(self, project_env: dict[str, str]) -> None:
            assert (
                is_valid_project(
                    project=project_env["valid_name"],
                    projectroot=project_env["projectroot"],
                    export_ok=project_env["export_ok"],
                    strict_export=True,
                    project_in_list=lambda _project: False,
                )
                is False
            )