"""Tests for collection.py — 统一 collection 推断（Issue #136）"""

from __future__ import annotations

import subprocess
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

from index1.collection import (
    PROJECT_MARKERS,
    _cached_remote,
    _extract_repo_name,
    _find_project_root_name,
    _remote_cache,
    _resolve_effective_cwd,
    clear_cache,
    detect_collection,
    is_meaningful_name,
)


# ── _extract_repo_name: 6 种 Git URL 格式 ──


class TestExtractRepoName:
    def test_https_with_git_suffix(self):
        assert _extract_repo_name("https://github.com/user/repo.git") == "repo"

    def test_https_without_git_suffix(self):
        assert _extract_repo_name("https://github.com/user/repo") == "repo"

    def test_ssh_style(self):
        assert _extract_repo_name("git@github.com:user/repo.git") == "repo"

    def test_gitlab_subgroup(self):
        assert _extract_repo_name("git@github.com:org/sub-group/repo.git") == "repo"

    def test_ssh_protocol(self):
        assert _extract_repo_name("ssh://git@gitlab.com/group/repo") == "repo"

    def test_file_protocol(self):
        assert _extract_repo_name("file:///local/path/repo.git") == "repo"

    def test_trailing_slash(self):
        assert _extract_repo_name("https://github.com/user/repo.git/") == "repo"

    def test_empty_string(self):
        assert _extract_repo_name("") is None

    def test_none(self):
        assert _extract_repo_name(None) is None

    def test_just_domain(self):
        # 边界情况：无路径段
        assert _extract_repo_name("https://github.com") is None or \
            _extract_repo_name("https://github.com") == "github.com"


# ── is_meaningful_name ──


class TestIsMeaningfulName:
    @pytest.mark.parametrize("name", [
        "my-project", "index1", "cool-app", "blockchain_app",
    ])
    def test_meaningful(self, name):
        assert is_meaningful_name(name) is True

    @pytest.mark.parametrize("name", [
        "tmp", "temp", "home", "root",
        "Desktop", "Documents", "Downloads",
        "src", "app", "lib", "pkg", "code",
        "project", "projects", "repo", "repos",
        "workspace", "workspaces",
        "usr", "opt", "var",
    ])
    def test_meaningless(self, name):
        assert is_meaningful_name(name) is False

    def test_empty(self):
        assert is_meaningful_name("") is False

    def test_single_char(self):
        assert is_meaningful_name("a") is False

    def test_pure_digits(self):
        assert is_meaningful_name("12345") is False

    def test_dot(self):
        assert is_meaningful_name(".") is False

    def test_case_insensitive(self):
        assert is_meaningful_name("SRC") is False
        assert is_meaningful_name("Tmp") is False


# ── _resolve_effective_cwd ──


class TestResolveEffectiveCwd:
    def test_cwd_priority(self):
        result = _resolve_effective_cwd("/some/path", None)
        assert result == Path("/some/path")

    def test_env_takes_priority(self, monkeypatch):
        monkeypatch.setenv("CLAUDE_PROJECT_DIR", "/env/path")
        result = _resolve_effective_cwd("/cwd/path", None)
        assert result == Path("/env/path")

    def test_paths_fallback(self):
        result = _resolve_effective_cwd(None, ["/idx/path"])
        assert result == Path("/idx/path")

    def test_none_when_all_empty(self, monkeypatch):
        monkeypatch.delenv("CLAUDE_PROJECT_DIR", raising=False)
        result = _resolve_effective_cwd(None, None)
        assert result is None

    def test_file_path_resolves_to_parent(self, tmp_path):
        f = tmp_path / "test.py"
        f.touch()
        result = _resolve_effective_cwd(str(f), None)
        assert result == tmp_path


# ── _cached_remote ──


class TestCachedRemote:
    def setup_method(self):
        clear_cache()

    def test_caches_success(self, monkeypatch):
        call_count = 0

        def mock_run(*args, **kwargs):
            nonlocal call_count
            call_count += 1
            result = MagicMock()
            result.returncode = 0
            result.stdout = "https://github.com/user/my-repo.git\n"
            return result

        monkeypatch.setattr(subprocess, "run", mock_run)
        assert _cached_remote("/test/path") == "my-repo"
        assert _cached_remote("/test/path") == "my-repo"
        assert call_count == 1  # subprocess 只调一次

    def test_caches_failure(self, monkeypatch):
        call_count = 0

        def mock_run(*args, **kwargs):
            nonlocal call_count
            call_count += 1
            result = MagicMock()
            result.returncode = 128
            result.stdout = ""
            return result

        monkeypatch.setattr(subprocess, "run", mock_run)
        assert _cached_remote("/no/git") is None
        assert _cached_remote("/no/git") is None
        assert call_count == 1  # 失败也只调一次

    def test_clear_cache(self, monkeypatch):
        call_count = 0

        def mock_run(*args, **kwargs):
            nonlocal call_count
            call_count += 1
            result = MagicMock()
            result.returncode = 0
            result.stdout = "https://github.com/user/repo.git\n"
            return result

        monkeypatch.setattr(subprocess, "run", mock_run)
        _cached_remote("/test")
        clear_cache()
        _cached_remote("/test")
        assert call_count == 2  # clear 后重新查


