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 == []