import pytest
import unittest
from unittest.mock import patch, MagicMock
from unittest import IsolatedAsyncioTestCase
import httpx
from uuid import uuid4
from preloop.sync.exceptions import (
    TrackerAuthenticationError,
    TrackerConnectionError,
    TrackerResponseError,
)

from preloop.sync.trackers.github import GitHubTracker


@pytest.mark.asyncio
class TestGitHubTrackerDependencies(unittest.IsolatedAsyncioTestCase):
    @patch("preloop.sync.trackers.github.httpx.AsyncClient.get")
    async def test_get_issues_parses_dependencies_from_body_and_comments(
        self, mock_requests_get
    ):
        """Test that dependency parsing is integrated into issue fetching."""
        # Mock API responses
        mock_issues_response = {
            "json": MagicMock(
                return_value=[
                    {
                        "id": 1,
                        "number": 1,
                        "title": "Test issue",
                        "body": "This closes #123 and fixes owner/other-repo#456",
                        "state": "open",
                        "created_at": "2023-01-01T00:00:00Z",
                        "updated_at": "2023-01-01T00:00:00Z",
                        "user": {
                            "id": 1,
                            "login": "testuser",
                            "avatar_url": "https://avatar.url",
                        },
                    }
                ]
            ),
            "status_code": 200,
            "links": {},
        }

        mock_comments_response = {
            "json": MagicMock(
                return_value=[
                    {
                        "id": 1,
                        "body": "This relates to #789",
                        "created_at": "2023-01-01T01:00:00Z",
                        "updated_at": "2023-01-01T01:00:00Z",
                        "html_url": "https://github.com/owner/repo/issues/1#issuecomment-1",
                        "user": {
                            "id": 1,
                            "login": "testuser",
                            "avatar_url": "https://avatar.url",
                        },
                    }
                ]
            ),
            "status_code": 200,
            "links": {},
        }

        # Set up mock responses
        mock_requests_get.side_effect = [
            MagicMock(**mock_issues_response),  # Issues response
            MagicMock(**mock_comments_response),  # Comments response
        ]

        # Create tracker instance
        tracker = GitHubTracker("test-tracker", "test-token", {})

        # Call get_issues
        issues = await tracker.get_issues("test-org", "owner/repo")

        # Verify the issue was processed and dependencies were parsed
        assert len(issues) == 1
        issue = issues[0]

        # Check that dependencies were parsed and added
        assert "dependencies" in issue
        dependencies = issue["dependencies"]

        # Should have 3 dependencies: 2 from body + 1 from comment
        assert len(dependencies) == 3

        # Check dependencies from body
        body_deps = [
            d
            for d in dependencies
            if d["target_key"] in ["owner/repo#123", "owner/other-repo#456"]
        ]
        assert len(body_deps) == 2
        assert any(
            d["type"] == "closes" and d["target_key"] == "owner/repo#123"
            for d in body_deps
        )
        assert any(
            d["type"] == "closes" and d["target_key"] == "owner/other-repo#456"
            for d in body_deps
        )

        # Check dependency from comment
        comment_deps = [d for d in dependencies if d["target_key"] == "owner/repo#789"]
        assert len(comment_deps) == 1
        assert comment_deps[0]["type"] == "related"


@pytest.mark.asyncio
class TestGitHubTrackerComments(unittest.IsolatedAsyncioTestCase):
    @patch("preloop.sync.trackers.github.httpx.AsyncClient.get")
    async def test_get_issues_fetches_and_transforms_comments(self, mock_requests_get):
        # --- Mock API Responses ---
        # Response for repo details (if project_id is not full_name)
        mock_repo_details_response = MagicMock()
        mock_repo_details_response.status_code = 200
        mock_repo_details_response.json.return_value = {
            "id": 1296269,
            "full_name": "octocat/Hello-World",
            "name": "Hello-World",
            # ... other fields if needed by the tracker
        }

        # Response for issues list
        mock_issues_list_response = MagicMock()
        mock_issues_list_response.status_code = 200
        mock_issues_list_response.json.return_value = [
            {
                "id": 1,
                "number": 1347,
                "title": "Found a bug",
                "body": "I'm having a problem with this.",
                "state": "open",
                "user": {"login": "octocat", "id": 1},
                "labels": [{"name": "bug"}],
                "assignees": [{"login": "octocat", "id": 1}],
                "created_at": "2011-04-22T13:33:48Z",
                "updated_at": "2011-04-22T13:33:48Z",
                "html_url": "https://github.com/octocat/Hello-World/issues/1347",
            }
        ]

        # Response for issue comments
        mock_comments_response = MagicMock()
        mock_comments_response.status_code = 200
        mock_comments_response.json.return_value = [
            {
                "id": 101,
                "user": {"login": "commenter", "id": 2, "avatar_url": ""},
                "body": "This is a comment on the issue.",
                "created_at": "2011-04-22T14:00:00Z",
                "updated_at": "2011-04-22T14:00:00Z",
                "html_url": "https://github.com/octocat/Hello-World/issues/1347#issuecomment-101",
            }
        ]

        # Configure mock_requests_get to return different responses based on URL
        async def side_effect_requests_get(url, headers, params=None):
            if "repositories/project-id-123" in url:
                return mock_repo_details_response  # If project_id is an ID
            if (
                "repos/octocat/Hello-World/issues"
                == url.split("?")[0].replace(GitHubTracker.API_BASE_URL + "/", "")
                and params.get("state") == "all"
            ):
                return mock_issues_list_response
            if "repos/octocat/Hello-World/issues/1347/comments" in url:
                return mock_comments_response
            # Fallback for unexpected calls
            fallback_response = MagicMock()
            fallback_response.status_code = 404
            fallback_response.json.return_value = {"message": "Not Found"}
            print(
                f"UNMOCKED URL in test: {url} with params {params}"
            )  # For debugging tests
            return fallback_response

        mock_requests_get.side_effect = side_effect_requests_get

        # --- Initialize Tracker ---
        tracker = GitHubTracker(
            tracker_id="test-github-tracker",
            api_key="fake_token",
            connection_details={},
        )

        # --- Call get_issues ---
        # Using 'octocat/Hello-World' directly as project_id to simplify one mock path
        raw_issues = await tracker.get_issues(
            organization_id="octocat", project_id="octocat/Hello-World"
        )

        # --- Assertions ---
        self.assertEqual(len(raw_issues), 1, "Should return one issue")

        project_mock = MagicMock()
        project_mock.id = "proj-123"
        project_mock.slug = "octocat/Hello-World"
        transformed_issue = tracker.transform_issue(raw_issues[0], project_mock)

        self.assertEqual(transformed_issue["external_id"], "1")
        self.assertEqual(transformed_issue["key"], "octocat/Hello-World#1347")
        self.assertEqual(transformed_issue["title"], "Found a bug")
        self.assertEqual(
            len(transformed_issue["comments"]), 1, "Should include one comment"
        )

        comment_data = transformed_issue["comments"][0]
        self.assertEqual(comment_data["id"], 101)
        self.assertEqual(comment_data["body"], "This is a comment on the issue.")
        self.assertEqual(comment_data["user"]["login"], "commenter")
        self.assertEqual(
            comment_data["created_at"],
            "2011-04-22T14:00:00Z",
        )

        # Verify API calls (simplified check of calls made)
        # Check that requests.get was called at least for issues and comments
        # A more specific check would involve asserting call_args_list
        self.assertTrue(mock_requests_get.call_count >= 2)


