"""
Tests for Phase 8: History Tracking.

Tests git-based version control, event-driven commits, and history queries.
"""

import pytest
import tempfile
import subprocess
from pathlib import Path
from datetime import datetime

from markitect.spaces.history import (
    # Models
    Commit,
    Branch,
    HistoryEntry,
    DiffResult,
    DiffLine,
    DiffType,
    HistoryConfig,
    # Backend
    GitHistoryBackend,
    GitError,
    # Events
    GitHistoryEventHandler,
    HistoryEventCoordinator,
    # Queries
    HistoryQueryService,
)
from markitect.spaces import (
    SpaceEvent,
    SpaceEventType,
    EventBus,
)


# ===========================================================================
# Fixtures
# ===========================================================================


def check_git_available() -> bool:
    """Check if git is available."""
    try:
        result = subprocess.run(
            ["git", "--version"],
            capture_output=True,
            timeout=5,
        )
        return result.returncode == 0
    except (FileNotFoundError, subprocess.TimeoutExpired):
        return False


# Skip all tests if git is not available
pytestmark = pytest.mark.skipif(
    not check_git_available(),
    reason="git not available"
)


@pytest.fixture
def temp_dir():
    """Create a temporary directory."""
    with tempfile.TemporaryDirectory() as tmpdir:
        yield Path(tmpdir)


@pytest.fixture
def git_backend():
    """Create a git backend instance."""
    return GitHistoryBackend()


@pytest.fixture
def history_config():
    """Create a history configuration."""
    return HistoryConfig(
        enabled=True,
        backend="git",
        auto_commit=True,
        author_name="Test User",
        author_email="test@example.com",
    )


@pytest.fixture
def initialized_repo(temp_dir, git_backend, history_config):
    """Create an initialized git repository."""
    git_backend.initialize(temp_dir, history_config)
    return temp_dir


@pytest.fixture
def event_bus():
    """Create an event bus."""
    return EventBus()


# ===========================================================================
# HistoryConfig Tests
# ===========================================================================


class TestHistoryConfig:
    """Tests for HistoryConfig model."""

    def test_default_config(self):
        """Test default configuration."""
        config = HistoryConfig()
        assert config.enabled is False
        assert config.backend == "git"
        assert config.auto_commit is True
        assert config.author_name == "markitect"

    def test_config_with_custom_values(self):
        """Test configuration with custom values."""
        config = HistoryConfig(
            enabled=True,
            backend="git",
            author_name="John Doe",
            author_email="john@example.com",
            directory="/path/to/space",
        )
        assert config.enabled is True
        assert config.author_name == "John Doe"
        assert config.directory == "/path/to/space"

    def test_config_serialization(self):
        """Test config to/from dict."""
        config = HistoryConfig(
            enabled=True,
            author_name="Test",
            commit_on_events=["DOCUMENT_ADDED"],
        )
        data = config.to_dict()
        restored = HistoryConfig.from_dict(data)
        assert restored.enabled == config.enabled
        assert restored.author_name == config.author_name
        assert restored.commit_on_events == config.commit_on_events


# ===========================================================================
# Commit Model Tests
# ===========================================================================


class TestCommitModel:
    """Tests for Commit model."""

    def test_create_commit(self):
        """Test creating a commit."""
        commit = Commit(
            id="abc123",
            message="Initial commit",
            author="Test User <test@example.com>",
        )
        assert commit.id == "abc123"
        assert commit.message == "Initial commit"
        assert commit.short_id == "abc123"

    def test_commit_serialization(self):
        """Test commit to/from dict."""
        commit = Commit(
            id="abc123def456",
            message="Test commit",
            author="Test <test@example.com>",
            files_changed=["file1.md", "file2.md"],
            metadata={"key": "value"},
        )
        data = commit.to_dict()
        restored = Commit.from_dict(data)
        assert restored.id == commit.id
        assert restored.message == commit.message
        assert restored.files_changed == commit.files_changed
        assert restored.metadata == commit.metadata

    def test_short_id(self):
        """Test short commit ID."""
        commit = Commit(id="abcdef1234567890", message="Test")
        assert commit.short_id == "abcdef1"

        # Short ID for short hash
        short_commit = Commit(id="abc", message="Test")
        assert short_commit.short_id == "abc"


