"""Indexer 模块测试"""

from __future__ import annotations

import asyncio
from pathlib import Path
from unittest.mock import AsyncMock, patch

import pytest

from index1.config import Config
from index1.db import Database
from index1.embed import OllamaEmbedder
from index1.indexer import (
    INDEXABLE_EXTENSIONS,
    SKIP_DIRS,
    _collect_files,
    _detect_doc_type,
    _file_hash,
    _in_skip_dir,
    _should_index,
    index_files,
)


@pytest.fixture
def tmp_project(tmp_path):
    """创建临时项目目录结构"""
    # 可索引文件
    (tmp_path / "README.md").write_text("# Hello\n\nWorld", encoding="utf-8")
    (tmp_path / "main.py").write_text("def hello():\n    pass\n", encoding="utf-8")
    (tmp_path / "lib.rs").write_text("fn main() {}\n", encoding="utf-8")
    (tmp_path / "config.yaml").write_text("key: value\n", encoding="utf-8")

    # 跳过的目录
    (tmp_path / ".git").mkdir()
    (tmp_path / ".git" / "config").write_text("git config", encoding="utf-8")
    (tmp_path / "__pycache__").mkdir()
    (tmp_path / "__pycache__" / "main.cpython-312.pyc").write_bytes(b"bytecode")

    # 不可索引文件
    (tmp_path / "image.png").write_bytes(b"\x89PNG")
    (tmp_path / "empty.py").write_text("", encoding="utf-8")

    # 子目录
    sub = tmp_path / "src"
    sub.mkdir()
    (sub / "app.py").write_text("class App:\n    pass\n", encoding="utf-8")

    return tmp_path


@pytest.fixture
def config(tmp_path):
    return Config(db_path=str(tmp_path / "test.db"))


@pytest.fixture
def db(config):
    d = Database(config.db_path_resolved, config.embedding_dim)
    d.connect()
    yield d
    d.close()


@pytest.fixture
def mock_embedder():
    embedder = AsyncMock(spec=OllamaEmbedder)
    embedder.model = "nomic-embed-text"
    embedder.dim = 768
    embedder.check_availability = AsyncMock(return_value=False)
    embedder.available = False
    embedder.close = AsyncMock()
    return embedder


class TestFileHash:
    def test_hash_file(self, tmp_path):
        f = tmp_path / "test.txt"
        f.write_text("hello", encoding="utf-8")
        h = _file_hash(f)
        assert len(h) == 64  # SHA256 hex

    def test_hash_nonexistent(self, tmp_path):
        h = _file_hash(tmp_path / "nonexistent.txt")
        assert h == ""

    def test_hash_deterministic(self, tmp_path):
        f = tmp_path / "test.txt"
        f.write_text("hello", encoding="utf-8")
        assert _file_hash(f) == _file_hash(f)


class TestDetectDocType:
    def test_markdown(self):
        assert _detect_doc_type(Path("README.md")) == "markdown"

    def test_python(self):
        assert _detect_doc_type(Path("app.py")) == "python"

    def test_rust(self):
        assert _detect_doc_type(Path("lib.rs")) == "rust"

    def test_javascript(self):
        assert _detect_doc_type(Path("app.js")) == "javascript"
        assert _detect_doc_type(Path("app.tsx")) == "javascript"

    def test_unknown(self):
        assert _detect_doc_type(Path("config.yaml")) == "text"


class TestShouldIndex:
    def test_valid_file(self, tmp_path):
        f = tmp_path / "test.py"
        f.write_text("code", encoding="utf-8")
        assert _should_index(f) is True

    def test_unsupported_ext(self, tmp_path):
        f = tmp_path / "image.png"
        f.write_bytes(b"\x89PNG")
        assert _should_index(f) is False

    def test_empty_file(self, tmp_path):
        f = tmp_path / "empty.py"
        f.write_text("", encoding="utf-8")
        assert _should_index(f) is False


class TestCollectFiles:
    def test_collect_directory(self, tmp_project):
        files = _collect_files([tmp_project])
        names = {f.name for f in files}
        assert "README.md" in names
        assert "main.py" in names
        assert "app.py" in names
        assert "config.yaml" in names
        # 排除项
        assert "image.png" not in names
        assert "empty.py" not in names

    def test_skip_dirs(self, tmp_project):
        files = _collect_files([tmp_project])
        paths = [str(f) for f in files]
        assert not any(".git" in p.split("/") for p in paths)
        assert not any("__pycache__" in p.split("/") for p in paths)

    def test_single_file(self, tmp_project):
        files = _collect_files([tmp_project / "main.py"])
        assert len(files) == 1
        assert files[0].name == "main.py"


class TestInSkipDir:
    def test_in_git(self, tmp_path):
        root = tmp_path
        f = root / ".git" / "config"
        assert _in_skip_dir(f, root) is True

    def test_not_in_skip(self, tmp_path):
        root = tmp_path
        f = root / "src" / "app.py"
        assert _in_skip_dir(f, root) is False


class TestIndexFiles:
    @pytest.mark.asyncio
    async def test_index_basic(self, tmp_project, config, db, mock_embedder):
        """基本索引流程：扫描、分块、入库"""
        stats = await index_files(
            [str(tmp_project)], config, db, mock_embedder,
            collection="test", base_dir=tmp_project,
        )
        assert stats["total"] > 0
        assert stats["indexed"] > 0
        assert stats["failed"] == 0

        # 检查数据库有数据
        db_stats = db.get_stats()
        assert db_stats["doc_count"] > 0
        assert db_stats["chunk_count"] > 0

    @pytest.mark.asyncio
    async def test_incremental_skip(self, tmp_project, config, db, mock_embedder):
        """增量索引：未变更文件应跳过"""
        # 第一次索引
        stats1 = await index_files(
            [str(tmp_project)], config, db, mock_embedder,
            collection="test", base_dir=tmp_project,
        )
        # 第二次索引（文件未变更）
        stats2 = await index_files(
            [str(tmp_project)], config, db, mock_embedder,
            collection="test", base_dir=tmp_project,
        )
        assert stats2["skipped"] == stats1["indexed"]
        assert stats2["indexed"] == 0

    @pytest.mark.asyncio
    async def test_force_reindex(self, tmp_project, config, db, mock_embedder):
        """强制全量重索引"""
        # 第一次索引
        await index_files(
            [str(tmp_project)], config, db, mock_embedder,
            collection="test", base_dir=tmp_project,
        )
        # 强制重索引
        stats = await index_files(
            [str(tmp_project)], config, db, mock_embedder,
            collection="test", force=True, base_dir=tmp_project,
        )
        assert stats["indexed"] > 0
        assert stats["skipped"] == 0

    @pytest.mark.asyncio
    async def test_empty_directory(self, tmp_path, config, db, mock_embedder):
        """空目录不应报错"""
        empty = tmp_path / "empty"
        empty.mkdir()
        stats = await index_files(
            [str(empty)], config, db, mock_embedder,
        )
        assert stats["total"] == 0

    @pytest.mark.asyncio
    async def test_with_embeddings(self, tmp_project, config, db, mock_embedder):
        """测试带 embedding 的索引"""
        mock_embedder.check_availability = AsyncMock(return_value=True)
        mock_embedder.available = True
        # 返回假向量
        mock_embedder.embed_batch = AsyncMock(
            side_effect=lambda texts: [[0.1] * 768 for _ in texts]
        )

        stats = await index_files(
            [str(tmp_project)], config, db, mock_embedder,
            collection="test", base_dir=tmp_project,
        )
        assert stats["indexed"] > 0