@pytest.mark.asyncio
class TestGitHubTracker(unittest.IsolatedAsyncioTestCase):
    @patch("preloop.sync.trackers.github.httpx.AsyncClient.get")
    async def test_get_organizations(self, mock_requests_get):
        mock_user_response = MagicMock()
        mock_user_response.status_code = 200
        mock_user_response.json.return_value = {
            "login": "octocat",
            "html_url": "https://github.com/octocat",
        }

        mock_orgs_response = MagicMock()
        mock_orgs_response.status_code = 200
        mock_orgs_response.json.return_value = [
            {
                "id": 1,
                "login": "github",
                "url": "https://api.github.com/orgs/github",
            }
        ]

        mock_requests_get.side_effect = [mock_user_response, mock_orgs_response]

        tracker = GitHubTracker(str(uuid4()), "api-key", {})
        orgs = await tracker.get_organizations()

        self.assertEqual(len(orgs), 2)
        self.assertEqual(orgs[0]["name"], "octocat")
        self.assertEqual(orgs[1]["name"], "github")

    @patch("preloop.sync.trackers.github.httpx.AsyncClient.get")
    async def test_get_projects(self, mock_requests_get):
        mock_repos_response = MagicMock()
        mock_repos_response.status_code = 200
        mock_repos_response.json.return_value = [
            {
                "id": 1296269,
                "name": "Hello-World",
                "full_name": "octocat/Hello-World",
                "description": "My first repository on GitHub!",
                "html_url": "https://github.com/octocat/Hello-World",
                "default_branch": "main",
                "language": "JavaScript",
                "created_at": "2011-01-26T19:01:12Z",
                "pushed_at": "2011-01-26T19:14:43Z",
                "stargazers_count": 80,
            }
        ]
        mock_requests_get.return_value = mock_repos_response

        tracker = GitHubTracker(str(uuid4()), "api-key", {})
        projects = await tracker.get_projects("octocat")

        self.assertEqual(len(projects), 1)
        self.assertEqual(projects[0]["name"], "Hello-World")

    @patch("preloop.sync.trackers.github.httpx.AsyncClient.get")
    async def test_get_projects_personal(self, mock_requests_get):
        mock_repos_response = MagicMock()
        mock_repos_response.status_code = 200
        mock_repos_response.json.return_value = [
            {
                "id": 1296269,
                "name": "Hello-World",
                "full_name": "octocat/Hello-World",
                "description": "My first repository on GitHub!",
                "html_url": "https://github.com/octocat/Hello-World",
                "default_branch": "main",
                "language": "JavaScript",
                "created_at": "2011-01-26T19:01:12Z",
                "pushed_at": "2011-01-26T19:14:43Z",
                "stargazers_count": 80,
            }
        ]
        mock_requests_get.return_value = mock_repos_response

        tracker = GitHubTracker(str(uuid4()), "api-key", {})
        projects = await tracker.get_projects("personal")

        self.assertEqual(len(projects), 1)
        self.assertEqual(projects[0]["name"], "Hello-World")
        mock_requests_get.assert_called_with(
            "https://api.github.com/user/repos",
            headers=tracker.headers,
            params={"per_page": 100, "sort": "updated", "direction": "desc"},
        )

    @patch("preloop.sync.trackers.github.httpx.AsyncClient.get")
    async def test_get_issues_repo_details_error(self, mock_requests_get):
        mock_requests_get.side_effect = TrackerResponseError("Failed to fetch")

        tracker = GitHubTracker(str(uuid4()), "api-key", {})
        issues = await tracker.get_issues("octocat", "12345")

        self.assertEqual(len(issues), 0)

    @patch("preloop.sync.trackers.github.httpx.AsyncClient.get")
    async def test_get_issues_fetch_error(self, mock_requests_get):
        mock_repo_details_response = MagicMock()
        mock_repo_details_response.status_code = 200
        mock_repo_details_response.json.return_value = {
            "full_name": "octocat/Hello-World"
        }

        async def side_effect(*args, **kwargs):
            if "repositories" in args[0]:
                return mock_repo_details_response
            else:
                raise TrackerResponseError("Failed to fetch")

        mock_requests_get.side_effect = side_effect

        tracker = GitHubTracker(str(uuid4()), "api-key", {})
        issues = await tracker.get_issues("octocat", "12345")

        self.assertEqual(len(issues), 0)

    @patch("preloop.sync.trackers.github.httpx.AsyncClient.get")
    async def test_get_issues_skips_pull_requests(self, mock_requests_get):
        mock_issues_response = MagicMock()
        mock_issues_response.status_code = 200
        mock_issues_response.json.return_value = [
            {
                "id": 1,
                "number": 1347,
                "title": "Found a bug",
                "pull_request": {},
            }
        ]
        mock_requests_get.return_value = mock_issues_response

        tracker = GitHubTracker(str(uuid4()), "api-key", {})
        issues = await tracker.get_issues("octocat", "octocat/Hello-World")

        self.assertEqual(len(issues), 0)

    @patch("preloop.sync.trackers.github.httpx.AsyncClient.get")
    async def test_get_issues_no_comments(self, mock_requests_get):
        mock_issues_response = MagicMock()
        mock_issues_response.status_code = 200
        mock_issues_response.json.return_value = [
            {
                "id": 1,
                "number": 1347,
                "title": "Found a bug",
                "body": "I'm having a problem with this.",
                "state": "open",
                "user": {"login": "octocat"},
                "labels": [],
                "assignees": [],
                "created_at": "2011-04-22T13:33:48Z",
                "updated_at": "2011-04-22T13:33:48Z",
                "html_url": "https://github.com/octocat/Hello-World/issues/1347",
            }
        ]

        mock_comments_response = MagicMock()
        mock_comments_response.status_code = 200
        mock_comments_response.json.return_value = []

        async def side_effect(*args, **kwargs):
            if "comments" in args[0]:
                return mock_comments_response
            return mock_issues_response

        mock_requests_get.side_effect = side_effect

        tracker = GitHubTracker(str(uuid4()), "api-key", {})
        issues = await tracker.get_issues("octocat", "octocat/Hello-World")

        self.assertEqual(len(issues), 1)
        self.assertEqual(len(issues[0]["comments"]), 0)


