from __future__ import annotations

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

import pygit2
import pytest

from pygitweb.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
			)

		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
			)