"""
Unit tests for git attribution stripping tools.

Tests the git attribution module for removing Claude attribution from commit messages.
"""

import subprocess
from pathlib import Path

import pytest

from persuade.git_attribution import (
    BACKUP_REF_PREFIX,
    CommitInfo,
    StripResult,
    create_backup_ref,
    list_backup_refs,
    preview_changes,
    restore_from_backup,
    strip_attribution_from_commits,
    strip_claude_attribution,
)

from .conftest import check_git_available, init_git_repo

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


class TestStripClaudeAttribution:
    """Tests for strip_claude_attribution function."""

    def test_strips_claude_coauthor(self) -> None:
        """Test removes Claude Co-Authored-By line."""
        message = """feat: add new feature

Co-Authored-By: Claude <noreply@anthropic.com>
"""
        result = strip_claude_attribution(message)
        assert "Claude" not in result
        assert "anthropic" not in result

    def test_strips_anthropic_email_coauthor(self) -> None:
        """Test removes Co-Authored-By with anthropic.com email."""
        message = """fix: resolve bug

Co-Authored-By: Someone <someone@anthropic.com>
"""
        result = strip_claude_attribution(message)
        assert "anthropic.com" not in result

    def test_strips_claude_code_coauthor(self) -> None:
        """Test removes Claude Code Co-Authored-By line."""
        message = """docs: update readme

Co-Authored-By: Claude Code <noreply@anthropic.com>
"""
        result = strip_claude_attribution(message)
        assert "Claude Code" not in result

    def test_strips_generated_with_line(self) -> None:
        """Test removes Generated with Claude line."""
        message = """feat: add feature

Generated with [Claude Code](https://claude.ai/code)
"""
        result = strip_claude_attribution(message)
        assert "Generated with" not in result
        assert "claude.ai" not in result

    def test_preserves_human_coauthors_by_default(self) -> None:
        """Test preserves Co-Authored-By lines for human contributors."""
        message = """feat: add feature

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: John Doe <john@example.com>
"""
        result = strip_claude_attribution(message)
        assert "Claude" not in result
        assert "John Doe" in result
        assert "john@example.com" in result

    def test_strips_all_coauthors_when_configured(self) -> None:
        """Test removes all Co-Authored-By lines when preserve_human_coauthors=False."""
        message = """feat: add feature

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: John Doe <john@example.com>
"""
        result = strip_claude_attribution(message, preserve_human_coauthors=False)
        assert "Co-Authored-By" not in result
        assert "Claude" not in result
        assert "John Doe" not in result

    def test_handles_multiple_claude_patterns(self) -> None:
        """Test handles message with multiple Claude attribution patterns."""
        message = """feat: comprehensive feature

This is a great feature.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: claude-bot <bot@anthropic.com>

Generated with [Claude Code](https://claude.ai/code)
"""
        result = strip_claude_attribution(message)
        assert "Claude" not in result
        assert "anthropic" not in result
        assert "Generated with" not in result
        assert "This is a great feature." in result

    def test_handles_message_without_attribution(self) -> None:
        """Test handles message that has no Claude attribution."""
        message = """feat: add feature

This is a manual commit with no AI attribution.

Co-Authored-By: Human Developer <human@company.com>
"""
        result = strip_claude_attribution(message)
        assert "Human Developer" in result
        assert result.strip() == message.strip()

    def test_handles_empty_message(self) -> None:
        """Test handles empty commit message."""
        result = strip_claude_attribution("")
        assert result == ""

    def test_handles_single_line_message(self) -> None:
        """Test handles single line commit message."""
        message = "fix: quick bug fix"
        result = strip_claude_attribution(message)
        assert result.strip() == message

    def test_case_insensitive_coauthor(self) -> None:
        """Test Co-Authored-By matching is case insensitive."""
        message = """feat: test

co-authored-by: Claude <noreply@anthropic.com>
"""
        result = strip_claude_attribution(message)
        assert "Claude" not in result.lower()

    def test_preserves_commit_body(self) -> None:
        """Test preserves the body content of the commit message."""
        message = """feat: add feature

This is a detailed description of the changes.

- Added new functionality
- Fixed edge cases

Co-Authored-By: Claude <noreply@anthropic.com>
"""
        result = strip_claude_attribution(message)
        assert "detailed description" in result
        assert "Added new functionality" in result
        assert "Fixed edge cases" in result