@pytest.mark.asyncio
class TestGitHubTrackerRequestErrors(unittest.IsolatedAsyncioTestCase):
    @patch("preloop.sync.trackers.github.httpx.AsyncClient.get")
    async def test_make_request_authentication_error(self, mock_requests_get):
        mock_response = MagicMock()
        mock_response.status_code = 401
        mock_requests_get.return_value = mock_response

        tracker = GitHubTracker(str(uuid4()), "api-key", {})
        with self.assertRaises(TrackerAuthenticationError):
            await tracker._make_request("user")

    @patch("preloop.sync.trackers.github.httpx.AsyncClient.get")
    async def test_make_request_connection_error(self, mock_requests_get):
        mock_requests_get.side_effect = httpx.RequestError(
            "Connection failed", request=MagicMock()
        )

        tracker = GitHubTracker(str(uuid4()), "api-key", {})
        with self.assertRaises(TrackerConnectionError):
            await tracker._make_request("user")

    @patch("preloop.sync.trackers.github.httpx.AsyncClient.get")
    async def test_make_request_response_error(self, mock_requests_get):
        mock_response = MagicMock()
        mock_response.status_code = 500
        mock_response.text = "Internal Server Error"
        mock_requests_get.return_value = mock_response

        tracker = GitHubTracker(str(uuid4()), "api-key", {})
        with self.assertRaises(TrackerResponseError):
            await tracker._make_request("user")


@pytest.mark.asyncio
class TestGitHubTrackerDeleteRequest(unittest.IsolatedAsyncioTestCase):
    @patch("preloop.sync.trackers.github.httpx.AsyncClient.delete")
    async def test_make_request_delete_success(self, mock_requests_delete):
        mock_response = MagicMock()
        mock_response.status_code = 204
        mock_requests_delete.return_value = mock_response

        tracker = GitHubTracker(str(uuid4()), "api-key", {})
        self.assertTrue(await tracker._make_request_delete("hooks/1"))

    @patch("preloop.sync.trackers.github.httpx.AsyncClient.delete")
    async def test_make_request_delete_not_found(self, mock_requests_delete):
        mock_response = MagicMock()
        mock_response.status_code = 404
        mock_requests_delete.return_value = mock_response

        tracker = GitHubTracker(str(uuid4()), "api-key", {})
        self.assertTrue(await tracker._make_request_delete("hooks/1"))

    @patch("preloop.sync.trackers.github.httpx.AsyncClient.delete")
    async def test_make_request_delete_authentication_error(self, mock_requests_delete):
        mock_response = MagicMock()
        mock_response.status_code = 401
        mock_requests_delete.return_value = mock_response

        tracker = GitHubTracker(str(uuid4()), "api-key", {})
        with self.assertRaises(TrackerAuthenticationError):
            await tracker._make_request_delete("hooks/1")

    @patch("preloop.sync.trackers.github.httpx.AsyncClient.delete")
    async def test_make_request_delete_connection_error(self, mock_requests_delete):
        mock_requests_delete.side_effect = httpx.RequestError(
            "Connection failed", request=MagicMock()
        )

        tracker = GitHubTracker(str(uuid4()), "api-key", {})
        with self.assertRaises(TrackerConnectionError):
            await tracker._make_request_delete("hooks/1")

    @patch("preloop.sync.trackers.github.httpx.AsyncClient.delete")
    async def test_make_request_delete_response_error(self, mock_requests_delete):
        mock_response = MagicMock()
        mock_response.status_code = 500
        mock_response.text = "Internal Server Error"
        mock_requests_delete.return_value = mock_response

        tracker = GitHubTracker(str(uuid4()), "api-key", {})
        with self.assertRaises(TrackerResponseError):
            await tracker._make_request_delete("hooks/1")