# ===========================================================================
# DiffResult Tests
# ===========================================================================


class TestDiffResult:
    """Tests for DiffResult model."""

    def test_create_diff(self):
        """Test creating a diff result."""
        diff = DiffResult(
            path="test.md",
            old_version="v1",
            new_version="v2",
        )
        assert diff.path == "test.md"
        assert diff.additions == 0
        assert diff.deletions == 0

    def test_unified_diff_format(self):
        """Test unified diff generation."""
        diff = DiffResult(path="test.md")
        diff.lines = [
            DiffLine(DiffType.CONTEXT, "unchanged line", 1, 1),
            DiffLine(DiffType.ADDITION, "added line", None, 2),
            DiffLine(DiffType.DELETION, "removed line", 2, None),
        ]
        diff.additions = 1
        diff.deletions = 1

        unified = diff.to_unified_diff()
        assert "--- a/test.md" in unified
        assert "+++ b/test.md" in unified
        assert "+added line" in unified
        assert "-removed line" in unified
        assert " unchanged line" in unified


# ===========================================================================
# GitHistoryBackend Tests
# ===========================================================================


class TestGitHistoryBackend:
    """Tests for GitHistoryBackend."""

    def test_initialize_creates_repo(self, temp_dir, git_backend, history_config):
        """Test initializing a git repository."""
        git_backend.initialize(temp_dir, history_config)
        assert (temp_dir / ".git").exists()
        assert git_backend.is_initialized(temp_dir)

    def test_initialize_idempotent(self, temp_dir, git_backend, history_config):
        """Test that initialize can be called multiple times."""
        git_backend.initialize(temp_dir, history_config)
        git_backend.initialize(temp_dir, history_config)
        assert git_backend.is_initialized(temp_dir)

    def test_is_initialized_false_for_non_repo(self, temp_dir, git_backend):
        """Test is_initialized returns False for non-repo."""
        assert not git_backend.is_initialized(temp_dir)

    def test_commit_creates_commit(self, initialized_repo, git_backend):
        """Test creating a commit."""
        # Create a file
        test_file = initialized_repo / "test.md"
        test_file.write_text("# Test")

        # Commit
        commit = git_backend.commit(
            initialized_repo,
            "Add test file",
            author="Test User <test@example.com>",
        )
        assert commit.message == "Add test file"
        assert commit.id is not None
        assert "test.md" in commit.files_changed

    def test_commit_with_metadata(self, initialized_repo, git_backend):
        """Test commit with metadata trailers."""
        test_file = initialized_repo / "doc.md"
        test_file.write_text("Content")

        commit = git_backend.commit(
            initialized_repo,
            "Add doc",
            metadata={"space_id": "space-1", "event_type": "test"},
        )
        # Git trailers are stored as "key: value" in commit body
        # They should be parseable
        assert commit.message == "Add doc"
        assert commit.id is not None
        # Metadata parsing from git trailers may vary by git version
        # Just verify commit succeeds

    def test_commit_specific_files(self, initialized_repo, git_backend):
        """Test committing specific files."""
        file1 = initialized_repo / "file1.md"
        file2 = initialized_repo / "file2.md"
        file1.write_text("File 1")
        file2.write_text("File 2")

        # Commit only file1
        commit = git_backend.commit(
            initialized_repo,
            "Add file1",
            files=["file1.md"],
        )
        assert "file1.md" in commit.files_changed

        # file2 should still be uncommitted
        assert git_backend.has_changes(initialized_repo)

    def test_commit_no_changes_raises_error(self, initialized_repo, git_backend):
        """Test that committing with no changes raises error."""
        with pytest.raises(GitError, match="No changes"):
            git_backend.commit(initialized_repo, "Empty commit")

    def test_get_commit(self, initialized_repo, git_backend):
        """Test getting a commit by ID."""
        test_file = initialized_repo / "test.md"
        test_file.write_text("Content")

        commit = git_backend.commit(initialized_repo, "Test commit")

        retrieved = git_backend.get_commit(initialized_repo, commit.id)
        assert retrieved is not None
        assert retrieved.id == commit.id
        assert retrieved.message == "Test commit"

    def test_get_commit_not_found(self, initialized_repo, git_backend):
        """Test getting non-existent commit returns None."""
        result = git_backend.get_commit(initialized_repo, "nonexistent")
        assert result is None

    def test_get_log(self, initialized_repo, git_backend):
        """Test getting commit history."""
        # Create multiple commits
        for i in range(3):
            file = initialized_repo / f"file{i}.md"
            file.write_text(f"File {i}")
            git_backend.commit(initialized_repo, f"Commit {i}")

        # Get log
        commits = git_backend.get_log(initialized_repo, limit=10)
        # Should have initial commit + 3 new commits
        assert len(commits) >= 3
        assert commits[0].message == "Commit 2"  # Most recent first
        assert commits[1].message == "Commit 1"

    def test_get_log_with_limit(self, initialized_repo, git_backend):
        """Test log with limit."""
        for i in range(5):
            file = initialized_repo / f"file{i}.md"
            file.write_text(f"File {i}")
            git_backend.commit(initialized_repo, f"Commit {i}")

        commits = git_backend.get_log(initialized_repo, limit=2)
        assert len(commits) == 2

    def test_get_log_with_path_filter(self, initialized_repo, git_backend):
        """Test log filtered by path."""
        file1 = initialized_repo / "file1.md"
        file2 = initialized_repo / "file2.md"

        file1.write_text("File 1")
        git_backend.commit(initialized_repo, "Add file1")

        file2.write_text("File 2")
        git_backend.commit(initialized_repo, "Add file2")

        file1.write_text("File 1 updated")
        git_backend.commit(initialized_repo, "Update file1")

        # Get log for file1 only
        commits = git_backend.get_log(initialized_repo, path="file1.md")
        # Should have 2 commits for file1 (add and update)
        assert len(commits) >= 2
        assert all("file1" in c.message for c in commits[:2])

    def test_get_diff(self, initialized_repo, git_backend):
        """Test getting diff between commits."""
        file = initialized_repo / "test.md"
        file.write_text("Line 1\nLine 2\n")
        commit1 = git_backend.commit(initialized_repo, "Initial")

        file.write_text("Line 1\nLine 2 modified\nLine 3\n")
        commit2 = git_backend.commit(initialized_repo, "Update")

        diffs = git_backend.get_diff(initialized_repo, commit1.id, commit2.id)
        assert len(diffs) > 0
        diff = diffs[0]
        assert diff.path == "test.md"
        assert diff.additions > 0 or diff.deletions > 0

    def test_checkout_version(self, initialized_repo, git_backend):
        """Test checking out a specific version."""
        file = initialized_repo / "test.md"
        file.write_text("Version 1")
        commit1 = git_backend.commit(initialized_repo, "V1")

        file.write_text("Version 2")
        git_backend.commit(initialized_repo, "V2")

        # Checkout file at commit1
        success = git_backend.checkout(initialized_repo, commit1.id, "test.md")
        assert success
        assert file.read_text() == "Version 1"

    def test_get_file_at_version(self, initialized_repo, git_backend):
        """Test getting file content at specific version."""
        file = initialized_repo / "test.md"
        file.write_text("Version 1")
        commit1 = git_backend.commit(initialized_repo, "V1")

        file.write_text("Version 2")
        git_backend.commit(initialized_repo, "V2")

        # Get content at commit1
        content = git_backend.get_file_at_version(
            initialized_repo, "test.md", commit1.id
        )
        assert content == "Version 1"

    def test_get_file_at_version_not_found(self, initialized_repo, git_backend):
        """Test getting non-existent file returns None."""
        content = git_backend.get_file_at_version(
            initialized_repo, "nonexistent.md", "HEAD"
        )
        assert content is None

    def test_list_branches(self, initialized_repo, git_backend):
        """Test listing branches."""
        branches = git_backend.list_branches(initialized_repo)
        assert len(branches) >= 1
        # Should have at least main/master branch
        assert any(b.name in ["main", "master"] for b in branches)

    def test_create_branch(self, initialized_repo, git_backend):
        """Test creating a branch."""
        # Create a commit first
        file = initialized_repo / "test.md"
        file.write_text("Content")
        git_backend.commit(initialized_repo, "Add file")

        branch = git_backend.create_branch(initialized_repo, "feature")
        assert branch.name == "feature"
        assert branch.head_commit_id is not None

        branches = git_backend.list_branches(initialized_repo)
        assert any(b.name == "feature" for b in branches)

    def test_switch_branch(self, initialized_repo, git_backend):
        """Test switching branches."""
        # Create commit and branch
        file = initialized_repo / "test.md"
        file.write_text("Content")
        git_backend.commit(initialized_repo, "Add file")
        git_backend.create_branch(initialized_repo, "feature")

        # Switch to feature branch
        success = git_backend.switch_branch(initialized_repo, "feature")
        assert success

        current = git_backend.get_current_branch(initialized_repo)
        assert current == "feature"

    def test_get_current_branch(self, initialized_repo, git_backend):
        """Test getting current branch."""
        current = git_backend.get_current_branch(initialized_repo)
        assert current in ["main", "master"]  # Depends on git version

    def test_has_changes(self, initialized_repo, git_backend):
        """Test checking for uncommitted changes."""
        # No changes initially
        assert not git_backend.has_changes(initialized_repo)

        # Add a file
        file = initialized_repo / "test.md"
        file.write_text("Content")

        # Should have changes now
        assert git_backend.has_changes(initialized_repo)

        # Commit
        git_backend.commit(initialized_repo, "Add file")

        # No changes after commit
        assert not git_backend.has_changes(initialized_repo)


