"""
Unit tests for core sync orchestration.

Tests the sync_automation function, particularly error handling
for git workflow scenarios.
"""

import subprocess
from pathlib import Path
from unittest.mock import patch

import pytest

from persuade.core import SyncOptions, sync_automation

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")


def create_source_template(source_path: Path) -> None:
    """Create a minimal source template for testing."""
    claude_dir = source_path / ".claude"
    claude_dir.mkdir(parents=True)

    # Create settings.json
    settings = claude_dir / "settings.json"
    settings.write_text('{"allowedTools": ["Read", "Write"]}')

    # Create a simple file to sync
    test_file = claude_dir / "test.md"
    test_file.write_text("# Test file for sync")


def create_target_project(target_path: Path, gitignore_claude: bool = False) -> None:
    """Create a target project for testing.

    Args:
        target_path: Path to create project in
        gitignore_claude: If True, add .claude to .gitignore
    """
    init_git_repo(target_path)

    # Create .claude directory
    claude_dir = target_path / ".claude"
    claude_dir.mkdir(parents=True)

    # Create initial settings
    settings = claude_dir / "settings.json"
    settings.write_text('{"allowedTools": []}')

    # Add .gitignore if requested
    if gitignore_claude:
        gitignore = target_path / ".gitignore"
        gitignore.write_text(".claude\n")

    # Make initial commit so we have a valid git state
    subprocess.run(
        ["git", "add", "-A"],
        cwd=target_path,
        capture_output=True,
        check=True,
    )
    subprocess.run(
        ["git", "commit", "-m", "Initial commit"],
        cwd=target_path,
        capture_output=True,
        check=True,
    )