class TestStripResult:
    """Tests for StripResult dataclass."""

    def test_default_values(self) -> None:
        """Test default values are set correctly."""
        result = StripResult(success=True, message="test")
        assert result.success is True
        assert result.message == "test"
        assert result.commits_rewritten == 0
        assert result.backup_ref is None
        assert result.error is None

    def test_all_fields(self) -> None:
        """Test all fields can be set."""
        result = StripResult(
            success=True,
            message="Success",
            commits_rewritten=5,
            backup_ref="refs/original/test",
            error=None,
        )
        assert result.commits_rewritten == 5
        assert result.backup_ref == "refs/original/test"


class TestCommitInfo:
    """Tests for CommitInfo dataclass."""

    def test_all_fields(self) -> None:
        """Test CommitInfo stores all required fields."""
        info = CommitInfo(
            sha="abc123",
            message="feat: test",
            author_name="Test Author",
            author_email="test@example.com",
            author_date="2024-01-01 12:00:00 +0000",
            committer_name="Test Committer",
            committer_email="committer@example.com",
            committer_date="2024-01-01 12:00:00 +0000",
        )
        assert info.sha == "abc123"
        assert info.message == "feat: test"
        assert info.author_name == "Test Author"


class TestCreateBackupRef:
    """Tests for create_backup_ref function."""

    def test_creates_backup_ref(self, tmp_path: Path) -> None:
        """Test creates backup ref successfully."""
        init_git_repo(tmp_path)
        (tmp_path / "README.md").write_text("test")
        subprocess.run(["git", "add", "."], cwd=tmp_path, capture_output=True, check=True)
        subprocess.run(
            ["git", "commit", "-m", "initial"],
            cwd=tmp_path,
            capture_output=True,
            check=True,
        )

        backup_ref = create_backup_ref(tmp_path)

        assert backup_ref is not None
        assert backup_ref.startswith(BACKUP_REF_PREFIX)

        # Verify ref exists
        result = subprocess.run(
            ["git", "rev-parse", "--verify", backup_ref],
            cwd=tmp_path,
            capture_output=True,
            check=False,
        )
        assert result.returncode == 0

    def test_creates_backup_with_branch_name(self, tmp_path: Path) -> None:
        """Test includes branch name in backup ref."""
        init_git_repo(tmp_path)
        (tmp_path / "README.md").write_text("test")
        subprocess.run(["git", "add", "."], cwd=tmp_path, capture_output=True, check=True)
        subprocess.run(
            ["git", "commit", "-m", "initial"],
            cwd=tmp_path,
            capture_output=True,
            check=True,
        )
        subprocess.run(
            ["git", "checkout", "-b", "feature/test"],
            cwd=tmp_path,
            capture_output=True,
            check=True,
        )

        backup_ref = create_backup_ref(tmp_path)

        assert backup_ref is not None
        # Branch name should be sanitized (/ replaced with -)
        assert "feature-test" in backup_ref


class TestListBackupRefs:
    """Tests for list_backup_refs function."""

    def test_lists_backup_refs(self, tmp_path: Path) -> None:
        """Test lists created backup refs."""
        init_git_repo(tmp_path)
        (tmp_path / "README.md").write_text("test")
        subprocess.run(["git", "add", "."], cwd=tmp_path, capture_output=True, check=True)
        subprocess.run(
            ["git", "commit", "-m", "initial"],
            cwd=tmp_path,
            capture_output=True,
            check=True,
        )

        # Create some backup refs
        backup_ref = create_backup_ref(tmp_path)
        assert backup_ref is not None

        refs = list_backup_refs(tmp_path)

        assert len(refs) >= 1
        ref_names = [r[0] for r in refs]
        assert backup_ref in ref_names

    def test_returns_empty_for_no_backups(self, tmp_path: Path) -> None:
        """Test returns empty list when no backups exist."""
        init_git_repo(tmp_path)

        refs = list_backup_refs(tmp_path)

        assert refs == []


