"""cognitive/rebuild.py 测试"""

from __future__ import annotations

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

import pytest

from index1.cognitive.rebuild import (
    RebuildStats,
    _commit_to_facts,
    _make_rebuild_fact,
    _parse_log_output,
    rebuild_from_git,
)


class TestParseLogOutput:
    def test_single_commit(self):
        output = """__COMMIT__abc123def456
fix: repair database connection
John Doe
2025-06-15T10:30:00+08:00
src/db.py
src/config.py
"""
        commits = _parse_log_output(output)
        assert len(commits) == 1
        assert commits[0]["hash"] == "abc123def456"
        assert commits[0]["message"] == "fix: repair database connection"
        assert commits[0]["author"] == "John Doe"
        assert commits[0]["files"] == ["src/db.py", "src/config.py"]

    def test_multiple_commits(self):
        output = """__COMMIT__aaa111
feat: add feature
Alice
2025-06-15T10:00:00+08:00
src/a.py
__COMMIT__bbb222
fix: bug
Bob
2025-06-14T09:00:00+08:00
src/b.py
src/c.py
"""
        commits = _parse_log_output(output)
        assert len(commits) == 2
        assert commits[0]["files"] == ["src/a.py"]
        assert commits[1]["files"] == ["src/b.py", "src/c.py"]

    def test_empty_output(self):
        assert _parse_log_output("") == []
        assert _parse_log_output("\n\n") == []

    def test_commit_with_no_files(self):
        output = """__COMMIT__ccc333
chore: empty commit
Dev
2025-01-01T00:00:00+00:00
"""
        commits = _parse_log_output(output)
        assert len(commits) == 1
        assert commits[0]["files"] == []


class TestCommitToFacts:
    def test_basic_commit(self):
        commit = {
            "hash": "abc123def456",
            "message": "fix: repair database connection",
            "author": "John",
            "date": "2025-06-15",
            "files": ["src/db.py"],
        }
        facts = _commit_to_facts(commit, "test-proj")
        # 1 commit 级 + 1 文件级
        assert len(facts) == 2
        assert any("Committed:" in f["assertion"] for f in facts)
        assert any("Modified src/db.py" in f["assertion"] for f in facts)
        for f in facts:
            assert f["source"] == "rebuild"
            assert f["confidence"] == 0.6
            assert f["collection"] == "test-proj"

    def test_many_files_skip_per_file(self):
        """超过 20 个文件时只生成 commit 级 fact"""
        commit = {
            "hash": "abc123",
            "message": "refactor: big rename",
            "author": "Dev",
            "date": "2025-01-01",
            "files": [f"src/file_{i}.py" for i in range(25)],
        }
        facts = _commit_to_facts(commit, "proj")
        assert len(facts) == 1
        assert "Committed:" in facts[0]["assertion"]

    def test_exactly_20_files(self):
        """恰好 20 个文件时生成文件级 fact"""
        commit = {
            "hash": "abc123",
            "message": "feat: add many files",
            "author": "Dev",
            "date": "2025-01-01",
            "files": [f"src/file_{i}.py" for i in range(20)],
        }
        facts = _commit_to_facts(commit, "proj")
        # 1 commit 级 + 20 文件级
        assert len(facts) == 21

    def test_no_files(self):
        """无文件的 commit 只生成 commit 级 fact"""
        commit = {
            "hash": "abc123",
            "message": "chore: update something",
            "author": "Dev",
            "date": "2025-01-01",
            "files": [],
        }
        facts = _commit_to_facts(commit, "proj")
        assert len(facts) == 1


class TestMakeRebuildFact:
    def test_defaults(self):
        fact = _make_rebuild_fact(
            assertion="test", category="code", collection="proj"
        )
        assert fact["source"] == "rebuild"
        assert fact["confidence"] == 0.6
        assert fact["status"] == "active"
        assert fact["scope_modules"] == "[]"
        assert fact["session_id"] is None

    def test_scope_files(self):
        fact = _make_rebuild_fact(
            assertion="test", category="code", collection="proj",
            scope_files=["a.py", "b.py"],
        )
        import json
        assert json.loads(fact["scope_files"]) == ["a.py", "b.py"]


