from __future__ import annotations

from collections.abc import Generator
from pathlib import Path
from unittest.mock import patch

import pygit2
import pytest

import pygitweb.projects as projects
from pygitweb.config import settings


def _create_initial_commit(repo: pygit2.Repository, repo_dir: Path, message: str) -> None:
	(repo_dir / "README.md").write_text(f"{message}\n", encoding="utf-8")
	index = repo.index
	index.add("README.md")
	index.write()
	tree = index.write_tree()
	sig = pygit2.Signature("tester", "tester@example.com")
	repo.create_commit("HEAD", sig, sig, message, tree, [])


class TestProjects:
	@pytest.fixture(scope="class")
	def project_env(self, tmp_path_factory: pytest.TempPathFactory) -> Generator[dict[str, str], None, None]:
		root: Path = tmp_path_factory.mktemp("projects")

		alpha_dir: Path = root / "alpha"
		alpha_dir.mkdir()
		alpha_repo = pygit2.init_repository(str(alpha_dir), bare=False)
		_create_initial_commit(alpha_repo, alpha_dir, "alpha init")
		(alpha_dir / "git-daemon-export-ok").write_text("", encoding="utf-8")

		nested_dir: Path = root / "group"
		nested_dir.mkdir()
		beta_dir: Path = nested_dir / "beta"
		beta_dir.mkdir()
		beta_repo = pygit2.init_repository(str(beta_dir), bare=False)
		_create_initial_commit(beta_repo, beta_dir, "beta init")
		(beta_dir / "git-daemon-export-ok").write_text("", encoding="utf-8")

		private_dir: Path = root / "private"
		private_dir.mkdir()
		private_repo = pygit2.init_repository(str(private_dir), bare=False)
		_create_initial_commit(private_repo, private_dir, "private init")

		(root / "not-a-repo").mkdir()

		projects_list_file: Path = root / "projects.list"
		projects_list_file.write_text(
			"\n".join([
				"alpha Alice",
				"group/beta Bob%20Owner",
				"private",
				"name%20with%20space Owner%20Space",
			])
			+ "\n",
			encoding="utf-8",
		)

		try:
			yield {
				"root": str(root),
				"alpha": "alpha",
				"beta": "group/beta",
				"private": "private",
				"projects_list_file": str(projects_list_file),
			}
		finally:
			projects._gitweb_project_owner = None

	class TestProjectInList:
		def test_project_in_list_true(self) -> None:
			def getter() -> list[dict[str, str]]:
				return [{"path": "alpha"}, {"path": "beta"}]

			assert projects.project_in_list("alpha", getter) is True

		def test_project_in_list_false(self) -> None:
			def getter() -> list[dict[str, str]]:
				return [{"path": "alpha"}]

			assert projects.project_in_list("missing", getter) is False

	class TestProjectDiscovery:
		def test_git_get_projects_list_directory_mode(self, project_env: dict[str, str]) -> None:
			with (
				patch.object(settings, "PROJECTROOT", project_env["root"]),
				patch.object(settings, "PROJECTS_LIST", project_env["root"]),
				patch.object(settings, "PROJECT_MAXDEPTH", 3),
				patch.object(settings, "LIST_ALL", True),
			):
				result = projects.git_get_projects_list(export_ok="git-daemon-export-ok")
			paths = {entry["path"] for entry in result}
			assert {"alpha", "group/beta", "private"} <= paths

		def test_git_get_projects_list_respects_export_ok_when_list_all_false(
			self,
			project_env: dict[str, str],
		) -> None:
			with (
				patch.object(settings, "PROJECTROOT", project_env["root"]),
				patch.object(settings, "PROJECTS_LIST", project_env["root"]),
				patch.object(settings, "PROJECT_MAXDEPTH", 3),
				patch.object(settings, "LIST_ALL", False),
			):
				result = projects.git_get_projects_list(export_ok="git-daemon-export-ok")
			paths = {entry["path"] for entry in result}
			assert "alpha" in paths
			assert "group/beta" in paths
			assert "private" not in paths

		def test_git_get_projects_list_filter_path(self, project_env: dict[str, str]) -> None:
			with (
				patch.object(settings, "PROJECTROOT", project_env["root"]),
				patch.object(settings, "PROJECTS_LIST", project_env["root"]),
				patch.object(settings, "PROJECT_MAXDEPTH", 3),
				patch.object(settings, "LIST_ALL", True),
			):
				result = projects.git_get_projects_list(filter_path="group")
			assert result == [{"path": "group/beta"}]

		def test_git_get_projects_list_file_mode(self, project_env: dict[str, str]) -> None:
			with (
				patch.object(settings, "PROJECTROOT", project_env["root"]),
				patch.object(settings, "PROJECTS_LIST", project_env["projects_list_file"]),
				patch.object(settings, "LIST_ALL", True),
			):
				result = projects.git_get_projects_list()
			assert result == [
				{"path": "alpha", "owner": "Alice"},
				{"path": "group/beta", "owner": "Bob Owner"},
				{"path": "private"},
				{"path": "name with space", "owner": "Owner Space"},
			]

	class TestProjectOwnerAndActivity:
		def test_git_get_project_list_from_file_and_cache(self, project_env: dict[str, str]) -> None:
			projects._gitweb_project_owner = None
			with patch.object(settings, "PROJECTS_LIST", project_env["projects_list_file"]):
				owners_first = projects.git_get_project_list_from_file()
				owners_second = projects.git_get_project_list_from_file()
			assert owners_first is owners_second
			assert owners_first["alpha"] == "Alice"
			assert owners_first["group/beta"] == "Bob Owner"

		def test_git_get_project_owner_from_file(self, project_env: dict[str, str]) -> None:
			projects._gitweb_project_owner = None
			with (
				patch.object(settings, "PROJECTROOT", project_env["root"]),
				patch.object(settings, "PROJECTS_LIST", project_env["projects_list_file"]),
			):
				owner = projects.git_get_project_owner("alpha")
			assert owner == "Alice"

		def test_git_get_project_owner_from_config_fallback(self, project_env: dict[str, str]) -> None:
			projects._gitweb_project_owner = None
			with (
				patch.object(settings, "PROJECTROOT", project_env["root"]),
				patch.object(settings, "PROJECTS_LIST", project_env["projects_list_file"]),
			):
				owner = projects.git_get_project_owner("private", get_project_config=lambda _p, _k: "Config Owner")
			assert owner == "Config Owner"

		def test_git_get_last_activity_for_repo_and_missing_project(self, project_env: dict[str, str]) -> None:
			with patch.object(settings, "PROJECTROOT", project_env["root"]):
				last_activity = projects.git_get_last_activity(project_env["alpha"])
				missing_activity = projects.git_get_last_activity("does-not-exist")
			assert isinstance(last_activity, int)
			assert last_activity > 0
			assert missing_activity is None

	class TestSearchProjects:
		def test_search_projects_list_by_tag_and_regex(self) -> None:
			projlist: list[dict[str, object]] = [
				{"path": "alpha", "descr_long": "backend api", "ctags": {"infra": 1}},
				{"path": "beta", "descr_long": "frontend web", "ctags": {"ui": 1}},
			]
			result = projects.search_projects_list(projlist, tagfilter="infra", search_regexp="backend")
			assert result == [projlist[0]]

		def test_search_projects_list_invalid_regex_returns_empty(self) -> None:
			projlist: list[dict[str, object]] = [{"path": "alpha", "descr_long": "desc"}]
			result = projects.search_projects_list(projlist, search_regexp="(")
			assert result == []