class TestRestoreFromBackup:
    """Tests for restore_from_backup function."""

    def test_restores_from_backup(self, tmp_path: Path) -> None:
        """Test restores branch from backup ref."""
        init_git_repo(tmp_path)
        # Create initial commit
        (tmp_path / "README.md").write_text("initial content")
        subprocess.run(["git", "add", "."], cwd=tmp_path, capture_output=True, check=True)
        subprocess.run(
            ["git", "commit", "-m", "initial"],
            cwd=tmp_path,
            capture_output=True,
            check=True,
        )

        # Create backup
        backup_ref = create_backup_ref(tmp_path)
        assert backup_ref is not None

        # Make another commit
        (tmp_path / "README.md").write_text("modified content")
        subprocess.run(["git", "add", "."], cwd=tmp_path, capture_output=True, check=True)
        subprocess.run(
            ["git", "commit", "-m", "modify"],
            cwd=tmp_path,
            capture_output=True,
            check=True,
        )

        # Restore from backup
        result = restore_from_backup(tmp_path, backup_ref)

        assert result.success is True
        assert "initial content" in (tmp_path / "README.md").read_text()

    def test_fails_for_invalid_ref(self, tmp_path: Path) -> None:
        """Test fails gracefully for invalid backup ref."""
        init_git_repo(tmp_path)
        (tmp_path / "README.md").write_text("test")
        subprocess.run(["git", "add", "."], cwd=tmp_path, capture_output=True, check=True)
        subprocess.run(
            ["git", "commit", "-m", "initial"],
            cwd=tmp_path,
            capture_output=True,
            check=True,
        )

        result = restore_from_backup(tmp_path, "refs/invalid/backup")

        assert result.success is False
        assert "not found" in result.message.lower() or result.error is not None


class TestPreviewChanges:
    """Tests for preview_changes function."""

    def test_previews_changes_with_attribution(self, tmp_path: Path) -> None:
        """Test previews commits that have Claude attribution."""
        init_git_repo(tmp_path)

        # Create commit with attribution
        (tmp_path / "file1.txt").write_text("content")
        subprocess.run(["git", "add", "."], cwd=tmp_path, capture_output=True, check=True)
        commit_msg = "feat: add file\n\nCo-Authored-By: Claude <noreply@anthropic.com>"
        subprocess.run(
            ["git", "commit", "-m", commit_msg],
            cwd=tmp_path,
            capture_output=True,
            check=True,
        )

        changes = preview_changes(tmp_path, commit_count=1)

        assert len(changes) == 1
        sha, original, new = changes[0]
        assert "Claude" in original
        assert "Claude" not in new

    def test_returns_empty_for_no_attribution(self, tmp_path: Path) -> None:
        """Test returns empty list when no attribution found."""
        init_git_repo(tmp_path)

        # Create commit without attribution
        (tmp_path / "file1.txt").write_text("content")
        subprocess.run(["git", "add", "."], cwd=tmp_path, capture_output=True, check=True)
        subprocess.run(
            ["git", "commit", "-m", "feat: add file"],
            cwd=tmp_path,
            capture_output=True,
            check=True,
        )

        changes = preview_changes(tmp_path, commit_count=1)

        assert changes == []