# ===========================================================================
# GitHistoryEventHandler Tests
# ===========================================================================


class TestGitHistoryEventHandler:
    """Tests for event-driven commits."""

    def test_create_handler(self, git_backend, history_config):
        """Test creating an event handler."""
        def resolver(space_id):
            return Path(f"/tmp/{space_id}")

        handler = GitHistoryEventHandler(git_backend, history_config, resolver)
        assert handler is not None

    def test_handler_registers_with_bus(
        self, git_backend, history_config, event_bus
    ):
        """Test handler registration."""
        def resolver(space_id):
            return Path(f"/tmp/{space_id}")

        handler = GitHistoryEventHandler(git_backend, history_config, resolver)
        handler.register(event_bus)

        # Should have registered handlers
        assert len(event_bus._handlers) > 0

        handler.unregister()

    def test_handler_creates_commit_on_event(
        self, temp_dir, git_backend, history_config, event_bus
    ):
        """Test that handler commits on document events."""
        space_id = "test-space"

        def resolver(sid):
            return temp_dir if sid == space_id else None

        # Initialize repo
        git_backend.initialize(temp_dir, history_config)

        # Create initial file
        file = temp_dir / "doc.md"
        file.write_text("Initial content")
        git_backend.commit(temp_dir, "Initial commit")

        # Create handler
        handler = GitHistoryEventHandler(git_backend, history_config, resolver)
        handler.register(event_bus)

        # Modify file
        file.write_text("Updated content")

        # Emit event
        event = SpaceEvent(
            event_type=SpaceEventType.DOCUMENT_CONTENT_CHANGED,
            space_id=space_id,
            payload={"document_id": "doc-1", "space_path": "/doc.md"},
        )
        event_bus.emit(event)

        # Should have created a new commit
        commits = git_backend.get_log(temp_dir, limit=5)
        assert len(commits) >= 2
        assert "Update document" in commits[0].message

        handler.unregister()

    def test_handler_disabled_when_not_enabled(
        self, temp_dir, git_backend, event_bus
    ):
        """Test handler does nothing when disabled."""
        config = HistoryConfig(enabled=False)
        space_id = "test-space"

        def resolver(sid):
            return temp_dir if sid == space_id else None

        handler = GitHistoryEventHandler(git_backend, config, resolver)
        handler.register(event_bus)

        # Initialize repo manually
        git_backend.initialize(temp_dir, config)

        file = temp_dir / "doc.md"
        file.write_text("Content")

        # Emit event - should not commit since disabled
        event = SpaceEvent(
            event_type=SpaceEventType.DOCUMENT_ADDED,
            space_id=space_id,
            payload={"space_path": "/doc.md"},
        )
        event_bus.emit(event)

        # Should still have uncommitted changes
        assert git_backend.has_changes(temp_dir)

        handler.unregister()


