"""
Integration tests for git attribution stripping workflow.

These tests validate the full end-to-end workflow of stripping Claude attribution
from git commits, including:
- Full strip and restore cycle
- Commit-msg hook automatic stripping
- Preserving human co-authors
- Handling merge commits
- CLI full workflow

Unlike unit tests, these tests exercise real git operations end-to-end.
"""

import os
import subprocess
from collections.abc import Generator
from pathlib import Path

import pytest
from click.testing import CliRunner

from persuade.git_attribution import (
    create_backup_ref,
    list_backup_refs,
    preview_changes,
    restore_from_backup,
    strip_attribution_from_commits,
)
from tests.conftest import check_git_available, init_git_repo

PROJECT_ROOT: Path = Path(__file__).parent.parent.parent
COMMIT_MSG_HOOK: Path = PROJECT_ROOT / ".claude" / "git-hooks" / "commit-msg-strip-attribution"

# Timeout for git operations
GIT_TIMEOUT_SECONDS: int = 30


def get_commit_message(repo_path: Path, commit: str = "HEAD") -> str:
    """Get the commit message for a given commit."""
    result = subprocess.run(
        ["git", "log", "-1", "--format=%B", commit],
        cwd=repo_path,
        capture_output=True,
        text=True,
        check=True,
        timeout=GIT_TIMEOUT_SECONDS,
    )
    return result.stdout


def get_commit_sha(repo_path: Path, commit: str = "HEAD") -> str:
    """Get the SHA for a given commit."""
    result = subprocess.run(
        ["git", "rev-parse", commit],
        cwd=repo_path,
        capture_output=True,
        text=True,
        check=True,
        timeout=GIT_TIMEOUT_SECONDS,
    )
    return result.stdout.strip()


def create_commit_with_message(repo_path: Path, message: str, filename: str | None = None) -> str:
    """Create a commit with the specified message and return its SHA."""
    if filename is None:
        filename = f"file_{os.urandom(4).hex()}.txt"
    (repo_path / filename).write_text(f"content for {filename}")
    subprocess.run(["git", "add", "."], cwd=repo_path, capture_output=True, check=True)
    subprocess.run(
        ["git", "commit", "-m", message],
        cwd=repo_path,
        capture_output=True,
        check=True,
        timeout=GIT_TIMEOUT_SECONDS,
    )
    return get_commit_sha(repo_path)


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


@pytest.fixture
def git_repo(tmp_path: Path) -> Generator[Path, None, None]:
    """Create a temporary git repository for testing."""
    init_git_repo(tmp_path)
    yield tmp_path


class TestFullStripAndRestoreCycle:
    """Test the complete strip -> verify -> restore -> verify cycle."""

    def test_strip_and_restore_single_commit(self, git_repo: Path) -> None:
        """Test stripping and restoring attribution from a single commit."""
        # Create commit with Claude attribution
        message = """feat: add authentication

Implemented OAuth2 flow.

Co-Authored-By: Claude <noreply@anthropic.com>
"""
        original_sha = create_commit_with_message(git_repo, message, "auth.py")

        # Verify attribution exists
        original_message = get_commit_message(git_repo)
        assert "Claude" in original_message
        assert "anthropic.com" in original_message

        # Strip attribution
        result = strip_attribution_from_commits(git_repo, commit_count=1)

        assert result.success is True
        assert result.commits_rewritten == 1
        assert result.backup_ref is not None

        # Verify attribution is removed
        stripped_message = get_commit_message(git_repo)
        assert "Claude" not in stripped_message
        assert "anthropic.com" not in stripped_message
        assert "feat: add authentication" in stripped_message

        # Verify SHA changed (history was rewritten)
        new_sha = get_commit_sha(git_repo)
        assert new_sha != original_sha

        # Restore from backup
        restore_result = restore_from_backup(git_repo, result.backup_ref)
        assert restore_result.success is True

        # Verify original message is back
        restored_message = get_commit_message(git_repo)
        assert "Claude" in restored_message
        assert "anthropic.com" in restored_message

        # Verify SHA matches original
        restored_sha = get_commit_sha(git_repo)
        assert restored_sha == original_sha

    def test_strip_and_restore_multiple_commits(self, git_repo: Path) -> None:
        """Test stripping and restoring attribution from multiple commits."""
        # Create multiple commits with varying attribution
        messages = [
            "feat: first commit\n\nCo-Authored-By: Claude <noreply@anthropic.com>",
            "feat: second commit (no attribution)",
            "fix: third commit\n\nGenerated with [Claude Code](https://claude.ai/code)",
            "docs: fourth commit\n\nCo-Authored-By: Human <human@example.com>",
        ]

        original_shas = []
        for i, msg in enumerate(messages):
            sha = create_commit_with_message(git_repo, msg, f"file{i}.txt")
            original_shas.append(sha)

        # Preview changes first
        changes = preview_changes(git_repo, commit_count=4)
        # Should find 2 commits with Claude attribution (1st and 3rd)
        assert len(changes) == 2

        # Strip attribution
        result = strip_attribution_from_commits(git_repo, commit_count=4)
        assert result.success is True
        assert result.commits_rewritten == 2
        assert result.backup_ref is not None

        # Verify 4th commit still has human co-author
        messages_after = []
        for sha in ["HEAD~3", "HEAD~2", "HEAD~1", "HEAD"]:
            messages_after.append(get_commit_message(git_repo, sha))

        # First commit: Claude attribution should be removed
        assert "Claude" not in messages_after[0]
        # Second commit: unchanged (no attribution)
        assert "second commit" in messages_after[1]
        # Third commit: Generated line should be removed
        assert "Generated" not in messages_after[2]
        # Fourth commit: Human co-author should be preserved
        assert "Human" in messages_after[3]
        assert "human@example.com" in messages_after[3]

        # Restore and verify
        restore_result = restore_from_backup(git_repo, result.backup_ref)
        assert restore_result.success is True

        # Verify original attribution is back
        restored_message = get_commit_message(git_repo, "HEAD~3")
        assert "Claude" in restored_message


