import pytest
import requests
from unittest.mock import Mock, patch

from blocks.bitbucket import BitbucketRepoProvider
from blocks.config import Config


@pytest.fixture
def mock_config():
    config = Mock(spec=Config)
    config.get_bitbucket_token.return_value = "test_bitbucket_token"
    config.get_bitbucket_workspace.return_value = "test-workspace"
    config.get_bitbucket_repo_slug.return_value = "test-repo"
    config.get_token_resolver.return_value = None
    return config


@pytest.fixture
def bitbucket_provider(mock_config):
    return BitbucketRepoProvider(mock_config)


def _mock_response(status_code=200, payload=None):
    response = Mock()
    response.status_code = status_code
    response.json.return_value = payload if payload is not None else {}
    response.raise_for_status.return_value = None
    return response


class TestBitbucketRepoProvider:
    def test_request_uses_bearer_auth_header(self, bitbucket_provider):
        with patch("blocks.bitbucket.requests.request") as mock_request:
            mock_request.return_value = _mock_response(payload={"ok": True})
            result = bitbucket_provider.request("/repositories/a/b/pullrequests")

            assert result == {"ok": True}
            _, kwargs = mock_request.call_args
            assert kwargs["headers"]["Authorization"] == "Bearer test_bitbucket_token"
            assert kwargs["headers"]["Content-Type"] == "application/json"

    def test_request_retries_once_on_auth_failure(self, bitbucket_provider, mock_config):
        first = _mock_response(status_code=401, payload={"error": "unauthorized"})
        first.raise_for_status.side_effect = requests.exceptions.HTTPError("401")
        second = _mock_response(payload={"ok": True})

        mock_config.get_token_resolver.return_value = lambda provider: "fresh_token" if provider == "bitbucket" else None

        with patch("blocks.bitbucket.requests.request", side_effect=[first, second]) as mock_request:
            result = bitbucket_provider.request("/repositories/a/b/pullrequests")

            assert result == {"ok": True}
            assert mock_request.call_count == 2
            assert mock_request.call_args_list[1][1]["headers"]["Authorization"] == "Bearer fresh_token"

    def test_create_pull_request_builds_expected_payload(self, bitbucket_provider):
        with patch.object(bitbucket_provider, "request") as mock_request:
            mock_request.return_value = {"id": 123, "links": {"html": {"href": "https://bitbucket.org/workspace/repo/pull-requests/123"}}}

            result = bitbucket_provider.create_pull_request(
                source_branch="feature",
                target_branch="main",
                title="Add feature",
                body="PR body",
                draft=True,
                issue_number=42,
            )

            mock_request.assert_called_once_with(
                "/repositories/test-workspace/test-repo/pullrequests",
                method="POST",
                data={
                    "title": "Add feature",
                    "source": {"branch": {"name": "feature"}},
                    "destination": {"branch": {"name": "main"}},
                    "draft": True,
                    "description": "PR body\n\nCloses #42",
                },
            )
            assert result["html_url"] == "https://bitbucket.org/workspace/repo/pull-requests/123"

    def test_update_pull_request_fetches_and_puts_payload(self, bitbucket_provider):
        with patch.object(bitbucket_provider, "request") as mock_request:
            mock_request.side_effect = [
                {
                    "title": "Old title",
                    "description": "Old body",
                    "source": {"branch": {"name": "feature"}},
                    "destination": {"branch": {"name": "main"}},
                    "close_source_branch": False,
                    "draft": False,
                },
                {"id": 100},
            ]

            bitbucket_provider.update_pull_request(
                pull_request_number=100,
                title="New title",
                body="New body",
                target_branch="release",
                state="declined",
            )

            assert mock_request.call_count == 2
            assert mock_request.call_args_list[0][0][0] == "/repositories/test-workspace/test-repo/pullrequests/100"
            assert mock_request.call_args_list[0][1]["method"] == "GET"

            put_call = mock_request.call_args_list[1]
            assert put_call[0][0] == "/repositories/test-workspace/test-repo/pullrequests/100"
            assert put_call[1]["method"] == "PUT"
            assert put_call[1]["data"]["title"] == "New title"
            assert put_call[1]["data"]["description"] == "New body"
            assert put_call[1]["data"]["destination"] == {"branch": {"name": "release"}}
            assert put_call[1]["data"]["state"] == "DECLINED"

    def test_comment_on_pull_request(self, bitbucket_provider):
        with patch.object(bitbucket_provider, "request") as mock_request:
            mock_request.return_value = {"id": 1}

            bitbucket_provider.comment_on_pull_request(pull_request_number=7, body="Looks good")

            mock_request.assert_called_once_with(
                "/repositories/test-workspace/test-repo/pullrequests/7/comments",
                method="POST",
                data={"content": {"raw": "Looks good"}},
            )

    def test_comment_on_pull_request_file_with_inline_line_and_reply(self, bitbucket_provider):
        with patch.object(bitbucket_provider, "request") as mock_request:
            mock_request.return_value = {"id": 2}

            bitbucket_provider.comment_on_pull_request_file(
                pull_request_number=7,
                body="Please adjust this",
                file_path="src/main.py",
                line=15,
                side="RIGHT",
                subject_type="line",
                reply_to_id=999,
            )

            mock_request.assert_called_once_with(
                "/repositories/test-workspace/test-repo/pullrequests/7/comments",
                method="POST",
                data={
                    "content": {"raw": "Please adjust this"},
                    "parent": {"id": 999},
                    "inline": {"path": "src/main.py", "to": 15},
                },
            )

    def test_comment_on_pull_request_lines_payload(self, bitbucket_provider):
        with patch.object(bitbucket_provider, "request") as mock_request:
            mock_request.return_value = {"id": 3}

            bitbucket_provider.comment_on_pull_request_lines(
                pull_request_number=8,
                body="Range comment",
                file_path="src/main.py",
                start_line=10,
                end_line=14,
                side="RIGHT",
            )

            mock_request.assert_called_once_with(
                "/repositories/test-workspace/test-repo/pullrequests/8/comments",
                method="POST",
                data={
                    "content": {"raw": "Range comment"},
                    "inline": {"path": "src/main.py", "from": 10, "to": 14},
                },
            )

    def test_review_pull_request_approve(self, bitbucket_provider):
        with patch.object(bitbucket_provider, "request") as mock_request:
            with patch.object(bitbucket_provider, "comment_on_pull_request") as mock_comment:
                mock_request.return_value = {"approved": True}
                result = bitbucket_provider.review_pull_request(
                    pull_request_number=9,
                    event="APPROVE",
                    body="Approved",
                )
                assert result == {"approved": True}
                mock_request.assert_called_once_with(
                    "/repositories/test-workspace/test-repo/pullrequests/9/approve",
                    method="POST",
                )
                mock_comment.assert_called_once_with(
                    pull_request_number=9,
                    body="Approved",
                    owner="test-workspace",
                    repo="test-repo",
                )

    def test_review_pull_request_request_changes(self, bitbucket_provider):
        with patch.object(bitbucket_provider, "request") as mock_request:
            mock_request.return_value = {"requested": True}
            result = bitbucket_provider.review_pull_request(
                pull_request_number=11,
                event="REQUEST_CHANGES",
                body="Please update",
            )
            assert result == {"requested": True}
            mock_request.assert_called_once_with(
                "/repositories/test-workspace/test-repo/pullrequests/11/request-changes",
                method="POST",
                data={"message": "Please update"},
            )

    def test_update_delete_pull_request_comment_not_supported(self, bitbucket_provider):
        with pytest.raises(NotImplementedError, match="not supported"):
            bitbucket_provider.update_pull_request_comment(comment_id=1, body="updated")

        with pytest.raises(NotImplementedError, match="not supported"):
            bitbucket_provider.delete_pull_request_comment(comment_id=1)