# ===========================================================================
# HistoryQueryService Tests
# ===========================================================================


class TestHistoryQueryService:
    """Tests for HistoryQueryService."""

    def test_create_service(self, git_backend):
        """Test creating a query service."""
        def resolver(space_id):
            return Path(f"/tmp/{space_id}")

        service = HistoryQueryService(git_backend, resolver)
        assert service is not None

    def test_get_history(self, initialized_repo, git_backend):
        """Test getting history entries."""
        space_id = "test-space"

        def resolver(sid):
            return initialized_repo if sid == space_id else None

        service = HistoryQueryService(git_backend, resolver)

        # Create commits
        for i in range(3):
            file = initialized_repo / f"file{i}.md"
            file.write_text(f"Content {i}")
            git_backend.commit(initialized_repo, f"Add file {i}")

        # Get history
        history = service.get_history(space_id, limit=10)
        assert len(history) >= 3
        assert isinstance(history[0], HistoryEntry)
        assert history[0].commit.message.startswith("Add file")

    def test_get_version_content(self, initialized_repo, git_backend):
        """Test getting content at specific version."""
        space_id = "test-space"

        def resolver(sid):
            return initialized_repo if sid == space_id else None

        service = HistoryQueryService(git_backend, resolver)

        # Create versions
        file = initialized_repo / "doc.md"
        file.write_text("Version 1")
        commit1 = git_backend.commit(initialized_repo, "V1")

        file.write_text("Version 2")
        git_backend.commit(initialized_repo, "V2")

        # Get content at V1
        content = service.get_version_content(space_id, "/doc.md", commit1.id)
        assert content == "Version 1"

    def test_compare_versions(self, initialized_repo, git_backend):
        """Test comparing two versions."""
        space_id = "test-space"

        def resolver(sid):
            return initialized_repo if sid == space_id else None

        service = HistoryQueryService(git_backend, resolver)

        file = initialized_repo / "doc.md"
        file.write_text("Line 1\n")
        commit1 = git_backend.commit(initialized_repo, "V1")

        file.write_text("Line 1\nLine 2\n")
        commit2 = git_backend.commit(initialized_repo, "V2")

        diff = service.compare_versions(space_id, "/doc.md", commit1.id, commit2.id)
        assert diff is not None
        assert diff.additions > 0

    def test_search_history(self, initialized_repo, git_backend):
        """Test searching commit messages."""
        space_id = "test-space"

        def resolver(sid):
            return initialized_repo if sid == space_id else None

        service = HistoryQueryService(git_backend, resolver)

        # Create commits with different messages
        file = initialized_repo / "doc.md"
        file.write_text("V1")
        git_backend.commit(initialized_repo, "Add feature X")

        file.write_text("V2")
        git_backend.commit(initialized_repo, "Fix bug Y")

        file.write_text("V3")
        git_backend.commit(initialized_repo, "Update feature X")

        # Search for "feature"
        results = service.search_history(space_id, "feature")
        assert len(results) >= 2
        assert all("feature" in r.commit.message.lower() for r in results)

    def test_restore_version(self, initialized_repo, git_backend):
        """Test restoring a document to previous version."""
        space_id = "test-space"

        def resolver(sid):
            return initialized_repo if sid == space_id else None

        service = HistoryQueryService(git_backend, resolver)

        file = initialized_repo / "doc.md"
        file.write_text("Version 1")
        commit1 = git_backend.commit(initialized_repo, "V1")

        file.write_text("Version 2")
        git_backend.commit(initialized_repo, "V2")

        # Restore to V1
        success = service.restore_version(space_id, "/doc.md", commit1.id)
        assert success
        assert file.read_text() == "Version 1"

    def test_has_history(self, initialized_repo, git_backend):
        """Test checking if space has history."""
        space1 = "space-with-history"
        space2 = "space-without-history"

        # Create fresh directory for space2
        with tempfile.TemporaryDirectory() as tmpdir:
            no_history_dir = Path(tmpdir)

            def resolver(sid):
                if sid == space1:
                    return initialized_repo
                elif sid == space2:
                    return no_history_dir
                return None

            service = HistoryQueryService(git_backend, resolver)

            assert service.has_history(space1)
            assert not service.has_history(space2)

    def test_get_branches(self, initialized_repo, git_backend):
        """Test getting branch names."""
        space_id = "test-space"

        def resolver(sid):
            return initialized_repo if sid == space_id else None

        service = HistoryQueryService(git_backend, resolver)

        # Create a commit first
        file = initialized_repo / "doc.md"
        file.write_text("Content")
        git_backend.commit(initialized_repo, "Add file")

        # Create branch
        git_backend.create_branch(initialized_repo, "feature")

        branches = service.get_branches(space_id)
        assert "feature" in branches

    def test_has_uncommitted_changes(self, initialized_repo, git_backend):
        """Test checking for uncommitted changes."""
        space_id = "test-space"

        def resolver(sid):
            return initialized_repo if sid == space_id else None

        service = HistoryQueryService(git_backend, resolver)

        # No changes initially
        assert not service.has_uncommitted_changes(space_id)

        # Add file
        file = initialized_repo / "doc.md"
        file.write_text("Content")

        # Should have changes
        assert service.has_uncommitted_changes(space_id)