class TestSyncAutomationPRBlocking:
    """Tests for PR creation blocking when no commits created (Bug #156 fix)."""

    def test_returns_error_when_pr_requested_but_no_commits_created(self, tmp_path: Path) -> None:
        """Test that --pr flag returns error when no commits are created.

        This is the key integration test for Bug #156. When create_atomic_commits
        fails to create any commits, PR creation should be blocked with a clear
        error message instead of proceeding to "No commits between main and branch".
        """
        source_path = tmp_path / "source"
        target_path = tmp_path / "target"
        source_path.mkdir()
        target_path.mkdir()

        create_source_template(source_path)
        create_target_project(target_path, gitignore_claude=False)

        # Request PR creation with commits
        options = SyncOptions(
            git_commit=True,
            create_branch=True,
            create_pr=True,
            quiet=True,
        )

        # Mock create_atomic_commits to return failure with 0 commits
        from persuade.git_workflow import GitWorkflowResult

        failed_commit_result = GitWorkflowResult(
            success=False,
            message="Failed to create commits",
            commits_created=0,
            error="Files ignored by .gitignore",
        )

        with patch("persuade.core.create_atomic_commits", return_value=failed_commit_result):
            result = sync_automation(source_path, target_path, options)

        # Should return error status, not success
        assert result.status == "error", f"Expected 'error' status, got '{result.status}'"
        assert "no commits were created" in result.message.lower()
        assert result.git_workflow_result is not None
        assert result.git_workflow_result.commits_created == 0

    def test_pr_creation_succeeds_when_commits_created(self, tmp_path: Path) -> None:
        """Test PR creation proceeds when commits are successfully created."""
        source_path = tmp_path / "source"
        target_path = tmp_path / "target"
        source_path.mkdir()
        target_path.mkdir()

        create_source_template(source_path)
        create_target_project(target_path, gitignore_claude=False)

        options = SyncOptions(
            git_commit=True,
            create_branch=True,
            create_pr=True,
            quiet=True,
        )

        # Mock the PR creation since we don't have a real remote
        with (
            patch("persuade.core.check_gh_available", return_value=True),
            patch("persuade.core.push_branch") as mock_push,
            patch("persuade.core.create_pull_request") as mock_pr,
        ):
            # Configure mocks
            mock_push.return_value = type(
                "PushResult", (), {"success": True, "error": None, "message": "Pushed"}
            )()
            mock_pr.return_value = type(
                "PRResult",
                (),
                {"success": True, "error": None, "pr_url": "https://github.com/test/pr/1"},
            )()

            result = sync_automation(source_path, target_path, options)

        # Should succeed and have commits
        assert result.status == "success", (
            f"Expected 'success', got '{result.status}': {result.message}"
        )
        assert result.git_workflow_result is not None
        assert result.git_workflow_result.commits_created > 0
        assert result.git_workflow_result.pr_url == "https://github.com/test/pr/1"

    def test_returns_error_when_pr_creation_fails(self, tmp_path: Path) -> None:
        """Test error status when PR creation fails.

        This tests the error path in sync_automation's PR creation block
        where PR creation fails after commits were successfully created.
        """
        source_path = tmp_path / "source"
        target_path = tmp_path / "target"
        source_path.mkdir()
        target_path.mkdir()

        create_source_template(source_path)
        create_target_project(target_path, gitignore_claude=False)

        options = SyncOptions(
            git_commit=True,
            create_branch=True,
            create_pr=True,
            quiet=True,
        )

        # Mock PR creation to fail
        with (
            patch("persuade.core.check_gh_available", return_value=True),
            patch("persuade.core.push_branch") as mock_push,
            patch("persuade.core.create_pull_request") as mock_pr,
        ):
            mock_push.return_value = type(
                "PushResult", (), {"success": True, "error": None, "message": "Pushed"}
            )()
            # PR creation fails
            mock_pr.return_value = type(
                "PRResult",
                (),
                {"success": False, "error": "Repository not found", "pr_url": None},
            )()

            result = sync_automation(source_path, target_path, options)

        # Should return error status
        assert result.status == "error", (
            f"Expected 'error', got '{result.status}': {result.message}"
        )
        assert "PR creation failed" in result.message
        assert result.git_workflow_result is not None
        assert result.git_workflow_result.commits_created > 0  # Commits were created

    def test_returns_error_when_push_fails(self, tmp_path: Path) -> None:
        """Test error status when git push fails."""
        source_path = tmp_path / "source"
        target_path = tmp_path / "target"
        source_path.mkdir()
        target_path.mkdir()

        create_source_template(source_path)
        create_target_project(target_path, gitignore_claude=False)

        options = SyncOptions(
            git_commit=True,
            create_branch=True,
            create_pr=True,
            quiet=True,
        )

        # Mock push to fail
        with (
            patch("persuade.core.check_gh_available", return_value=True),
            patch("persuade.core.push_branch") as mock_push,
        ):
            mock_push.return_value = type(
                "PushResult",
                (),
                {"success": False, "error": "No remote configured", "message": "Failed"},
            )()

            result = sync_automation(source_path, target_path, options)

        # Should return error status
        assert result.status == "error"
        assert result.git_workflow_result is not None


