"""Tests for workflow client."""

import json
import subprocess
from unittest.mock import MagicMock, patch

import pytest

from aieng_bot.auto_merger.models import PRQueueItem, PRStatus
from aieng_bot.auto_merger.workflow_client import WorkflowClient


@pytest.fixture
def workflow_client():
    """Create a WorkflowClient instance."""
    return WorkflowClient(gh_token="test-token")


@pytest.fixture
def sample_pr():
    """Sample Dependabot PR for testing."""
    return PRQueueItem(
        repo="VectorInstitute/test-repo",
        pr_number=123,
        pr_title="Bump dependency",
        pr_author="app/dependabot",
        pr_url="https://github.com/VectorInstitute/test-repo/pull/123",
        status=PRStatus.PENDING,
        queued_at="2025-01-15T10:00:00Z",
        last_updated="2025-01-15T10:00:00Z",
    )


@pytest.fixture
def precommit_pr():
    """Sample pre-commit.ci PR for testing."""
    return PRQueueItem(
        repo="VectorInstitute/test-repo",
        pr_number=456,
        pr_title="[pre-commit.ci] pre-commit autoupdate",
        pr_author="app/pre-commit-ci",
        pr_url="https://github.com/VectorInstitute/test-repo/pull/456",
        status=PRStatus.PENDING,
        queued_at="2025-01-15T10:00:00Z",
        last_updated="2025-01-15T10:00:00Z",
    )