@pytest.mark.asyncio
class TestGitHubTrackerWebhooks(unittest.IsolatedAsyncioTestCase):
    @patch("preloop.sync.trackers.github.GitHubTracker.get_projects")
    @patch("preloop.sync.trackers.github.GitHubTracker._make_request")
    async def test_get_webhooks(self, mock_make_request, mock_get_projects):
        mock_get_projects.return_value = [
            {"meta_data": {"full_name": "octocat/Hello-World"}}
        ]
        mock_make_request.return_value = [
            {
                "id": 1,
                "url": "https://example.com/webhook",
                "config": {"url": "https://example.com/webhook"},
            }
        ]

        tracker = GitHubTracker(str(uuid4()), "api-key", {})
        webhooks = await tracker.get_webhooks("octocat")

        self.assertEqual(len(webhooks), 1)
        self.assertEqual(webhooks[0]["id"], 1)
        mock_make_request.assert_called_with(
            "repos/octocat/Hello-World/hooks", params={"per_page": 100}
        )

    @patch("preloop.sync.trackers.github.httpx.AsyncClient.post")
    @patch("preloop.sync.trackers.github.crud_webhook.create")
    async def test_register_webhook(self, mock_crud_create, mock_requests_post):
        mock_response = MagicMock()
        mock_response.status_code = 201
        mock_response.json.return_value = {
            "id": 1,
            "url": "https://example.com/webhook",
            "config": {"url": "https://example.com/webhook"},
        }
        mock_requests_post.return_value = mock_response

        tracker = GitHubTracker(str(uuid4()), "api-key", {})
        db_mock = MagicMock()
        organization_mock = MagicMock()
        organization_mock.identifier = "octocat"
        result = await tracker.register_webhook(
            db=db_mock,
            organization=organization_mock,
            webhook_url="https://example.com/webhook",
            secret="secret",
        )

        self.assertTrue(result)

    @patch("preloop.sync.trackers.github.crud_webhook.remove")
    @patch("preloop.sync.trackers.github.httpx.AsyncClient.delete")
    async def test_unregister_webhook(self, mock_requests_delete, mock_crud_remove):
        mock_response = MagicMock()
        mock_response.status_code = 204
        mock_requests_delete.return_value = mock_response

        tracker = GitHubTracker(str(uuid4()), "api-key", {})
        db_mock = MagicMock()
        webhook_mock = MagicMock()
        webhook_mock.id = "webhook-db-id"
        webhook_mock.external_id = "1"
        webhook_mock.project.slug = "octocat/Hello-World"
        webhook_mock.organization.identifier = "octocat"
        result = await tracker.unregister_webhook(db=db_mock, webhook=webhook_mock)

        self.assertTrue(result)
        mock_requests_delete.assert_called_with(
            "https://api.github.com/repos/octocat/Hello-World/hooks/1",
            headers=tracker.headers,
        )
        mock_crud_remove.assert_called_with(db_mock, id="webhook-db-id")

    @patch("preloop.sync.trackers.github.GitHubTracker._make_request")
    async def test_is_webhook_registered_for_project(self, mock_make_request):
        mock_make_request.return_value = [
            {"config": {"url": "https://example.com/webhook"}}
        ]

        tracker = GitHubTracker(str(uuid4()), "api-key", {})
        project_mock = MagicMock()
        project_mock.slug = "octocat/Hello-World"
        is_registered = await tracker.is_webhook_registered_for_project(
            project_mock, "https://example.com/webhook"
        )

        self.assertTrue(is_registered)
        mock_make_request.assert_called_with("repos/octocat/Hello-World/hooks")

    @patch("preloop.sync.trackers.github.GitHubTracker._make_request")
    async def test_is_webhook_not_registered_for_project(self, mock_make_request):
        mock_make_request.return_value = [
            {"config": {"url": "https://example.com/other-webhook"}}
        ]

        tracker = GitHubTracker(str(uuid4()), "api-key", {})
        project_mock = MagicMock()
        project_mock.slug = "octocat/Hello-World"
        is_registered = await tracker.is_webhook_registered_for_project(
            project_mock, "https://example.com/webhook"
        )

        self.assertFalse(is_registered)
        mock_make_request.assert_called_with("repos/octocat/Hello-World/hooks")


class TestGitHubTrackerPagination(unittest.IsolatedAsyncioTestCase):
    @patch("preloop.sync.trackers.github.httpx.AsyncClient.get")
    async def test_make_request_pagination(self, mock_requests_get):
        mock_response_1 = MagicMock()
        mock_response_1.status_code = 200
        mock_response_1.json.return_value = [{"id": 1}]
        mock_response_1.links = {
            "next": {"url": "https://api.github.com/user/repos?page=2"}
        }

        mock_response_2 = MagicMock()
        mock_response_2.status_code = 200
        mock_response_2.json.return_value = [{"id": 2}]
        mock_response_2.links = {}

        mock_requests_get.side_effect = [mock_response_1, mock_response_2]

        tracker = GitHubTracker(str(uuid4()), "api-key", {})
        results = await tracker._make_request("user/repos")

        self.assertEqual(len(results), 2)
        self.assertEqual(results[0]["id"], 1)
        self.assertEqual(results[1]["id"], 2)
        self.assertEqual(mock_requests_get.call_count, 2)


@pytest.mark.asyncio
class TestGitHubTrackerUnregisterAllWebhooks(unittest.IsolatedAsyncioTestCase):
    @patch("preloop.sync.trackers.github.crud_project.get_for_organization")
    @patch("preloop.sync.trackers.github.crud_organization.get_multi")
    @patch("preloop.sync.trackers.github.GitHubTracker.unregister_webhook")
    async def test_unregister_all_webhooks_success(
        self,
        mock_unregister_webhook,
        mock_crud_organization_get_multi,
        mock_crud_project_get_for_organization,
    ):
        # Arrange
        tracker = GitHubTracker(str(uuid4()), "api-key", {})
        db_mock = MagicMock()

        mock_org = MagicMock()
        mock_org.id = "org-id-1"
        mock_crud_organization_get_multi.return_value = [mock_org]

        mock_project = MagicMock()
        mock_project.id = "project-id-1"
        mock_crud_project_get_for_organization.return_value = [mock_project]

        mock_webhook = MagicMock()
        db_mock.query.return_value.filter.return_value.all.return_value = [
            mock_webhook,
            mock_webhook,
        ]
        mock_unregister_webhook.return_value = True

        # Act
        results = await tracker.unregister_all_webhooks(db_mock)

        # Assert
        self.assertEqual(results["unregistered"], 2)
        self.assertEqual(results["failed"], 0)

    @patch("preloop.sync.trackers.github.crud_project.get_for_organization")
    @patch("preloop.sync.trackers.github.crud_organization.get_multi")
    @patch("preloop.sync.trackers.github.GitHubTracker.unregister_webhook")
    async def test_unregister_all_webhooks_failure(
        self,
        mock_unregister_webhook,
        mock_crud_organization_get_multi,
        mock_crud_project_get_for_organization,
    ):
        # Arrange
        tracker = GitHubTracker(str(uuid4()), "api-key", {})
        db_mock = MagicMock()

        mock_org = MagicMock()
        mock_org.id = "org-id-1"
        mock_crud_organization_get_multi.return_value = [mock_org]

        mock_project = MagicMock()
        mock_project.id = "project-id-1"
        mock_crud_project_get_for_organization.return_value = [mock_project]

        mock_webhook = MagicMock()
        db_mock.query.return_value.filter.return_value.all.return_value = [
            mock_webhook,
            mock_webhook,
        ]
        mock_unregister_webhook.side_effect = [True, False]

        # Act
        results = await tracker.unregister_all_webhooks(db_mock)

        # Assert
        self.assertEqual(results["unregistered"], 1)
        self.assertEqual(results["failed"], 1)

    @patch("preloop.sync.trackers.github.crud_project.get_for_organization")
    @patch("preloop.sync.trackers.github.crud_organization.get_multi")
    async def test_unregister_all_webhooks_no_webhooks(
        self, mock_crud_organization_get_multi, mock_crud_project_get_for_organization
    ):
        # Arrange
        tracker = GitHubTracker(str(uuid4()), "api-key", {})
        db_mock = MagicMock()

        mock_org = MagicMock()
        mock_org.id = "org-id-1"
        mock_crud_organization_get_multi.return_value = [mock_org]

        mock_project = MagicMock()
        mock_project.id = "project-id-1"
        mock_crud_project_get_for_organization.return_value = [mock_project]

        db_mock.query.return_value.filter.return_value.all.return_value = []

        # Act
        results = await tracker.unregister_all_webhooks(db_mock)

        # Assert
        self.assertEqual(results["unregistered"], 0)
        self.assertEqual(results["failed"], 0)