class TestStripPreservesHumanCoauthorsE2E:
    """Test that human co-authors are preserved correctly end-to-end."""

    def test_preserves_human_coauthors_by_default(self, git_repo: Path) -> None:
        """Test that human Co-Authored-By lines are preserved by default."""
        message = """feat: collaborative feature

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Alice <alice@example.com>
Co-Authored-By: Bob <bob@company.org>

Generated with [Claude Code](https://claude.ai/code)
"""
        create_commit_with_message(git_repo, message)

        # Strip with default settings (preserve humans)
        result = strip_attribution_from_commits(
            git_repo, commit_count=1, preserve_human_coauthors=True
        )
        assert result.success is True

        # Verify
        stripped_message = get_commit_message(git_repo)
        assert "Claude" not in stripped_message
        assert "anthropic.com" not in stripped_message
        assert "Generated with" not in stripped_message
        # Human co-authors preserved
        assert "Alice" in stripped_message
        assert "alice@example.com" in stripped_message
        assert "Bob" in stripped_message
        assert "bob@company.org" in stripped_message

    def test_strips_all_coauthors_when_configured(self, git_repo: Path) -> None:
        """Test that all co-authors are removed when preserve_human_coauthors=False."""
        message = """feat: collaborative feature

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Alice <alice@example.com>
"""
        create_commit_with_message(git_repo, message)

        # Strip with preserve_human_coauthors=False
        result = strip_attribution_from_commits(
            git_repo, commit_count=1, preserve_human_coauthors=False
        )
        assert result.success is True

        # Verify all co-authors removed
        stripped_message = get_commit_message(git_repo)
        assert "Co-Authored-By" not in stripped_message
        assert "Claude" not in stripped_message
        assert "Alice" not in stripped_message


