"""
Unit tests for git workflow operations.

Tests the git workflow functions for atomic commits, branch management, and PR creation.
"""

import subprocess
from pathlib import Path

import pytest

from persuade.commit_categorization import CommitGroup
from persuade.git_ops import check_ignored_synced_files, is_git_ignored
from persuade.git_pr import (
    check_gh_available,
    generate_pr_body,
)
from persuade.git_workflow import (
    GitWorkflowResult,
    create_atomic_commits,
    create_sync_branch,
    get_current_branch,
    get_default_branch,
    is_git_repository,
    is_on_default_branch,
)

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 TestIsGitRepository:
    """Tests for is_git_repository function."""

    def test_returns_true_for_git_repo(self, tmp_path: Path) -> None:
        """Test returns True for a git repository."""
        init_git_repo(tmp_path)
        assert is_git_repository(tmp_path) is True

    def test_returns_false_for_non_git_dir(self, tmp_path: Path) -> None:
        """Test returns False for a non-git directory."""
        assert is_git_repository(tmp_path) is False


class TestGetCurrentBranch:
    """Tests for get_current_branch function."""

    def test_returns_branch_name(self, tmp_path: Path) -> None:
        """Test returns current branch name."""
        init_git_repo(tmp_path)
        # Create initial commit to have a branch
        (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,
        )

        branch = get_current_branch(tmp_path)
        assert branch in ["main", "master"]

    def test_returns_none_for_non_git_dir(self, tmp_path: Path) -> None:
        """Test returns None for non-git directory."""
        branch = get_current_branch(tmp_path)
        assert branch is None


class TestGetDefaultBranch:
    """Tests for get_default_branch function."""

    def test_returns_main_or_master(self, tmp_path: Path) -> None:
        """Test returns 'main' or 'master' as default branch."""
        init_git_repo(tmp_path)
        # Create initial commit
        (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,
        )

        default = get_default_branch(tmp_path)
        assert default in ["main", "master"]


class TestIsOnDefaultBranch:
    """Tests for is_on_default_branch function."""

    def test_returns_true_on_default_branch(self, tmp_path: Path) -> None:
        """Test returns True when on default branch."""
        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,
        )

        assert is_on_default_branch(tmp_path) is True

    def test_returns_false_on_feature_branch(self, tmp_path: Path) -> None:
        """Test returns False when on a feature branch."""
        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-branch"],
            cwd=tmp_path,
            capture_output=True,
            check=True,
        )

        assert is_on_default_branch(tmp_path) is False