@pytest.mark.asyncio
class TestGitHubTrackerCleanupStaleWebhooks(unittest.IsolatedAsyncioTestCase):
    @patch("preloop.sync.trackers.github.GitHubTracker._make_request_delete")
    @patch("preloop.sync.trackers.github.GitHubTracker._make_request")
    async def test_cleanup_stale_webhooks_org_hooks(
        self, mock_make_request, mock_make_request_delete
    ):
        """
        Test that cleanup_stale_webhooks only deletes webhooks that:
        1. Point to our preloop_url
        2. Are NOT in our database

        This test has a webhook pointing to preloop_url that's not in DB, so it should be deleted.
        """
        # Arrange
        tracker = GitHubTracker(str(uuid4()), "api-key", {})
        mock_make_request.side_effect = [
            {"login": "user", "html_url": ""},
            [{"login": "org1", "url": "...", "id": "org1", "name": "org1"}],
            [{"config": {"url": "http://my-preloop.com/webhook"}, "id": 1}],
        ]
        mock_make_request_delete.return_value = True

        # Act
        results = await tracker.cleanup_stale_webhooks("http://my-preloop.com")

        # Assert: Should delete the stale webhook (points to preloop but not in DB)
        self.assertEqual(results["unregistered"], 1)
        self.assertEqual(results["failed"], 0)
        mock_make_request_delete.assert_called_with("orgs/org1/hooks/1")

    @patch("preloop.sync.trackers.github.GitHubTracker._make_request_delete")
    @patch("preloop.sync.trackers.github.GitHubTracker._make_request")
    async def test_cleanup_stale_webhooks_project_hooks(
        self, mock_make_request, mock_make_request_delete
    ):
        """
        Test cleanup for project-level webhooks.
        A webhook pointing to preloop_url that's not in DB should be deleted.
        """
        # Arrange
        tracker = GitHubTracker(str(uuid4()), "api-key", {})
        repo_meta = {
            "full_name": "org1/repo1",
            "id": "123",
            "name": "repo1",
            "description": "",
            "html_url": "",
            "default_branch": "main",
            "created_at": "2021-01-01T00:00:00Z",
            "pushed_at": "2021-01-01T00:00:00Z",
            "stargazers_count": 0,
            "meta_data": {"full_name": "org1/repo1"},
        }
        mock_make_request.side_effect = [
            {"login": "user", "html_url": ""},
            [{"login": "org1", "url": "...", "id": "org1", "name": "org1"}],
            [repo_meta],
            [{"config": {"url": "http://my-preloop.com/webhook"}, "id": 2}],
            [],
        ]
        mock_make_request_delete.return_value = True

        # Act
        results = await tracker.cleanup_stale_webhooks(
            "http://my-preloop.com", cleanup_projects=True
        )

        # Assert: Should delete the stale webhook (points to preloop but not in DB)
        self.assertEqual(results["unregistered"], 1)
        self.assertEqual(results["failed"], 0)
        mock_make_request_delete.assert_called_with("repos/org1/repo1/hooks/2")

    @patch("preloop.sync.trackers.github.GitHubTracker._make_request_delete")
    @patch("preloop.sync.trackers.github.GitHubTracker._make_request")
    async def test_cleanup_stale_webhooks_no_stale_hooks(
        self, mock_make_request, mock_make_request_delete
    ):
        """
        Test that webhooks pointing to OTHER URLs (not our preloop_url) are ignored.
        A webhook at "http://other-service.com/webhook" should NOT be deleted
        even though it's not in our DB, because it doesn't point to our Preloop.
        """
        # Arrange
        tracker = GitHubTracker(str(uuid4()), "api-key", {})
        mock_make_request.side_effect = [
            {"login": "user", "html_url": ""},
            [{"login": "org1", "url": "...", "id": "org1", "name": "org1"}],
            [{"config": {"url": "http://other-service.com/webhook"}, "id": 1}],
        ]

        # Act
        results = await tracker.cleanup_stale_webhooks("http://my-preloop.com")

        # Assert: Should NOT delete webhooks that don't point to our Preloop
        self.assertEqual(results["unregistered"], 0)
        self.assertEqual(results["failed"], 0)
        mock_make_request_delete.assert_not_called()