class TestRebuildFromGit:
    def test_dry_run_no_db_writes(self, tmp_path):
        """dry_run 只统计不写入"""
        cog_db = MagicMock()

        with patch("index1.cognitive.rebuild._parse_git_log") as mock_log:
            mock_log.return_value = [{
                "hash": "abc123",
                "message": "feat: add feature",
                "author": "Dev",
                "date": "2025-01-01",
                "files": ["src/a.py"],
            }]

            stats = rebuild_from_git(
                db_cog=cog_db,
                repo_path=tmp_path,
                collection="test",
                dry_run=True,
            )

        assert stats.facts_created > 0
        assert stats.commits_processed == 1
        cog_db.insert_fact.assert_not_called()

    def test_merge_commits_skipped(self, tmp_path):
        """Merge commit 应被跳过"""
        cog_db = MagicMock()

        with patch("index1.cognitive.rebuild._parse_git_log") as mock_log:
            mock_log.return_value = [{
                "hash": "abc123",
                "message": "Merge branch 'feature' into main",
                "author": "Dev",
                "date": "2025-01-01",
                "files": ["src/a.py"],
            }]

            stats = rebuild_from_git(
                db_cog=cog_db,
                repo_path=tmp_path,
                collection="test",
            )

        assert stats.commits_skipped == 1
        assert stats.commits_processed == 0

    def test_merge_pull_request_skipped(self, tmp_path):
        """Merge pull request 也应被跳过"""
        cog_db = MagicMock()

        with patch("index1.cognitive.rebuild._parse_git_log") as mock_log:
            mock_log.return_value = [{
                "hash": "abc123",
                "message": "Merge pull request #42 from feature-branch",
                "author": "Dev",
                "date": "2025-01-01",
                "files": [],
            }]

            stats = rebuild_from_git(
                db_cog=cog_db,
                repo_path=tmp_path,
                collection="test",
            )

        assert stats.commits_skipped == 1

    def test_duplicate_dedup(self, tmp_path):
        """重复 fact 应被跳过"""
        cog_db = MagicMock()

        with patch("index1.cognitive.rebuild._parse_git_log") as mock_log, \
             patch("index1.cognitive.rebuild._fact_exists_any_session", return_value=True):
            mock_log.return_value = [{
                "hash": "abc123",
                "message": "fix: something",
                "author": "Dev",
                "date": "2025-01-01",
                "files": ["src/a.py"],
            }]

            stats = rebuild_from_git(
                db_cog=cog_db,
                repo_path=tmp_path,
                collection="test",
            )

        assert stats.facts_duplicate > 0
        cog_db.insert_fact.assert_not_called()

    def test_insert_failure_counted(self, tmp_path):
        """insert_fact 返回 None 时计入 errors"""
        cog_db = MagicMock()
        cog_db.insert_fact.return_value = None

        with patch("index1.cognitive.rebuild._parse_git_log") as mock_log, \
             patch("index1.cognitive.rebuild._fact_exists_any_session", return_value=False):
            mock_log.return_value = [{
                "hash": "abc123",
                "message": "fix: something",
                "author": "Dev",
                "date": "2025-01-01",
                "files": [],
            }]

            stats = rebuild_from_git(
                db_cog=cog_db,
                repo_path=tmp_path,
                collection="test",
            )

        assert stats.errors > 0
        assert stats.facts_created == 0

    def test_successful_insert(self, tmp_path):
        """正常插入应计入 facts_created"""
        cog_db = MagicMock()
        cog_db.insert_fact.return_value = 1

        with patch("index1.cognitive.rebuild._parse_git_log") as mock_log, \
             patch("index1.cognitive.rebuild._fact_exists_any_session", return_value=False):
            mock_log.return_value = [{
                "hash": "abc123",
                "message": "fix: something",
                "author": "Dev",
                "date": "2025-01-01",
                "files": [],
            }]

            stats = rebuild_from_git(
                db_cog=cog_db,
                repo_path=tmp_path,
                collection="test",
            )

        assert stats.facts_created > 0
        assert stats.commits_processed == 1