class TestWorkflowClient:
    """Test suite for WorkflowClient."""

    def test_init(self):
        """Test WorkflowClient initialization."""
        client = WorkflowClient(gh_token="token", bot_repo="VectorInstitute/bot")
        assert client.gh_token == "token"
        assert client.bot_repo == "VectorInstitute/bot"

    @patch("subprocess.run")
    def test_check_latest_comment_success(self, mock_run, workflow_client, sample_pr):
        """Test checking latest comment from dependabot."""
        mock_run.return_value = MagicMock(
            returncode=0,
            stdout="Looks like this PR is already up-to-date with main!\n",
        )

        result = workflow_client.check_latest_comment(sample_pr)

        assert "already up-to-date" in result
        mock_run.assert_called_once()
        # Verify the gh command structure
        call_args = mock_run.call_args[0][0]
        assert call_args[0] == "gh"
        assert call_args[1] == "pr"
        assert call_args[2] == "view"
        assert str(sample_pr.pr_number) in call_args
        assert "--json" in call_args
        assert "comments" in call_args

    @patch("subprocess.run")
    def test_check_latest_comment_no_comments(
        self, mock_run, workflow_client, sample_pr
    ):
        """Test checking comments when none exist."""
        mock_run.return_value = MagicMock(returncode=0, stdout="")

        result = workflow_client.check_latest_comment(sample_pr)

        assert result == ""

    @patch("subprocess.run")
    def test_check_latest_comment_failure(self, mock_run, workflow_client, sample_pr):
        """Test checking comments when command fails."""
        mock_run.side_effect = subprocess.CalledProcessError(1, ["gh"])

        result = workflow_client.check_latest_comment(sample_pr)

        assert result == ""

    @patch("subprocess.run")
    def test_check_latest_comment_custom_author(
        self, mock_run, workflow_client, sample_pr
    ):
        """Test checking latest comment from custom author."""
        mock_run.return_value = MagicMock(returncode=0, stdout="Some comment\n")

        result = workflow_client.check_latest_comment(sample_pr, author="custom-bot")

        assert result == "Some comment"
        # Verify custom author was used in jq filter
        call_args = mock_run.call_args[0][0]
        jq_filter = call_args[-1]
        assert "custom-bot" in jq_filter

    @patch("subprocess.run")
    def test_check_latest_comment_infer_dependabot(
        self, mock_run, workflow_client, sample_pr
    ):
        """Test checking latest comment infers dependabot author."""
        mock_run.return_value = MagicMock(returncode=0, stdout="Dependabot comment\n")

        result = workflow_client.check_latest_comment(sample_pr)

        assert result == "Dependabot comment"
        # Verify dependabot was inferred from pr_author
        call_args = mock_run.call_args[0][0]
        jq_filter = call_args[-1]
        assert "dependabot" in jq_filter

    @patch("subprocess.run")
    def test_check_latest_comment_infer_precommit(
        self, mock_run, workflow_client, precommit_pr
    ):
        """Test checking latest comment infers pre-commit.ci author."""
        mock_run.return_value = MagicMock(returncode=0, stdout="Pre-commit comment\n")

        result = workflow_client.check_latest_comment(precommit_pr)

        assert result == "Pre-commit comment"
        # Verify pre-commit-ci[bot] was inferred from pr_author
        call_args = mock_run.call_args[0][0]
        jq_filter = call_args[-1]
        assert "pre-commit-ci[bot]" in jq_filter

    @patch("subprocess.run")
    def test_get_pr_head_sha_success(self, mock_run, workflow_client, sample_pr):
        """Test getting PR head SHA successfully."""
        mock_run.return_value = MagicMock(
            returncode=0, stdout="abc123def456abc123def456abc123def456abc1\n"
        )

        result = workflow_client.get_pr_head_sha(sample_pr)

        assert result == "abc123def456abc123def456abc123def456abc1"
        mock_run.assert_called_once()
        call_args = mock_run.call_args[0][0]
        assert call_args[0] == "gh"
        assert call_args[1] == "pr"
        assert call_args[2] == "view"
        assert str(sample_pr.pr_number) in call_args
        assert "--json" in call_args
        assert "headRefOid" in call_args

    @patch("subprocess.run")
    def test_get_pr_head_sha_failure(self, mock_run, workflow_client, sample_pr):
        """Test getting PR head SHA when command fails."""
        mock_run.side_effect = subprocess.CalledProcessError(1, ["gh"])

        result = workflow_client.get_pr_head_sha(sample_pr)

        assert result is None

    @patch("subprocess.run")
    def test_trigger_rebase_dependabot_success(
        self, mock_run, workflow_client, sample_pr
    ):
        """Test successful rebase triggering for Dependabot PR."""
        mock_run.return_value = MagicMock(returncode=0, stdout="")

        success, new_sha, sha_changed = workflow_client.trigger_rebase(sample_pr)

        assert success is True
        assert new_sha is None  # Async rebase, no immediate SHA
        assert sha_changed is True  # Will poll for SHA change
        mock_run.assert_called_once()
        call_args = mock_run.call_args[0][0]
        assert "gh" in call_args
        assert "pr" in call_args
        assert "comment" in call_args
        assert "@dependabot rebase" in call_args

    @patch("subprocess.run")
    def test_trigger_rebase_dependabot_failure(
        self, mock_run, workflow_client, sample_pr
    ):
        """Test rebase triggering failure for Dependabot PR."""
        mock_run.side_effect = subprocess.CalledProcessError(1, "gh")

        success, new_sha, sha_changed = workflow_client.trigger_rebase(sample_pr)

        assert success is False
        assert new_sha is None
        assert sha_changed is False

    @patch("subprocess.run")
    def test_trigger_rebase_precommit_success(
        self, mock_run, workflow_client, precommit_pr
    ):
        """Test manual rebase for pre-commit.ci PR."""
        # Mock gh pr view to get branch names
        # Mock gh repo clone
        # Mock git operations
        mock_run.side_effect = [
            # gh pr view for branch names
            MagicMock(
                returncode=0,
                stdout='{"headRefName":"pre-commit-ci-update-config","baseRefName":"main"}',
            ),
            # gh repo clone
            MagicMock(returncode=0, stdout=""),
            # git config user.name
            MagicMock(returncode=0, stdout=""),
            # git config user.email
            MagicMock(returncode=0, stdout=""),
            # git remote set-url (embeds token in URL for auth)
            MagicMock(returncode=0, stdout=""),
            # git fetch origin head_ref
            MagicMock(returncode=0, stdout=""),
            # git checkout
            MagicMock(returncode=0, stdout=""),
            # git rev-parse HEAD (before rebase)
            MagicMock(returncode=0, stdout="abc123def456\n"),
            # git fetch origin base_ref
            MagicMock(returncode=0, stdout=""),
            # git rebase
            MagicMock(returncode=0, stdout=""),
            # git rev-parse HEAD (after rebase)
            MagicMock(returncode=0, stdout="789ghi012jkl\n"),
            # git push --force
            MagicMock(returncode=0, stdout=""),
        ]

        success, new_sha, sha_changed = workflow_client.trigger_rebase(precommit_pr)

        assert success is True
        assert new_sha == "789ghi012jkl"
        assert sha_changed is True
        # Should make multiple git-related calls
        assert mock_run.call_count == 12
        # Verify we use --force (not --force-with-lease)
        push_call = mock_run.call_args_list[-1]
        assert "--force" in push_call[0][0]

    @patch("subprocess.run")
    def test_trigger_rebase_precommit_failure(
        self, mock_run, workflow_client, precommit_pr
    ):
        """Test manual rebase failure for pre-commit.ci PR."""
        # Mock gh pr view succeeds but rebase fails
        mock_run.side_effect = [
            # gh pr view for branch names
            MagicMock(
                returncode=0,
                stdout='{"headRefName":"pre-commit-ci-update-config","baseRefName":"main"}',
            ),
            # gh repo clone
            MagicMock(returncode=0, stdout=""),
            # git config user.name
            MagicMock(returncode=0, stdout=""),
            # git config user.email
            MagicMock(returncode=0, stdout=""),
            # git remote set-url
            MagicMock(returncode=0, stdout=""),
            # git fetch origin head_ref
            MagicMock(returncode=0, stdout=""),
            # git checkout
            MagicMock(returncode=0, stdout=""),
            # git rev-parse HEAD (before rebase)
            MagicMock(returncode=0, stdout="abc123def456\n"),
            # git fetch origin base_ref
            MagicMock(returncode=0, stdout=""),
            # git rebase fails
            subprocess.CalledProcessError(
                1, "git rebase", stderr=b"CONFLICT (content): Merge conflict"
            ),
        ]

        success, new_sha, sha_changed = workflow_client.trigger_rebase(precommit_pr)

        assert success is False
        assert new_sha is None
        assert sha_changed is False

    @patch("subprocess.run")
    def test_trigger_rebase_precommit_already_uptodate(
        self, mock_run, workflow_client, precommit_pr
    ):
        """Test manual rebase when branch is already up-to-date."""
        # SHA before and after rebase is the same
        mock_run.side_effect = [
            # gh pr view for branch names
            MagicMock(
                returncode=0,
                stdout='{"headRefName":"pre-commit-ci-update-config","baseRefName":"main"}',
            ),
            # gh repo clone
            MagicMock(returncode=0, stdout=""),
            # git config user.name
            MagicMock(returncode=0, stdout=""),
            # git config user.email
            MagicMock(returncode=0, stdout=""),
            # git remote set-url
            MagicMock(returncode=0, stdout=""),
            # git fetch origin head_ref
            MagicMock(returncode=0, stdout=""),
            # git checkout
            MagicMock(returncode=0, stdout=""),
            # git rev-parse HEAD (before rebase)
            MagicMock(returncode=0, stdout="abc123def456\n"),
            # git fetch origin base_ref
            MagicMock(returncode=0, stdout=""),
            # git rebase (no-op, already up-to-date)
            MagicMock(returncode=0, stdout=""),
            # git rev-parse HEAD (after rebase - same SHA)
            MagicMock(returncode=0, stdout="abc123def456\n"),
            # No force push since SHA didn't change
        ]

        success, new_sha, sha_changed = workflow_client.trigger_rebase(precommit_pr)

        assert success is True
        assert new_sha == "abc123def456"
        assert sha_changed is False
        # Should NOT push since no changes
        assert mock_run.call_count == 11  # No push call
        # Verify last call was NOT a push
        last_call = mock_run.call_args_list[-1]
        assert "push" not in str(last_call)

    @patch("subprocess.run")
    def test_trigger_rebase_unknown_bot(self, mock_run, workflow_client):
        """Test rebase failure for unknown bot author."""
        unknown_pr = PRQueueItem(
            repo="VectorInstitute/test-repo",
            pr_number=789,
            pr_title="Update by unknown bot",
            pr_author="unknown-bot[bot]",
            pr_url="https://github.com/VectorInstitute/test-repo/pull/789",
            status=PRStatus.PENDING,
            queued_at="2025-01-15T10:00:00Z",
            last_updated="2025-01-15T10:00:00Z",
        )

        success, new_sha, sha_changed = workflow_client.trigger_rebase(unknown_pr)

        assert success is False
        assert new_sha is None
        assert sha_changed is False
        # Should not make any gh CLI calls
        mock_run.assert_not_called()

    @patch("time.sleep")
    @patch("subprocess.run")
    def test_trigger_fix_workflow_success(
        self, mock_run, mock_sleep, workflow_client, sample_pr
    ):
        """Test successful fix workflow triggering."""
        # First call: trigger workflow
        # Second call: get run list
        mock_run.side_effect = [
            MagicMock(returncode=0, stdout=""),
            MagicMock(returncode=0, stdout=json.dumps([{"databaseId": 789}])),
        ]

        run_id = workflow_client.trigger_fix_workflow(sample_pr)

        assert run_id == "789"
        assert mock_run.call_count == 2

    @patch("time.sleep")
    @patch("subprocess.run")
    def test_trigger_fix_workflow_no_run_id(
        self, mock_run, mock_sleep, workflow_client, sample_pr
    ):
        """Test fix workflow triggering when run ID cannot be retrieved."""
        mock_run.side_effect = [
            MagicMock(returncode=0, stdout=""),
            MagicMock(returncode=0, stdout="[]"),
        ]

        run_id = workflow_client.trigger_fix_workflow(sample_pr)

        assert run_id is None

    @patch("time.sleep")
    @patch("subprocess.run")
    def test_poll_workflow_status_success(self, mock_run, mock_sleep, workflow_client):
        """Test polling workflow status until success."""
        mock_run.return_value = MagicMock(
            returncode=0,
            stdout=json.dumps({"status": "completed", "conclusion": "success"}),
        )

        status = workflow_client.poll_workflow_status("789", timeout_minutes=1)

        assert status == "SUCCESS"

    @patch("time.sleep")
    @patch("subprocess.run")
    def test_poll_workflow_status_failure(self, mock_run, mock_sleep, workflow_client):
        """Test polling workflow status until failure."""
        mock_run.return_value = MagicMock(
            returncode=0,
            stdout=json.dumps({"status": "completed", "conclusion": "failure"}),
        )

        status = workflow_client.poll_workflow_status("789", timeout_minutes=1)

        assert status == "FAILURE"

    @patch("time.sleep")
    @patch("subprocess.run")
    def test_poll_workflow_status_timeout(self, mock_run, mock_sleep, workflow_client):
        """Test polling workflow status timeout."""
        mock_run.return_value = MagicMock(
            returncode=0,
            stdout=json.dumps({"status": "in_progress", "conclusion": None}),
        )

        status = workflow_client.poll_workflow_status("789", timeout_minutes=1)

        assert status == "RUNNING"

    @patch("subprocess.run")
    def test_auto_merge_pr_success(self, mock_run, workflow_client, sample_pr):
        """Test successful auto-merge."""
        mock_run.side_effect = [
            MagicMock(returncode=0, stdout=json.dumps({"reviewDecision": None})),
            MagicMock(returncode=0, stdout=""),  # approve
            MagicMock(returncode=0, stdout=""),  # merge
        ]

        result = workflow_client.auto_merge_pr(sample_pr)

        assert result is True
        assert mock_run.call_count == 3

    @patch("subprocess.run")
    def test_auto_merge_pr_already_approved(self, mock_run, workflow_client, sample_pr):
        """Test auto-merge when PR is already approved."""
        mock_run.side_effect = [
            MagicMock(returncode=0, stdout=json.dumps({"reviewDecision": "APPROVED"})),
            MagicMock(returncode=0, stdout=""),  # merge
        ]

        result = workflow_client.auto_merge_pr(sample_pr)

        assert result is True
        assert mock_run.call_count == 2

    @patch("subprocess.run")
    def test_auto_merge_pr_failure(self, mock_run, workflow_client, sample_pr):
        """Test auto-merge failure."""
        mock_run.side_effect = subprocess.CalledProcessError(1, "gh")

        result = workflow_client.auto_merge_pr(sample_pr)

        assert result is False