class TestGitHubTrackerIssueOperations(IsolatedAsyncioTestCase):
    """Test GitHub tracker issue-related operations."""

    async def test_get_issue_success(self):
        """Test successful issue retrieval."""
        # Arrange
        issue_data = {
            "id": 12345,
            "number": 1,
            "title": "Test Issue",
            "body": "Issue description",
            "state": "open",
            "created_at": "2023-01-01T10:00:00Z",
            "updated_at": "2023-01-02T11:00:00Z",
            "html_url": "https://github.com/owner/repo/issues/1",
            "url": "https://api.github.com/repos/owner/repo/issues/1",
            "labels": [{"name": "bug"}, {"name": "critical"}],
            "assignees": [{"login": "user1"}, {"login": "user2"}],
            "assignee": {
                "id": 123,
                "login": "user1",
                "avatar_url": "https://example.com/avatar.png",
            },
            "user": {
                "id": 456,
                "login": "reporter",
                "avatar_url": "https://example.com/reporter.png",
            },
        }

        connection_details = {"owner": "testowner", "repo": "testrepo"}
        tracker = GitHubTracker(str(uuid4()), "api-key", connection_details)

        # Mock the _make_request method directly
        from unittest.mock import AsyncMock

        tracker._make_request = AsyncMock(return_value=issue_data)

        # Act
        result = await tracker.get_issue("1")

        # Assert - Now result is an Issue object, not a dict
        self.assertEqual(result.id, "12345")
        self.assertEqual(result.key, "testowner/testrepo#1")
        self.assertEqual(result.title, "Test Issue")
        self.assertEqual(result.description, "Issue description")
        self.assertEqual(result.status.id, "open")
        self.assertEqual(result.status.name, "Open")
        self.assertEqual(result.labels, ["bug", "critical"])
        self.assertEqual(result.url, "https://github.com/owner/repo/issues/1")
        self.assertIsNotNone(result.assignee)
        self.assertEqual(result.assignee.name, "user1")
        self.assertIsNotNone(result.reporter)
        self.assertEqual(result.reporter.name, "reporter")

        tracker._make_request.assert_called_once_with(
            "repos/testowner/testrepo/issues/1"
        )

    async def test_get_issue_pull_request_error(self):
        """Test error when trying to get a pull request as an issue."""
        # Arrange
        issue_data = {
            "id": 12345,
            "number": 1,
            "title": "Test PR",
            "pull_request": {"url": "https://api.github.com/repos/owner/repo/pulls/1"},
        }

        connection_details = {"owner": "testowner", "repo": "testrepo"}
        tracker = GitHubTracker(str(uuid4()), "api-key", connection_details)

        # Mock the _make_request method directly
        from unittest.mock import AsyncMock

        tracker._make_request = AsyncMock(return_value=issue_data)

        # Act & Assert
        with self.assertRaises(TrackerResponseError) as context:
            await tracker.get_issue("1")

        self.assertIn("is a pull request, not an issue", str(context.exception))

    async def test_get_issue_missing_connection_details(self):
        """Test error when connection details are missing."""
        # Arrange
        tracker = GitHubTracker(str(uuid4()), "api-key", {})

        # Act & Assert
        with self.assertRaises(TrackerResponseError) as context:
            await tracker.get_issue("1")

        self.assertIn(
            "Owner/repo not found in connection details", str(context.exception)
        )

    async def test_get_comments_success(self):
        """Test successful comments retrieval."""
        # Arrange
        comments_data = [
            {
                "id": 1001,
                "body": "First comment",
                "user": {
                    "id": 101,
                    "login": "commenter1",
                    "avatar_url": "https://avatars.github.com/u/101",
                },
                "created_at": "2023-01-01T12:00:00Z",
                "updated_at": "2023-01-01T12:00:00Z",
                "html_url": "https://github.com/owner/repo/issues/1#issuecomment-1001",
            },
            {
                "id": 1002,
                "body": "Second comment",
                "user": {
                    "id": 102,
                    "login": "commenter2",
                    "avatar_url": "https://avatars.github.com/u/102",
                },
                "created_at": "2023-01-01T13:00:00Z",
                "updated_at": "2023-01-01T13:00:00Z",
                "html_url": "https://github.com/owner/repo/issues/1#issuecomment-1002",
            },
        ]

        connection_details = {"owner": "testowner", "repo": "testrepo"}
        tracker = GitHubTracker(str(uuid4()), "api-key", connection_details)

        # Mock the _make_request method directly
        from unittest.mock import AsyncMock

        tracker._make_request = AsyncMock(return_value=comments_data)

        # Act
        result = await tracker.get_comments("1")

        # Assert
        self.assertEqual(len(result), 2)

        self.assertEqual(result[0].id, "1001")
        self.assertEqual(result[0].body, "First comment")
        self.assertEqual(result[0].author.id, "101")
        self.assertEqual(result[0].author.name, "commenter1")
        self.assertEqual(
            result[0].url, "https://github.com/owner/repo/issues/1#issuecomment-1001"
        )

        self.assertEqual(result[1].id, "1002")
        self.assertEqual(result[1].body, "Second comment")
        self.assertEqual(result[1].author.id, "102")
        self.assertEqual(result[1].author.name, "commenter2")

        tracker._make_request.assert_called_once_with(
            "repos/testowner/testrepo/issues/1/comments", params={"per_page": 100}
        )

    async def test_get_comments_missing_connection_details(self):
        """Test error when connection details are missing."""
        # Arrange
        tracker = GitHubTracker(str(uuid4()), "api-key", {})

        # Act & Assert
        with self.assertRaises(TrackerResponseError) as context:
            await tracker.get_comments("1")

        self.assertIn(
            "Owner/repo not found in connection details", str(context.exception)
        )

    async def test_get_comments_api_error(self):
        """Test handling of API errors when fetching comments."""
        # Arrange
        connection_details = {"owner": "testowner", "repo": "testrepo"}
        tracker = GitHubTracker(str(uuid4()), "api-key", connection_details)

        # Mock the _make_request method to raise an exception
        from unittest.mock import AsyncMock

        tracker._make_request = AsyncMock(
            side_effect=TrackerResponseError("404 Not Found")
        )

        # Act
        result = await tracker.get_comments("1")

        # Assert - should return empty list on error
        self.assertEqual(result, [])