class TestStripAttributionFromCommits:
    """Tests for strip_attribution_from_commits function."""

    def test_requires_commit_selection(self, tmp_path: Path) -> None:
        """Test requires at least one commit selection method."""
        init_git_repo(tmp_path)

        result = strip_attribution_from_commits(tmp_path)

        assert result.success is False
        assert "Must specify" in result.message

    def test_dry_run_does_not_modify(self, tmp_path: Path) -> None:
        """Test dry run previews without modifying."""
        init_git_repo(tmp_path)

        # Create commit with attribution
        (tmp_path / "file1.txt").write_text("content")
        subprocess.run(["git", "add", "."], cwd=tmp_path, capture_output=True, check=True)
        commit_msg = "feat: add file\n\nCo-Authored-By: Claude <noreply@anthropic.com>"
        subprocess.run(
            ["git", "commit", "-m", commit_msg],
            cwd=tmp_path,
            capture_output=True,
            check=True,
        )

        # Get original commit SHA
        result = subprocess.run(
            ["git", "rev-parse", "HEAD"],
            cwd=tmp_path,
            capture_output=True,
            text=True,
            check=True,
        )
        original_sha = result.stdout.strip()

        # Dry run
        strip_result = strip_attribution_from_commits(tmp_path, commit_count=1, dry_run=True)

        # Verify SHA unchanged
        result = subprocess.run(
            ["git", "rev-parse", "HEAD"],
            cwd=tmp_path,
            capture_output=True,
            text=True,
            check=True,
        )
        assert result.stdout.strip() == original_sha
        assert strip_result.success is True
        assert "Dry run" in strip_result.message

    def test_returns_no_changes_when_none_found(self, tmp_path: Path) -> None:
        """Test returns success with 0 commits when no attribution found."""
        init_git_repo(tmp_path)

        # Create commit without attribution
        (tmp_path / "file1.txt").write_text("content")
        subprocess.run(["git", "add", "."], cwd=tmp_path, capture_output=True, check=True)
        subprocess.run(
            ["git", "commit", "-m", "feat: add file"],
            cwd=tmp_path,
            capture_output=True,
            check=True,
        )

        result = strip_attribution_from_commits(tmp_path, commit_count=1)

        assert result.success is True
        assert result.commits_rewritten == 0


class TestCLIStripAttribution:
    """Tests for CLI strip-attribution command."""

    def test_requires_commit_selection(self) -> None:
        """Test CLI requires commit selection argument."""
        from click.testing import CliRunner

        from persuade.cli import cli

        runner = CliRunner()
        result = runner.invoke(cli, ["strip-attribution"])

        assert result.exit_code != 0
        assert "Must specify" in result.output

    def test_dry_run_option(self, tmp_path: Path) -> None:
        """Test CLI --dry-run option works."""
        from click.testing import CliRunner

        from persuade.cli import cli

        init_git_repo(tmp_path)
        (tmp_path / "file.txt").write_text("test")
        subprocess.run(["git", "add", "."], cwd=tmp_path, capture_output=True, check=True)
        subprocess.run(
            ["git", "commit", "-m", "test\n\nCo-Authored-By: Claude <noreply@anthropic.com>"],
            cwd=tmp_path,
            capture_output=True,
            check=True,
        )

        runner = CliRunner()
        result = runner.invoke(
            cli,
            ["strip-attribution", "--commits", "1", "--dry-run", "--project-dir", str(tmp_path)],
        )

        # The test verifies the command runs without error, output varies based on commits found
        assert result.exit_code == 0


class TestCLIRestoreAttribution:
    """Tests for CLI restore-attribution command."""

    def test_list_option(self, tmp_path: Path) -> None:
        """Test CLI --list option works."""
        from click.testing import CliRunner

        from persuade.cli import cli

        init_git_repo(tmp_path)
        (tmp_path / "file.txt").write_text("test")
        subprocess.run(["git", "add", "."], cwd=tmp_path, capture_output=True, check=True)
        subprocess.run(
            ["git", "commit", "-m", "test"],
            cwd=tmp_path,
            capture_output=True,
            check=True,
        )

        runner = CliRunner()
        result = runner.invoke(
            cli,
            ["restore-attribution", "--list", "--project-dir", str(tmp_path)],
        )

        # Should succeed even if no backups
        assert result.exit_code == 0

    def test_requires_backup_or_list(self) -> None:
        """Test CLI requires --backup or --list."""
        from click.testing import CliRunner

        from persuade.cli import cli

        runner = CliRunner()
        result = runner.invoke(cli, ["restore-attribution"])

        assert result.exit_code != 0
        assert "Must specify" in result.output