class TestIsGitIgnored:
    """Tests for is_git_ignored function (Bug #156 fix)."""

    def test_detects_directly_ignored_file(self, tmp_path: Path) -> None:
        """Test detects a file that is directly ignored."""
        init_git_repo(tmp_path)

        # Create .gitignore that ignores *.log files
        (tmp_path / ".gitignore").write_text("*.log\n")
        (tmp_path / "test.log").write_text("log content")

        assert is_git_ignored(Path("test.log"), tmp_path) is True

    def test_detects_parent_directory_ignored(self, tmp_path: Path) -> None:
        """Test detects file when parent directory is ignored (Bug #156 fix).

        This is the key fix for Bug #156. When `.gitignore` contains `.claude`
        (without trailing slash), `git check-ignore .claude/settings.json`
        returns "not ignored", but the file IS effectively ignored because
        the parent directory is ignored.
        """
        init_git_repo(tmp_path)

        # Create .gitignore that ignores .claude (WITHOUT trailing slash)
        (tmp_path / ".gitignore").write_text(".claude\n")

        # Create the file
        claude_dir = tmp_path / ".claude"
        claude_dir.mkdir()
        (claude_dir / "settings.json").write_text("{}")

        # This MUST return True - the file is effectively ignored because
        # the parent directory is ignored
        assert is_git_ignored(Path(".claude/settings.json"), tmp_path) is True

    def test_detects_deeply_nested_file_with_ignored_parent(self, tmp_path: Path) -> None:
        """Test detects deeply nested file when ancestor is ignored."""
        init_git_repo(tmp_path)

        # Create .gitignore that ignores .claude
        (tmp_path / ".gitignore").write_text(".claude\n")

        # Create deeply nested file
        deep_dir = tmp_path / ".claude" / "knowledge" / "git"
        deep_dir.mkdir(parents=True)
        (deep_dir / "README.md").write_text("content")

        # This MUST return True
        assert is_git_ignored(Path(".claude/knowledge/git/README.md"), tmp_path) is True

    def test_returns_false_for_non_ignored_file(self, tmp_path: Path) -> None:
        """Test returns False for a file that is not ignored."""
        init_git_repo(tmp_path)

        (tmp_path / "README.md").write_text("readme")

        assert is_git_ignored(Path("README.md"), tmp_path) is False

    def test_detects_parent_directory_ignored_with_absolute_path(self, tmp_path: Path) -> None:
        """Test handles absolute paths correctly when checking parent directories.

        This verifies the code path at git_ops.py:100-107 that converts absolute
        paths to relative paths before checking parent directories.
        """
        init_git_repo(tmp_path)

        # Create .gitignore that ignores .claude (without trailing slash)
        (tmp_path / ".gitignore").write_text(".claude\n")

        # Create the file
        claude_dir = tmp_path / ".claude"
        claude_dir.mkdir()
        (claude_dir / "settings.json").write_text("{}")

        # Pass ABSOLUTE path instead of relative
        absolute_path = (tmp_path / ".claude" / "settings.json").resolve()
        assert is_git_ignored(absolute_path, tmp_path) is True

    def test_handles_absolute_path_outside_repo(self, tmp_path: Path) -> None:
        """Test handles absolute paths that are not under repo root."""
        init_git_repo(tmp_path)

        # Create a path outside the repo
        outside_path = Path("/tmp/outside/file.txt")

        # Should return False (not ignored) without crashing
        result = is_git_ignored(outside_path, tmp_path)
        assert result is False


class TestCheckIgnoredSyncedFiles:
    """Tests for check_ignored_synced_files function."""

    def test_detects_gitignored_files(self, tmp_path: Path) -> None:
        """Test detection of gitignored synced files."""
        init_git_repo(tmp_path)

        # Create .gitignore that ignores .claude/knowledge/
        gitignore = tmp_path / ".gitignore"
        gitignore.write_text(".claude/knowledge/\n")

        # Create the file
        knowledge_dir = tmp_path / ".claude" / "knowledge"
        knowledge_dir.mkdir(parents=True)
        (knowledge_dir / "test.md").write_text("test")

        # Check if it's detected as ignored
        ignored = check_ignored_synced_files(tmp_path, [".claude/knowledge/test.md"])

        assert ".claude/knowledge/test.md" in ignored

    def test_returns_empty_for_tracked_files(self, tmp_path: Path) -> None:
        """Test returns empty list for files not gitignored."""
        init_git_repo(tmp_path)

        # Create a file without gitignore
        (tmp_path / "README.md").write_text("test")

        ignored = check_ignored_synced_files(tmp_path, ["README.md"])
        assert ignored == []

    def test_skips_nonexistent_files(self, tmp_path: Path) -> None:
        """Test skips files that don't exist."""
        init_git_repo(tmp_path)

        ignored = check_ignored_synced_files(tmp_path, ["nonexistent.md"])
        assert ignored == []

    def test_detects_parent_directory_gitignored(self, tmp_path: Path) -> None:
        """Test detection when parent directory is gitignored (Bug #156 fix).

        This is the key test for the bug fix. When .gitignore contains ".claude"
        (without trailing slash), `git check-ignore .claude/settings.json` returns
        "not ignored", but `git add .claude/settings.json` fails because the
        parent directory is ignored.
        """
        init_git_repo(tmp_path)

        # Create .gitignore that ignores .claude (without trailing slash)
        gitignore = tmp_path / ".gitignore"
        gitignore.write_text(".claude\n")  # No trailing slash!

        # Create the file
        claude_dir = tmp_path / ".claude"
        claude_dir.mkdir(parents=True)
        (claude_dir / "settings.json").write_text("{}")

        # Check if it's detected as ignored - this MUST return the file
        # even though git check-ignore on the file itself returns "not ignored"
        ignored = check_ignored_synced_files(tmp_path, [".claude/settings.json"])

        assert ".claude/settings.json" in ignored