@pytest.mark.asyncio
class TestGitHubTrackerTokenValidation(unittest.IsolatedAsyncioTestCase):
    """Tests for GitHub token permission validation."""

    @patch("preloop.sync.trackers.github.httpx.AsyncClient")
    async def test_validate_token_permissions_valid_token_with_all_scopes(
        self, mock_client_class
    ):
        """Test validation with a token that has all required scopes."""
        from unittest.mock import AsyncMock

        # Setup mock response with all required scopes
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.headers = {"X-OAuth-Scopes": "repo, admin:org_hook, user"}
        mock_response.json.return_value = {"login": "testuser"}

        mock_client = AsyncMock()
        mock_client.get.return_value = mock_response
        mock_client.__aenter__.return_value = mock_client
        mock_client.__aexit__.return_value = None
        mock_client_class.return_value = mock_client

        # Create tracker and validate
        tracker = GitHubTracker(str(uuid4()), "test-token", {})
        result = await tracker.validate_token_permissions()

        # Assert
        self.assertTrue(result["valid"])
        self.assertIn("repo", result["scopes"])
        self.assertIn("admin:org_hook", result["scopes"])
        self.assertEqual(len(result["warnings"]), 0)
        self.assertEqual(len(result["errors"]), 0)

    @patch("preloop.sync.trackers.github.httpx.AsyncClient")
    async def test_validate_token_permissions_missing_org_hook_scope(
        self, mock_client_class
    ):
        """Test validation warns when admin:org_hook scope is missing."""
        from unittest.mock import AsyncMock

        # Setup mock response without org_hook scope
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.headers = {"X-OAuth-Scopes": "repo, user"}
        mock_response.json.return_value = {"login": "testuser"}

        mock_client = AsyncMock()
        mock_client.get.return_value = mock_response
        mock_client.__aenter__.return_value = mock_client
        mock_client.__aexit__.return_value = None
        mock_client_class.return_value = mock_client

        # Create tracker and validate
        tracker = GitHubTracker(str(uuid4()), "test-token", {})
        result = await tracker.validate_token_permissions()

        # Assert - should be valid but with warning
        self.assertTrue(result["valid"])
        self.assertTrue(
            any("admin:org_hook" in w for w in result["warnings"]),
            "Should warn about missing admin:org_hook scope",
        )

    @patch("preloop.sync.trackers.github.httpx.AsyncClient")
    async def test_validate_token_permissions_invalid_token(self, mock_client_class):
        """Test validation fails with invalid token."""
        from unittest.mock import AsyncMock

        # Setup mock response for unauthorized
        mock_response = MagicMock()
        mock_response.status_code = 401

        mock_client = AsyncMock()
        mock_client.get.return_value = mock_response
        mock_client.__aenter__.return_value = mock_client
        mock_client.__aexit__.return_value = None
        mock_client_class.return_value = mock_client

        # Create tracker and validate
        tracker = GitHubTracker(str(uuid4()), "invalid-token", {})
        result = await tracker.validate_token_permissions()

        # Assert - should not be valid
        self.assertFalse(result["valid"])
        self.assertTrue(len(result["errors"]) > 0)

    @patch("preloop.sync.trackers.github.httpx.AsyncClient")
    async def test_validate_token_permissions_with_org_admin_check(
        self, mock_client_class
    ):
        """Test validation checks org admin status when org_identifier provided."""
        from unittest.mock import AsyncMock

        # Setup mock responses
        user_response = MagicMock()
        user_response.status_code = 200
        user_response.headers = {"X-OAuth-Scopes": "repo, admin:org_hook"}
        user_response.json.return_value = {"login": "testuser"}

        membership_response = MagicMock()
        membership_response.status_code = 200
        membership_response.json.return_value = {"role": "admin", "state": "active"}

        mock_client = AsyncMock()
        mock_client.get.side_effect = [user_response, membership_response]
        mock_client.__aenter__.return_value = mock_client
        mock_client.__aexit__.return_value = None
        mock_client_class.return_value = mock_client

        # Create tracker and validate with org
        tracker = GitHubTracker(str(uuid4()), "test-token", {})
        result = await tracker.validate_token_permissions(org_identifier="test-org")

        # Assert - should be valid and user is admin
        self.assertTrue(result["valid"])
        self.assertTrue(result["is_org_admin"])
        self.assertEqual(len(result["warnings"]), 0)

    @patch("preloop.sync.trackers.github.httpx.AsyncClient")
    async def test_validate_token_permissions_non_admin_user(self, mock_client_class):
        """Test validation warns when user is not an org admin."""
        from unittest.mock import AsyncMock

        # Setup mock responses
        user_response = MagicMock()
        user_response.status_code = 200
        user_response.headers = {"X-OAuth-Scopes": "repo, admin:org_hook"}
        user_response.json.return_value = {"login": "testuser"}

        membership_response = MagicMock()
        membership_response.status_code = 200
        membership_response.json.return_value = {"role": "member", "state": "active"}

        mock_client = AsyncMock()
        mock_client.get.side_effect = [user_response, membership_response]
        mock_client.__aenter__.return_value = mock_client
        mock_client.__aexit__.return_value = None
        mock_client_class.return_value = mock_client

        # Create tracker and validate with org
        tracker = GitHubTracker(str(uuid4()), "test-token", {})
        result = await tracker.validate_token_permissions(org_identifier="test-org")

        # Assert - should be valid but not admin with warning
        self.assertTrue(result["valid"])
        self.assertFalse(result["is_org_admin"])
        self.assertTrue(
            any("not an admin" in w for w in result["warnings"]),
            "Should warn that user is not an admin",
        )


@pytest.mark.asyncio
class TestGitHubTrackerReactions(unittest.IsolatedAsyncioTestCase):
    """Tests for reaction add/remove functionality."""

    @patch("preloop.sync.trackers.github.httpx.AsyncClient")
    async def test_remove_issue_reaction_with_user_auth(self, mock_client_class):
        """Test removing a reaction with user token auth."""
        from unittest.mock import AsyncMock

        # Setup mock responses
        reactions_response = MagicMock()
        reactions_response.status_code = 200
        reactions_response.json.return_value = [
            {"id": 123, "content": "eyes", "user": {"login": "testuser"}},
            {"id": 456, "content": "eyes", "user": {"login": "otheruser"}},
        ]

        user_response = MagicMock()
        user_response.status_code = 200
        user_response.json.return_value = {"login": "testuser"}

        delete_response = MagicMock()
        delete_response.status_code = 204

        mock_client = AsyncMock()
        mock_client.get.side_effect = [reactions_response, user_response]
        mock_client.delete.return_value = delete_response
        mock_client.__aenter__.return_value = mock_client
        mock_client.__aexit__.return_value = None
        mock_client_class.return_value = mock_client

        # Create tracker with owner/repo
        tracker = GitHubTracker(
            str(uuid4()), "test-token", {"owner": "testowner", "repo": "testrepo"}
        )
        result = await tracker.remove_issue_reaction("42", "eyes")

        # Should delete the reaction owned by testuser (id 123), not otheruser
        self.assertTrue(result)
        mock_client.delete.assert_called_once()
        delete_call = mock_client.delete.call_args
        self.assertIn("/reactions/123", delete_call[0][0])

    @patch("preloop.sync.trackers.github.httpx.AsyncClient")
    async def test_remove_issue_reaction_with_app_slug_in_connection_details(
        self, mock_client_class
    ):
        """Test removing a reaction with GitHub App when app_slug is in connection_details."""
        from unittest.mock import AsyncMock

        # Setup mock responses
        reactions_response = MagicMock()
        reactions_response.status_code = 200
        reactions_response.json.return_value = [
            {"id": 789, "content": "eyes", "user": {"login": "my-app[bot]"}},
            {"id": 456, "content": "eyes", "user": {"login": "humanuser"}},
        ]

        # GET /user returns 403 for app installation tokens
        user_response = MagicMock()
        user_response.status_code = 403

        delete_response = MagicMock()
        delete_response.status_code = 204

        mock_client = AsyncMock()
        mock_client.get.side_effect = [reactions_response, user_response]
        mock_client.delete.return_value = delete_response
        mock_client.__aenter__.return_value = mock_client
        mock_client.__aexit__.return_value = None
        mock_client_class.return_value = mock_client

        # Create tracker with owner/repo AND app_slug in connection_details
        tracker = GitHubTracker(
            str(uuid4()),
            "test-token",
            {"owner": "testowner", "repo": "testrepo", "app_slug": "my-app"},
        )
        result = await tracker.remove_issue_reaction("42", "eyes")

        # Should delete the reaction owned by my-app[bot] (id 789)
        self.assertTrue(result)
        mock_client.delete.assert_called_once()
        delete_call = mock_client.delete.call_args
        self.assertIn("/reactions/789", delete_call[0][0])

    @patch("preloop.sync.trackers.github.httpx.AsyncClient")
    async def test_remove_issue_reaction_installation_token_without_app_slug(
        self, mock_client_class
    ):
        """Test that reaction removal fails for installation tokens without app_slug.

        GitHub App installation tokens cannot call GET /user or GET /app.
        Without app_slug in connection_details, we cannot determine the bot identity.
        """
        from unittest.mock import AsyncMock

        # Setup mock responses
        reactions_response = MagicMock()
        reactions_response.status_code = 200
        reactions_response.json.return_value = [
            {"id": 789, "content": "eyes", "user": {"login": "my-app[bot]"}},
        ]

        # GET /user returns 403 for app installation tokens
        user_response = MagicMock()
        user_response.status_code = 403

        # GET /app also returns 403 for installation tokens (requires JWT)
        app_response = MagicMock()
        app_response.status_code = 403

        mock_client = AsyncMock()
        mock_client.get.side_effect = [reactions_response, user_response, app_response]
        mock_client.__aenter__.return_value = mock_client
        mock_client.__aexit__.return_value = None
        mock_client_class.return_value = mock_client

        # Create tracker WITHOUT app_slug in connection_details
        tracker = GitHubTracker(
            str(uuid4()),
            "test-token",
            {"owner": "testowner", "repo": "testrepo"},
        )
        result = await tracker.remove_issue_reaction("42", "eyes")

        # Should return False - cannot determine identity
        self.assertFalse(result)
        mock_client.delete.assert_not_called()

    @patch("preloop.sync.trackers.github.httpx.AsyncClient")
    async def test_remove_issue_reaction_refuses_without_auth(self, mock_client_class):
        """Test that reaction removal refuses when auth cannot be determined."""
        from unittest.mock import AsyncMock

        # Setup mock responses
        reactions_response = MagicMock()
        reactions_response.status_code = 200
        reactions_response.json.return_value = [
            {"id": 123, "content": "eyes", "user": {"login": "someuser"}},
        ]

        # GET /user fails
        user_response = MagicMock()
        user_response.status_code = 401

        mock_client = AsyncMock()
        mock_client.get.side_effect = [reactions_response, user_response]
        mock_client.__aenter__.return_value = mock_client
        mock_client.__aexit__.return_value = None
        mock_client_class.return_value = mock_client

        # Create tracker with owner/repo
        tracker = GitHubTracker(
            str(uuid4()), "test-token", {"owner": "testowner", "repo": "testrepo"}
        )
        result = await tracker.remove_issue_reaction("42", "eyes")

        # Should return False and not attempt to delete
        self.assertFalse(result)
        mock_client.delete.assert_not_called()