class TestCommitMsgHookStripsAutomatically:
    """Test the commit-msg hook for automatic attribution stripping."""

    @pytest.mark.skipif(not COMMIT_MSG_HOOK.exists(), reason="Commit-msg hook script not found")
    def test_hook_strips_claude_attribution(self, git_repo: Path) -> None:
        """Test that the commit-msg hook strips Claude attribution automatically."""
        # Install the hook
        hooks_dir = git_repo / ".git" / "hooks"
        hooks_dir.mkdir(exist_ok=True)
        hook_path = hooks_dir / "commit-msg"

        # Copy hook content
        hook_path.write_text(COMMIT_MSG_HOOK.read_text())
        hook_path.chmod(0o755)

        # Create a commit message file with attribution
        commit_msg_file = git_repo / ".git" / "COMMIT_EDITMSG"
        commit_msg_file.write_text("""feat: add feature

Some description.

Co-Authored-By: Claude <noreply@anthropic.com>
""")

        # Run the hook manually
        env = os.environ.copy()
        env.pop("PERSUADE_KEEP_ATTRIBUTION", None)  # Ensure not set

        result = subprocess.run(
            ["bash", str(hook_path), str(commit_msg_file)],
            cwd=git_repo,
            capture_output=True,
            text=True,
            check=False,
            env=env,
            timeout=GIT_TIMEOUT_SECONDS,
        )

        # Hook should succeed
        assert result.returncode == 0, f"Hook failed: {result.stderr}"

        # Verify message was stripped
        final_message = commit_msg_file.read_text()
        assert "Claude" not in final_message
        assert "feat: add feature" in final_message

    @pytest.mark.skipif(not COMMIT_MSG_HOOK.exists(), reason="Commit-msg hook script not found")
    def test_hook_preserves_human_coauthors(self, git_repo: Path) -> None:
        """Test that the hook preserves human co-authors."""
        hooks_dir = git_repo / ".git" / "hooks"
        hooks_dir.mkdir(exist_ok=True)
        hook_path = hooks_dir / "commit-msg"
        hook_path.write_text(COMMIT_MSG_HOOK.read_text())
        hook_path.chmod(0o755)

        commit_msg_file = git_repo / ".git" / "COMMIT_EDITMSG"
        commit_msg_file.write_text("""feat: collaborative work

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Human <human@example.com>
""")

        env = os.environ.copy()
        env.pop("PERSUADE_KEEP_ATTRIBUTION", None)

        result = subprocess.run(
            ["bash", str(hook_path), str(commit_msg_file)],
            cwd=git_repo,
            capture_output=True,
            text=True,
            check=False,
            env=env,
            timeout=GIT_TIMEOUT_SECONDS,
        )

        assert result.returncode == 0

        final_message = commit_msg_file.read_text()
        assert "Claude" not in final_message
        # Human co-author should be preserved
        assert "Human" in final_message
        assert "human@example.com" in final_message

    @pytest.mark.skipif(not COMMIT_MSG_HOOK.exists(), reason="Commit-msg hook script not found")
    def test_hook_opt_out_with_env_var(self, git_repo: Path) -> None:
        """Test that PERSUADE_KEEP_ATTRIBUTION=1 skips stripping."""
        hooks_dir = git_repo / ".git" / "hooks"
        hooks_dir.mkdir(exist_ok=True)
        hook_path = hooks_dir / "commit-msg"
        hook_path.write_text(COMMIT_MSG_HOOK.read_text())
        hook_path.chmod(0o755)

        commit_msg_file = git_repo / ".git" / "COMMIT_EDITMSG"
        original_message = """feat: keep attribution

Co-Authored-By: Claude <noreply@anthropic.com>
"""
        commit_msg_file.write_text(original_message)

        # Set opt-out environment variable
        env = os.environ.copy()
        env["PERSUADE_KEEP_ATTRIBUTION"] = "1"

        result = subprocess.run(
            ["bash", str(hook_path), str(commit_msg_file)],
            cwd=git_repo,
            capture_output=True,
            text=True,
            check=False,
            env=env,
            timeout=GIT_TIMEOUT_SECONDS,
        )

        assert result.returncode == 0

        # Message should be unchanged
        final_message = commit_msg_file.read_text()
        assert "Claude" in final_message


class TestStripHandlesMergeCommits:
    """Test that merge commits are handled correctly."""

    def test_skip_merge_commits_in_history(self, git_repo: Path) -> None:
        """Test that stripping handles repos with merge commits gracefully."""
        # Create main branch with a commit
        create_commit_with_message(
            git_repo,
            "feat: main commit\n\nCo-Authored-By: Claude <noreply@anthropic.com>",
            "main.txt",
        )

        # Create and switch to feature branch
        subprocess.run(
            ["git", "checkout", "-b", "feature"],
            cwd=git_repo,
            capture_output=True,
            check=True,
        )
        create_commit_with_message(
            git_repo,
            "feat: feature commit\n\nGenerated with [Claude Code](https://claude.ai/code)",
            "feature.txt",
        )

        # Switch back to main and create another commit
        subprocess.run(
            ["git", "checkout", "master"],
            cwd=git_repo,
            capture_output=True,
            check=False,  # Might be 'main' instead
        )
        subprocess.run(
            ["git", "checkout", "main"],
            cwd=git_repo,
            capture_output=True,
            check=False,  # Might be 'master' instead
        )

        # Get the actual main branch name (verify we're on main)
        _ = subprocess.run(
            ["git", "branch", "--show-current"],
            cwd=git_repo,
            capture_output=True,
            text=True,
            check=True,
        )

        create_commit_with_message(git_repo, "feat: another main commit", "main2.txt")

        # Merge feature branch (creates merge commit)
        subprocess.run(
            ["git", "merge", "feature", "--no-edit"],
            cwd=git_repo,
            capture_output=True,
            check=True,
        )

        # Strip attribution from all commits
        result = strip_attribution_from_commits(git_repo, commit_count=5)

        # Should succeed (merge commits may or may not be rewritten depending on implementation)
        assert result.success is True