class TestSyncAutomationPartialStatus:
    """Tests for partial status when git workflow fails (Bug #156/157 related)."""

    def test_returns_partial_when_files_synced_but_commits_fail(self, tmp_path: Path) -> None:
        """Test partial status when files sync but git workflow fails.

        When sync succeeds but git commit fails (e.g., due to gitignored files),
        the result should be 'partial' not 'success' or 'error'.

        Note: When create_branch=True, the branch creation's success=True is used.
        The partial status is only triggered when create_branch=False and
        create_atomic_commits is the sole git workflow result.
        """
        source_path = tmp_path / "source"
        target_path = tmp_path / "target"
        source_path.mkdir()
        target_path.mkdir()

        create_source_template(source_path)
        create_target_project(target_path, gitignore_claude=False)

        # Request commits without branch creation (to hit the partial status path)
        # When create_branch=False, git_workflow_result comes from create_atomic_commits
        options = SyncOptions(
            git_commit=True,
            create_branch=False,  # No branch - commit result is the workflow result
            create_pr=False,
            quiet=True,
        )

        # Mock create_atomic_commits to return failure with 0 commits
        from persuade.git_workflow import GitWorkflowResult

        failed_commit_result = GitWorkflowResult(
            success=False,
            message="Failed to create commits",
            commits_created=0,
            error="Files ignored by .gitignore",
        )

        with patch("persuade.core.create_atomic_commits", return_value=failed_commit_result):
            result = sync_automation(source_path, target_path, options)

        # Should return partial status
        assert result.status == "partial", f"Expected 'partial' status, got '{result.status}'"
        assert "git workflow failed" in result.message.lower()
        assert result.git_workflow_result is not None
        assert result.git_workflow_result.commits_created == 0
        assert result.git_workflow_result.success is False

    def test_returns_success_when_no_git_commit_requested(self, tmp_path: Path) -> None:
        """Test that sync without --commit flag returns success even with gitignore."""
        source_path = tmp_path / "source"
        target_path = tmp_path / "target"
        source_path.mkdir()
        target_path.mkdir()

        create_source_template(source_path)
        create_target_project(target_path, gitignore_claude=True)

        # No git operations requested
        options = SyncOptions(
            git_commit=False,
            create_branch=False,
            create_pr=False,
            quiet=True,
        )

        result = sync_automation(source_path, target_path, options)

        # Should succeed since we're not doing git operations
        assert result.status == "success", f"Expected 'success', got '{result.status}'"
        assert result.git_workflow_result is None  # No git workflow was requested

    def test_returns_success_when_commits_created_successfully(self, tmp_path: Path) -> None:
        """Test full success when commits are created."""
        source_path = tmp_path / "source"
        target_path = tmp_path / "target"
        source_path.mkdir()
        target_path.mkdir()

        create_source_template(source_path)
        create_target_project(target_path, gitignore_claude=False)

        options = SyncOptions(
            git_commit=True,
            create_branch=True,
            create_pr=False,
            quiet=True,
        )

        result = sync_automation(source_path, target_path, options)

        # Should succeed
        assert result.status == "success", f"Expected 'success', got '{result.status}'"
        assert result.git_workflow_result is not None
        assert result.git_workflow_result.commits_created > 0
        assert result.git_workflow_result.success is True


class TestIsGitIgnoredLogging:
    """Tests for is_git_ignored logging behavior."""

    def test_logs_warning_on_timeout(
        self, tmp_path: Path, caplog: pytest.LogCaptureFixture
    ) -> None:
        """Test that timeout is logged at WARNING level."""
        from persuade.git_ops import is_git_ignored

        init_git_repo(tmp_path)
        test_file = tmp_path / "test.txt"
        test_file.write_text("test")

        # Patch subprocess to raise TimeoutExpired
        with patch("persuade.git_ops.subprocess.run") as mock_run:
            mock_run.side_effect = subprocess.TimeoutExpired(cmd="git", timeout=5)

            # Enable logging capture
            import logging

            with caplog.at_level(logging.WARNING):
                result = is_git_ignored(Path("test.txt"), tmp_path)

            # Should return False (fallback) and log warning
            assert result is False
            assert "git_check_ignore_timeout" in caplog.text

    def test_logs_warning_when_git_not_found(
        self, tmp_path: Path, caplog: pytest.LogCaptureFixture
    ) -> None:
        """Test that git not found is logged at WARNING level."""
        from persuade.git_ops import is_git_ignored

        init_git_repo(tmp_path)
        test_file = tmp_path / "test.txt"
        test_file.write_text("test")

        # Patch subprocess to raise FileNotFoundError
        with patch("persuade.git_ops.subprocess.run") as mock_run:
            mock_run.side_effect = FileNotFoundError("git not found")

            import logging

            with caplog.at_level(logging.WARNING):
                result = is_git_ignored(Path("test.txt"), tmp_path)

            # Should return False (fallback) and log warning
            assert result is False
            assert "git_command_not_found" in caplog.text