@pytest.mark.asyncio
class TestGitHubTrackerPullRequestUpdates(unittest.IsolatedAsyncioTestCase):
    """Tests for PR update functionality including assignees/reviewers."""

    @patch("preloop.sync.trackers.github.httpx.AsyncClient")
    async def test_update_pull_request_empty_assignees_clears_all(
        self, mock_client_class
    ):
        """Test that assignees=[] removes all existing assignees."""
        from unittest.mock import AsyncMock

        # PR data with existing assignees
        pr_data = {
            "id": 123,
            "number": 42,
            "title": "Test PR",
            "body": "Description",
            "state": "open",
            "html_url": "https://github.com/owner/repo/pull/42",
            "draft": False,
            "assignees": [{"login": "user1"}, {"login": "user2"}],
            "requested_reviewers": [],
        }

        patch_response = MagicMock()
        patch_response.status_code = 200
        patch_response.json.return_value = pr_data

        delete_response = MagicMock()
        delete_response.status_code = 200
        delete_response.json.return_value = {}

        mock_client = AsyncMock()
        # PATCH for PR update, DELETE for clearing assignees
        mock_client.request.side_effect = [patch_response, delete_response]
        mock_client.__aenter__.return_value = mock_client
        mock_client.__aexit__.return_value = None
        mock_client_class.return_value = mock_client

        tracker = GitHubTracker(
            str(uuid4()), "test-token", {"owner": "testowner", "repo": "testrepo"}
        )
        await tracker.update_pull_request(pr_identifier="42", assignees=[])

        # Verify DELETE was called with existing assignees
        calls = mock_client.request.call_args_list
        self.assertEqual(len(calls), 2)

        # Second call should be DELETE for assignees
        delete_call = calls[1]
        self.assertEqual(delete_call[0][0], "DELETE")
        self.assertIn("/assignees", delete_call[0][1])
        self.assertEqual(delete_call[1]["json"]["assignees"], ["user1", "user2"])

    @patch("preloop.sync.trackers.github.httpx.AsyncClient")
    async def test_update_pull_request_empty_reviewers_clears_all(
        self, mock_client_class
    ):
        """Test that reviewers=[] removes all existing reviewers."""
        from unittest.mock import AsyncMock

        # PR data with existing reviewers
        pr_data = {
            "id": 123,
            "number": 42,
            "title": "Test PR",
            "body": "Description",
            "state": "open",
            "html_url": "https://github.com/owner/repo/pull/42",
            "draft": False,
            "assignees": [],
            "requested_reviewers": [{"login": "reviewer1"}, {"login": "reviewer2"}],
        }

        patch_response = MagicMock()
        patch_response.status_code = 200
        patch_response.json.return_value = pr_data

        delete_response = MagicMock()
        delete_response.status_code = 200
        delete_response.json.return_value = {}

        mock_client = AsyncMock()
        mock_client.request.side_effect = [patch_response, delete_response]
        mock_client.__aenter__.return_value = mock_client
        mock_client.__aexit__.return_value = None
        mock_client_class.return_value = mock_client

        tracker = GitHubTracker(
            str(uuid4()), "test-token", {"owner": "testowner", "repo": "testrepo"}
        )
        await tracker.update_pull_request(pr_identifier="42", reviewers=[])

        # Verify DELETE was called with existing reviewers
        calls = mock_client.request.call_args_list
        self.assertEqual(len(calls), 2)

        # Second call should be DELETE for reviewers
        delete_call = calls[1]
        self.assertEqual(delete_call[0][0], "DELETE")
        self.assertIn("/requested_reviewers", delete_call[0][1])
        self.assertEqual(
            delete_call[1]["json"]["reviewers"], ["reviewer1", "reviewer2"]
        )