# ===========================================================================
# Integration Tests
# ===========================================================================


class TestHistoryIntegration:
    """Integration tests for history tracking."""

    def test_full_workflow(self, temp_dir, git_backend, history_config, event_bus):
        """Test complete history workflow."""
        space_id = "test-space"

        def resolver(sid):
            return temp_dir if sid == space_id else None

        # Initialize
        git_backend.initialize(temp_dir, history_config)

        # Create query service
        query_service = HistoryQueryService(git_backend, resolver)

        # Create event handler
        event_handler = GitHistoryEventHandler(
            git_backend, history_config, resolver
        )
        event_handler.register(event_bus)

        # Add initial file
        file = temp_dir / "intro.md"
        file.write_text("# Introduction")
        git_backend.commit(temp_dir, "Initial commit")

        # Emit document added event
        file2 = temp_dir / "chapter1.md"
        file2.write_text("# Chapter 1")
        event = SpaceEvent(
            event_type=SpaceEventType.DOCUMENT_ADDED,
            space_id=space_id,
            payload={"space_path": "/chapter1.md"},
        )
        event_bus.emit(event)

        # Should have 2 commits
        history = query_service.get_history(space_id)
        assert len(history) >= 2

        # Get specific version (find the commit where intro.md was added)
        commits = git_backend.get_log(temp_dir)
        intro_commit = None
        for commit in reversed(commits):
            if "intro.md" in commit.files_changed:
                intro_commit = commit
                break

        if intro_commit:
            old_content = query_service.get_version_content(
                space_id, "/intro.md", intro_commit.id
            )
            assert old_content == "# Introduction"

        # Compare versions
        file.write_text("# Introduction\nUpdated")
        git_backend.commit(temp_dir, "Update intro")

        diff = query_service.compare_versions(
            space_id, "/intro.md", commits[-1].id, "HEAD"
        )
        assert diff is not None
        assert diff.additions > 0

        event_handler.unregister()