class TestCreateSyncBranch:
    """Tests for create_sync_branch function."""

    def test_creates_branch(self, tmp_path: Path) -> None:
        """Test creates a new branch 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,
        )

        result = create_sync_branch(tmp_path, "test-branch")

        assert result.success is True
        assert result.branch_name == "test-branch"

        # Verify we're on the new branch
        current = get_current_branch(tmp_path)
        assert current == "test-branch"

    def test_auto_generates_branch_name(self, tmp_path: Path) -> None:
        """Test auto-generates branch name if not provided."""
        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 = create_sync_branch(tmp_path)

        assert result.success is True
        assert result.branch_name is not None
        assert "persuade/sync-" in result.branch_name

    def test_adds_suffix_for_existing_branch(self, tmp_path: Path) -> None:
        """Test adds suffix when branch already exists."""
        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 first branch
        result1 = create_sync_branch(tmp_path, "test-branch")
        assert result1.branch_name == "test-branch"

        # Go back to main
        subprocess.run(
            ["git", "checkout", "-"],
            cwd=tmp_path,
            capture_output=True,
            check=True,
        )

        # Create second branch with same name
        result2 = create_sync_branch(tmp_path, "test-branch")
        assert result2.success is True
        assert result2.branch_name == "test-branch-1"


class TestCreateAtomicCommits:
    """Tests for create_atomic_commits function."""

    def test_creates_commits(self, tmp_path: Path) -> None:
        """Test creates atomic commits from groups."""
        init_git_repo(tmp_path)
        (tmp_path / "README.md").write_text("initial")
        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 files to commit
        (tmp_path / "doc1.md").write_text("doc1")
        (tmp_path / "config.yml").write_text("config")

        groups = [
            CommitGroup("docs", "add documentation", files=["doc1.md"]),
            CommitGroup("chore", "add config", files=["config.yml"]),
        ]

        result = create_atomic_commits(tmp_path, groups)

        assert result.success is True
        assert result.commits_created == 2

    def test_empty_groups_no_error(self, tmp_path: Path) -> None:
        """Test handles empty groups list without error."""
        init_git_repo(tmp_path)

        result = create_atomic_commits(tmp_path, [])

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

    def test_force_add_for_gitignored_files(self, tmp_path: Path) -> None:
        """Test force-add flag works for gitignored files."""
        init_git_repo(tmp_path)
        (tmp_path / "README.md").write_text("initial")
        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 gitignore that ignores .claude/
        (tmp_path / ".gitignore").write_text(".claude/\n")
        subprocess.run(["git", "add", "."], cwd=tmp_path, capture_output=True, check=True)
        subprocess.run(
            ["git", "commit", "-m", "add gitignore"],
            cwd=tmp_path,
            capture_output=True,
            check=True,
        )

        # Create ignored file
        claude_dir = tmp_path / ".claude"
        claude_dir.mkdir()
        (claude_dir / "test.md").write_text("test")

        groups = [
            CommitGroup("docs", "add claude config", files=[".claude/test.md"]),
        ]

        result = create_atomic_commits(tmp_path, groups, force_add=True)

        assert result.success is True
        assert result.commits_created == 1

    def test_returns_failure_when_all_commits_fail(self, tmp_path: Path) -> None:
        """Test returns failure when no commits are created (Bug #156 fix).

        Previously, create_atomic_commits returned success=True even when
        all commits failed (e.g., due to gitignore issues). Now it correctly
        returns success=False with error details.
        """
        init_git_repo(tmp_path)
        (tmp_path / "README.md").write_text("initial")
        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 gitignore that ignores .claude/
        (tmp_path / ".gitignore").write_text(".claude/\n")
        subprocess.run(["git", "add", "."], cwd=tmp_path, capture_output=True, check=True)
        subprocess.run(
            ["git", "commit", "-m", "add gitignore"],
            cwd=tmp_path,
            capture_output=True,
            check=True,
        )

        # Create ignored file
        claude_dir = tmp_path / ".claude"
        claude_dir.mkdir()
        (claude_dir / "test.md").write_text("test")

        groups = [
            CommitGroup("docs", "add claude config", files=[".claude/test.md"]),
        ]

        # Without force_add, this should fail because the file is ignored
        result = create_atomic_commits(tmp_path, groups, force_add=False)

        assert result.success is False
        assert result.commits_created == 0
        assert result.error is not None
        assert "ignored" in result.error.lower()

    def test_returns_partial_success_with_some_failures(self, tmp_path: Path) -> None:
        """Test returns success=True when some commits succeed (Bug #156 fix).

        When some commits fail but others succeed, success=True but error
        field contains details about the failures.
        """
        init_git_repo(tmp_path)
        (tmp_path / "README.md").write_text("initial")
        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 gitignore that ignores .claude/
        (tmp_path / ".gitignore").write_text(".claude/\n")
        subprocess.run(["git", "add", "."], cwd=tmp_path, capture_output=True, check=True)
        subprocess.run(
            ["git", "commit", "-m", "add gitignore"],
            cwd=tmp_path,
            capture_output=True,
            check=True,
        )

        # Create one ignored file and one not ignored
        claude_dir = tmp_path / ".claude"
        claude_dir.mkdir()
        (claude_dir / "test.md").write_text("test")
        (tmp_path / "allowed.md").write_text("allowed content")

        groups = [
            CommitGroup("docs", "add claude config", files=[".claude/test.md"]),
            CommitGroup("docs", "add allowed file", files=["allowed.md"]),
        ]

        # Without force_add, first commit should fail, second should succeed
        result = create_atomic_commits(tmp_path, groups, force_add=False)

        assert result.success is True  # At least one commit succeeded
        assert result.commits_created == 1
        assert result.error is not None  # But there were failures


class TestCheckGhAvailable:
    """Tests for check_gh_available function."""

    def test_returns_boolean(self) -> None:
        """Test returns a boolean value."""
        result = check_gh_available()
        assert isinstance(result, bool)


class TestGeneratePrBody:
    """Tests for generate_pr_body function."""

    def test_basic_body(self) -> None:
        """Test basic PR body generation."""
        groups = [
            CommitGroup("docs", "add git knowledge base", files=["f1.md", "f2.md"]),
        ]
        body = generate_pr_body(groups)

        assert "## Summary" in body
        assert "persuade" in body.lower()
        assert "2 files" in body

    def test_includes_ignored_files_note(self) -> None:
        """Test includes note about force-added files."""
        groups = [
            CommitGroup("docs", "add docs", files=["f1.md"]),
        ]
        body = generate_pr_body(groups, ignored_files_count=5)

        assert "5 files" in body
        assert "gitignored" in body.lower()

    def test_multiple_groups(self) -> None:
        """Test body with multiple commit groups."""
        groups = [
            CommitGroup("docs", "add docs", files=["d1.md"]),
            CommitGroup("ci", "add workflows", files=["w1.yml", "w2.yml"]),
            CommitGroup("chore", "add config", files=["c1.yml"]),
        ]
        body = generate_pr_body(groups)

        assert "docs" in body.lower() or "Added" in body
        assert "workflow" in body.lower() or "Added" in body
        assert "config" in body.lower() or "Added" in body


class TestGitWorkflowResult:
    """Tests for GitWorkflowResult dataclass."""

    def test_default_values(self) -> None:
        """Test default values are set correctly."""
        result = GitWorkflowResult(success=True, message="test")

        assert result.success is True
        assert result.message == "test"
        assert result.branch_name is None
        assert result.commits_created == 0
        assert result.pr_url is None
        assert result.error is None

    def test_all_fields(self) -> None:
        """Test all fields can be set."""
        result = GitWorkflowResult(
            success=True,
            message="Success",
            branch_name="test-branch",
            commits_created=3,
            pr_url="https://github.com/org/repo/pull/123",
            error=None,
        )

        assert result.branch_name == "test-branch"
        assert result.commits_created == 3
        assert result.pr_url == "https://github.com/org/repo/pull/123"