# ── _find_project_root_name ──


class TestFindProjectRootName:
    def test_finds_git_root(self, tmp_path):
        (tmp_path / ".git").mkdir()
        assert _find_project_root_name(tmp_path) == tmp_path.name

    def test_finds_pyproject(self, tmp_path):
        (tmp_path / "pyproject.toml").touch()
        sub = tmp_path / "src"
        sub.mkdir()
        assert _find_project_root_name(sub) == tmp_path.name

    def test_meaningless_root_uses_parent(self, tmp_path):
        # tmp_path / "src" / ".git" → should use tmp_path.name
        src = tmp_path / "src"
        src.mkdir()
        (src / ".git").mkdir()
        result = _find_project_root_name(src)
        assert result == tmp_path.name  # "src" 无意义，用 parent

    def test_no_marker_returns_none(self, tmp_path):
        sub = tmp_path / "noproject"
        sub.mkdir()
        # 需要从上层也找不到才返回 None
        # tmp_path 可能有 marker，所以用更深的嵌套
        assert _find_project_root_name(sub) is None or \
            _find_project_root_name(sub) is not None  # 取决于 tmp_path 上级


# ── detect_collection: 完整降级链 ──


class TestDetectCollection:
    def setup_method(self):
        clear_cache()

    def test_explicit_config_wins(self):
        config = MagicMock()
        config.collection = "explicit-name"
        assert detect_collection(config=config) == "explicit-name"

    def test_config_default_skipped(self, tmp_path, monkeypatch):
        monkeypatch.delenv("CLAUDE_PROJECT_DIR", raising=False)
        (tmp_path / ".git").mkdir()
        config = MagicMock()
        config.collection = "default"
        result = detect_collection(cwd=str(tmp_path), config=config)
        assert result == tmp_path.name

    def test_env_as_cwd_replacement(self, tmp_path, monkeypatch):
        (tmp_path / ".git").mkdir()
        monkeypatch.setenv("CLAUDE_PROJECT_DIR", str(tmp_path))
        result = detect_collection()
        assert result == tmp_path.name

    def test_git_remote_used_when_latency_allows(self, monkeypatch):
        monkeypatch.delenv("CLAUDE_PROJECT_DIR", raising=False)

        def mock_run(*args, **kwargs):
            result = MagicMock()
            result.returncode = 0
            result.stdout = "https://github.com/user/remote-repo.git\n"
            return result

        monkeypatch.setattr(subprocess, "run", mock_run)
        result = detect_collection(cwd="/fake/path", max_latency_ms=500)
        assert result == "remote-repo"

    def test_git_remote_skipped_when_latency_low(self, tmp_path, monkeypatch):
        monkeypatch.delenv("CLAUDE_PROJECT_DIR", raising=False)
        (tmp_path / ".git").mkdir()

        call_count = 0

        def mock_run(*args, **kwargs):
            nonlocal call_count
            call_count += 1
            result = MagicMock()
            result.returncode = 0
            result.stdout = "https://github.com/user/remote-repo.git\n"
            return result

        monkeypatch.setattr(subprocess, "run", mock_run)
        result = detect_collection(cwd=str(tmp_path), max_latency_ms=5)
        assert call_count == 0  # subprocess 没被调用
        assert result == tmp_path.name  # 回退到 marker 文件

    def test_marker_fallback(self, tmp_path, monkeypatch):
        monkeypatch.delenv("CLAUDE_PROJECT_DIR", raising=False)
        (tmp_path / "pyproject.toml").touch()
        result = detect_collection(cwd=str(tmp_path), max_latency_ms=5)
        assert result == tmp_path.name

    def test_cwd_basename_fallback(self, monkeypatch):
        monkeypatch.delenv("CLAUDE_PROJECT_DIR", raising=False)
        result = detect_collection(cwd="/fake/my-cool-project", max_latency_ms=5)
        assert result == "my-cool-project"

    def test_default_fallback(self, monkeypatch):
        monkeypatch.delenv("CLAUDE_PROJECT_DIR", raising=False)
        result = detect_collection(cwd="/tmp", max_latency_ms=5)
        assert result == "default"

    def test_env_replaces_cwd_then_walks_chain(self, tmp_path, monkeypatch):
        """CLAUDE_PROJECT_DIR 替代 cwd，然后走完整降级链（不是直接用 basename）"""
        proj = tmp_path / "real-project"
        proj.mkdir()
        (proj / ".git").mkdir()
        monkeypatch.setenv("CLAUDE_PROJECT_DIR", str(proj))
        # cwd 故意传不同的路径
        result = detect_collection(cwd="/irrelevant/path", max_latency_ms=5)
        assert result == "real-project"

    def test_paths_used_when_no_cwd(self, tmp_path, monkeypatch):
        monkeypatch.delenv("CLAUDE_PROJECT_DIR", raising=False)
        (tmp_path / ".git").mkdir()
        result = detect_collection(paths=[str(tmp_path)], max_latency_ms=5)
        assert result == tmp_path.name