class TestCLIFullWorkflow:
    """Test the CLI commands for full workflow."""

    def test_cli_strip_dry_run_then_execute(self, git_repo: Path) -> None:
        """Test CLI workflow: dry-run preview then actual strip."""
        from persuade.cli import cli

        # Create commit with attribution
        create_commit_with_message(
            git_repo,
            "feat: add feature\n\nCo-Authored-By: Claude <noreply@anthropic.com>",
            "feature.py",
        )

        runner = CliRunner()

        # Step 1: Dry run to preview
        result = runner.invoke(
            cli,
            [
                "strip-attribution",
                "--commits",
                "1",
                "--dry-run",
                "--project-dir",
                str(git_repo),
            ],
        )
        assert result.exit_code == 0
        assert "Dry run" in result.output or "Found" in result.output

        # Verify commit unchanged after dry run
        message_after_dry_run = get_commit_message(git_repo)
        assert "Claude" in message_after_dry_run

        # Step 2: Actual strip (with simulated confirmation)
        result = runner.invoke(
            cli,
            [
                "strip-attribution",
                "--commits",
                "1",
                "--project-dir",
                str(git_repo),
            ],
            input="y\n",  # Confirm the operation
        )
        # Note: May fail if no TTY, but we test what we can
        # The important thing is the dry-run works correctly

    def test_cli_list_backups(self, git_repo: Path) -> None:
        """Test CLI list backups command."""
        from persuade.cli import cli

        # Create commit and backup
        create_commit_with_message(git_repo, "feat: test commit", "test.py")
        backup_ref = create_backup_ref(git_repo)
        assert backup_ref is not None

        runner = CliRunner()

        # List backups
        result = runner.invoke(
            cli,
            [
                "restore-attribution",
                "--list",
                "--project-dir",
                str(git_repo),
            ],
        )
        assert result.exit_code == 0
        assert "backup" in result.output.lower() or backup_ref in result.output


class TestBackupRefManagement:
    """Test backup ref creation and management."""

    def test_multiple_backups_are_tracked(self, git_repo: Path) -> None:
        """Test that multiple backup refs are created and listed correctly."""
        # Create initial commit
        create_commit_with_message(git_repo, "initial commit", "init.txt")

        # Create multiple backups
        backup_refs = []
        for i in range(3):
            ref = create_backup_ref(git_repo)
            assert ref is not None
            backup_refs.append(ref)
            # Add slight delay to ensure unique timestamps
            create_commit_with_message(git_repo, f"commit {i}", f"file{i}.txt")

        # List all backups
        refs = list_backup_refs(git_repo)
        ref_names = [r[0] for r in refs]

        # All created backups should be listed
        for backup_ref in backup_refs:
            assert backup_ref in ref_names

    def test_backup_ref_name_sanitization(self, git_repo: Path) -> None:
        """Test that branch names with special characters are sanitized in backup refs."""
        create_commit_with_message(git_repo, "initial", "init.txt")

        # Create branch with special characters
        subprocess.run(
            ["git", "checkout", "-b", "feature/user/awesome-feature"],
            cwd=git_repo,
            capture_output=True,
            check=True,
        )

        backup_ref = create_backup_ref(git_repo)
        assert backup_ref is not None

        # Branch name should be sanitized (/ -> -)
        assert "feature-user-awesome-feature" in backup_ref
        assert "/" not in backup_ref.split("/")[-2]  # Branch part should have no /


class TestEdgeCases:
    """Test edge cases and error handling."""

    def test_handles_empty_commit_messages(self, git_repo: Path) -> None:
        """Test handling of commits with minimal messages."""
        # Create commit with just subject line
        create_commit_with_message(git_repo, "fix bug", "fix.py")

        # Should handle gracefully (no attribution to strip)
        result = strip_attribution_from_commits(git_repo, commit_count=1)
        assert result.success is True
        assert result.commits_rewritten == 0

    def test_handles_unicode_in_messages(self, git_repo: Path) -> None:
        """Test handling of Unicode characters in commit messages."""
        message = """feat: add internationalization 🌍

Added support for Japanese (日本語) and Chinese (中文).

Co-Authored-By: Claude <noreply@anthropic.com>
"""
        create_commit_with_message(git_repo, message, "i18n.py")

        # Strip attribution
        result = strip_attribution_from_commits(git_repo, commit_count=1)
        assert result.success is True

        # Verify Unicode preserved
        stripped_message = get_commit_message(git_repo)
        assert "🌍" in stripped_message
        assert "日本語" in stripped_message
        assert "中文" in stripped_message
        assert "Claude" not in stripped_message

    def test_handles_very_long_commit_messages(self, git_repo: Path) -> None:
        """Test handling of very long commit messages."""
        # Create a very long commit message
        long_body = "This is a very detailed commit message. " * 100
        message = f"""feat: comprehensive feature

{long_body}

Co-Authored-By: Claude <noreply@anthropic.com>
"""
        create_commit_with_message(git_repo, message, "big.py")

        # Strip attribution
        result = strip_attribution_from_commits(git_repo, commit_count=1)
        assert result.success is True

        # Verify message preserved (minus attribution)
        stripped_message = get_commit_message(git_repo)
        assert "This is a very detailed commit message." in stripped_message
        assert "Claude" not in stripped_message
