"""Tests for GitLab MR review methods in GitLabTracker.

This module tests the MR review functionality:
- approve_merge_request
- unapprove_merge_request
- get_mr_discussions
- update_mr_note
- resolve_mr_discussion
- create_mr_discussion
"""

import unittest
from unittest.mock import MagicMock, patch

from preloop.sync.trackers.gitlab import GitLabTracker
from preloop.sync.exceptions import TrackerResponseError


class TestApproveMergeRequest(unittest.IsolatedAsyncioTestCase):
    """Tests for approve_merge_request method."""

    @patch("preloop.sync.trackers.gitlab.gitlab.Gitlab")
    async def test_approve_merge_request_success(self, mock_gitlab_constructor):
        """Test successful MR approval."""
        mock_gl_instance = MagicMock()
        mock_gitlab_constructor.return_value = mock_gl_instance
        mock_gl_instance.auth.return_value = None

        mock_project = MagicMock()
        mock_gl_instance.projects.get.return_value = mock_project

        mock_mr = MagicMock()
        mock_mr.id = 12345
        mock_mr.iid = 1
        mock_mr.approve.return_value = {"approved": True, "user": {"id": 1}}
        mock_project.mergerequests.get.return_value = mock_mr

        tracker = GitLabTracker("tracker-1", "api-key", {"project_id": "proj-1"})
        result = await tracker.approve_merge_request("1")

        self.assertEqual(result["id"], "12345")
        self.assertEqual(result["iid"], 1)
        self.assertTrue(result["approved"])
        mock_mr.approve.assert_called_once()

    @patch("preloop.sync.trackers.gitlab.gitlab.Gitlab")
    async def test_approve_merge_request_no_project_id(self, mock_gitlab_constructor):
        """Test approval fails without project_id in connection details."""
        mock_gl_instance = MagicMock()
        mock_gitlab_constructor.return_value = mock_gl_instance
        mock_gl_instance.auth.return_value = None

        tracker = GitLabTracker("tracker-1", "api-key", {})

        with self.assertRaises(TrackerResponseError) as context:
            await tracker.approve_merge_request("1")

        self.assertIn("Project ID not found", str(context.exception))

    @patch("preloop.sync.trackers.gitlab.gitlab.Gitlab")
    async def test_approve_merge_request_api_error(self, mock_gitlab_constructor):
        """Test approval handles API errors."""
        mock_gl_instance = MagicMock()
        mock_gitlab_constructor.return_value = mock_gl_instance
        mock_gl_instance.auth.return_value = None

        mock_project = MagicMock()
        mock_gl_instance.projects.get.return_value = mock_project

        mock_mr = MagicMock()
        mock_mr.approve.side_effect = Exception("API Error")
        mock_project.mergerequests.get.return_value = mock_mr

        tracker = GitLabTracker("tracker-1", "api-key", {"project_id": "proj-1"})

        with self.assertRaises(TrackerResponseError) as context:
            await tracker.approve_merge_request("1")

        self.assertIn("Failed to approve merge request", str(context.exception))


class TestUnapproveMergeRequest(unittest.IsolatedAsyncioTestCase):
    """Tests for unapprove_merge_request method."""

    @patch("preloop.sync.trackers.gitlab.gitlab.Gitlab")
    async def test_unapprove_merge_request_success(self, mock_gitlab_constructor):
        """Test successful MR unapproval."""
        mock_gl_instance = MagicMock()
        mock_gitlab_constructor.return_value = mock_gl_instance
        mock_gl_instance.auth.return_value = None

        mock_project = MagicMock()
        mock_gl_instance.projects.get.return_value = mock_project

        mock_mr = MagicMock()
        mock_mr.id = 12345
        mock_mr.iid = 1
        mock_mr.unapprove.return_value = None
        mock_project.mergerequests.get.return_value = mock_mr

        tracker = GitLabTracker("tracker-1", "api-key", {"project_id": "proj-1"})
        result = await tracker.unapprove_merge_request("1")

        self.assertEqual(result["id"], "12345")
        self.assertEqual(result["iid"], 1)
        self.assertFalse(result["approved"])
        mock_mr.unapprove.assert_called_once()

    @patch("preloop.sync.trackers.gitlab.gitlab.Gitlab")
    async def test_unapprove_merge_request_no_project_id(self, mock_gitlab_constructor):
        """Test unapproval fails without project_id in connection details."""
        mock_gl_instance = MagicMock()
        mock_gitlab_constructor.return_value = mock_gl_instance
        mock_gl_instance.auth.return_value = None

        tracker = GitLabTracker("tracker-1", "api-key", {})

        with self.assertRaises(TrackerResponseError) as context:
            await tracker.unapprove_merge_request("1")

        self.assertIn("Project ID not found", str(context.exception))

    @patch("preloop.sync.trackers.gitlab.gitlab.Gitlab")
    async def test_unapprove_merge_request_api_error(self, mock_gitlab_constructor):
        """Test unapproval handles API errors."""
        mock_gl_instance = MagicMock()
        mock_gitlab_constructor.return_value = mock_gl_instance
        mock_gl_instance.auth.return_value = None

        mock_project = MagicMock()
        mock_gl_instance.projects.get.return_value = mock_project

        mock_mr = MagicMock()
        mock_mr.unapprove.side_effect = Exception("API Error")
        mock_project.mergerequests.get.return_value = mock_mr

        tracker = GitLabTracker("tracker-1", "api-key", {"project_id": "proj-1"})

        with self.assertRaises(TrackerResponseError) as context:
            await tracker.unapprove_merge_request("1")

        self.assertIn("Failed to unapprove merge request", str(context.exception))


class TestGetMrDiscussions(unittest.IsolatedAsyncioTestCase):
    """Tests for get_mr_discussions method."""

    @patch("preloop.sync.trackers.gitlab.gitlab.Gitlab")
    async def test_get_mr_discussions_success(self, mock_gitlab_constructor):
        """Test successful retrieval of MR discussions."""
        mock_gl_instance = MagicMock()
        mock_gitlab_constructor.return_value = mock_gl_instance
        mock_gl_instance.auth.return_value = None

        mock_project = MagicMock()
        mock_gl_instance.projects.get.return_value = mock_project

        mock_mr = MagicMock()
        mock_project.mergerequests.get.return_value = mock_mr

        # Create mock discussions
        mock_discussion = MagicMock()
        mock_discussion.id = "discussion-1"
        mock_discussion.individual_note = False
        mock_discussion.notes = [
            {
                "id": 101,
                "author": {"username": "user1"},
                "body": "This is a comment",
                "created_at": "2023-01-01T10:00:00Z",
                "updated_at": "2023-01-01T10:00:00Z",
                "resolvable": True,
                "resolved": False,
                "system": False,
            }
        ]
        mock_mr.discussions.list.return_value = [mock_discussion]

        tracker = GitLabTracker("tracker-1", "api-key", {"project_id": "proj-1"})
        result = await tracker.get_mr_discussions("1")

        self.assertEqual(len(result), 1)
        self.assertEqual(result[0]["id"], "discussion-1")
        self.assertEqual(len(result[0]["notes"]), 1)
        self.assertEqual(result[0]["notes"][0]["author"], "user1")
        self.assertEqual(result[0]["notes"][0]["body"], "This is a comment")

    @patch("preloop.sync.trackers.gitlab.gitlab.Gitlab")
    async def test_get_mr_discussions_with_author_filter(self, mock_gitlab_constructor):
        """Test MR discussions filtered by author."""
        mock_gl_instance = MagicMock()
        mock_gitlab_constructor.return_value = mock_gl_instance
        mock_gl_instance.auth.return_value = None

        mock_project = MagicMock()
        mock_gl_instance.projects.get.return_value = mock_project

        mock_mr = MagicMock()
        mock_project.mergerequests.get.return_value = mock_mr

        # Create mock discussions with different authors
        mock_discussion1 = MagicMock()
        mock_discussion1.id = "discussion-1"
        mock_discussion1.individual_note = False
        mock_discussion1.notes = [
            {
                "id": 101,
                "author": {"username": "user1"},
                "body": "Comment from user1",
                "created_at": "2023-01-01T10:00:00Z",
                "updated_at": "2023-01-01T10:00:00Z",
                "resolvable": True,
                "resolved": False,
                "system": False,
            }
        ]

        mock_discussion2 = MagicMock()
        mock_discussion2.id = "discussion-2"
        mock_discussion2.individual_note = False
        mock_discussion2.notes = [
            {
                "id": 102,
                "author": {"username": "user2"},
                "body": "Comment from user2",
                "created_at": "2023-01-01T11:00:00Z",
                "updated_at": "2023-01-01T11:00:00Z",
                "resolvable": True,
                "resolved": False,
                "system": False,
            }
        ]

        mock_mr.discussions.list.return_value = [mock_discussion1, mock_discussion2]

        tracker = GitLabTracker("tracker-1", "api-key", {"project_id": "proj-1"})
        result = await tracker.get_mr_discussions("1", filter_author="user1")

        # Only discussion-1 should be included (has notes from user1)
        self.assertEqual(len(result), 1)
        self.assertEqual(result[0]["id"], "discussion-1")
        self.assertEqual(result[0]["notes"][0]["author"], "user1")

    @patch("preloop.sync.trackers.gitlab.gitlab.Gitlab")
    async def test_get_mr_discussions_no_project_id(self, mock_gitlab_constructor):
        """Test get discussions fails without project_id."""
        mock_gl_instance = MagicMock()
        mock_gitlab_constructor.return_value = mock_gl_instance
        mock_gl_instance.auth.return_value = None

        tracker = GitLabTracker("tracker-1", "api-key", {})

        with self.assertRaises(TrackerResponseError) as context:
            await tracker.get_mr_discussions("1")

        self.assertIn("Project ID not found", str(context.exception))

    @patch("preloop.sync.trackers.gitlab.gitlab.Gitlab")
    async def test_get_mr_discussions_api_error(self, mock_gitlab_constructor):
        """Test get discussions handles API errors."""
        mock_gl_instance = MagicMock()
        mock_gitlab_constructor.return_value = mock_gl_instance
        mock_gl_instance.auth.return_value = None

        mock_project = MagicMock()
        mock_gl_instance.projects.get.return_value = mock_project

        mock_mr = MagicMock()
        mock_mr.discussions.list.side_effect = Exception("API Error")
        mock_project.mergerequests.get.return_value = mock_mr

        tracker = GitLabTracker("tracker-1", "api-key", {"project_id": "proj-1"})

        with self.assertRaises(TrackerResponseError) as context:
            await tracker.get_mr_discussions("1")

        self.assertIn("Failed to get MR discussions", str(context.exception))

    @patch("preloop.sync.trackers.gitlab.gitlab.Gitlab")
    async def test_get_mr_discussions_empty(self, mock_gitlab_constructor):
        """Test get discussions with no discussions."""
        mock_gl_instance = MagicMock()
        mock_gitlab_constructor.return_value = mock_gl_instance
        mock_gl_instance.auth.return_value = None

        mock_project = MagicMock()
        mock_gl_instance.projects.get.return_value = mock_project

        mock_mr = MagicMock()
        mock_mr.discussions.list.return_value = []
        mock_project.mergerequests.get.return_value = mock_mr

        tracker = GitLabTracker("tracker-1", "api-key", {"project_id": "proj-1"})
        result = await tracker.get_mr_discussions("1")

        self.assertEqual(result, [])


class TestUpdateMrNote(unittest.IsolatedAsyncioTestCase):
    """Tests for update_mr_note method."""

    @patch("preloop.sync.trackers.gitlab.gitlab.Gitlab")
    async def test_update_mr_note_success(self, mock_gitlab_constructor):
        """Test successful MR note update."""
        mock_gl_instance = MagicMock()
        mock_gitlab_constructor.return_value = mock_gl_instance
        mock_gl_instance.auth.return_value = None

        mock_project = MagicMock()
        mock_gl_instance.projects.get.return_value = mock_project

        mock_mr = MagicMock()
        mock_project.mergerequests.get.return_value = mock_mr

        mock_note = MagicMock()
        mock_note.id = 101
        mock_note.body = "Updated body"
        mock_note.author = {"username": "user1"}
        mock_note.updated_at = "2023-01-02T10:00:00Z"
        mock_mr.notes.get.return_value = mock_note

        tracker = GitLabTracker("tracker-1", "api-key", {"project_id": "proj-1"})
        result = await tracker.update_mr_note("1", "101", "Updated body")

        self.assertEqual(result["id"], "101")
        self.assertEqual(result["body"], "Updated body")
        self.assertEqual(result["author"], "user1")
        mock_note.save.assert_called_once()

    @patch("preloop.sync.trackers.gitlab.gitlab.Gitlab")
    async def test_update_mr_note_no_project_id(self, mock_gitlab_constructor):
        """Test note update fails without project_id."""
        mock_gl_instance = MagicMock()
        mock_gitlab_constructor.return_value = mock_gl_instance
        mock_gl_instance.auth.return_value = None

        tracker = GitLabTracker("tracker-1", "api-key", {})

        with self.assertRaises(TrackerResponseError) as context:
            await tracker.update_mr_note("1", "101", "New body")

        self.assertIn("Project ID not found", str(context.exception))

    @patch("preloop.sync.trackers.gitlab.gitlab.Gitlab")
    async def test_update_mr_note_api_error(self, mock_gitlab_constructor):
        """Test note update handles API errors."""
        mock_gl_instance = MagicMock()
        mock_gitlab_constructor.return_value = mock_gl_instance
        mock_gl_instance.auth.return_value = None

        mock_project = MagicMock()
        mock_gl_instance.projects.get.return_value = mock_project

        mock_mr = MagicMock()
        mock_mr.notes.get.side_effect = Exception("Note not found")
        mock_project.mergerequests.get.return_value = mock_mr

        tracker = GitLabTracker("tracker-1", "api-key", {"project_id": "proj-1"})

        with self.assertRaises(TrackerResponseError) as context:
            await tracker.update_mr_note("1", "999", "New body")

        self.assertIn("Failed to update MR note", str(context.exception))


class TestResolveMrDiscussion(unittest.IsolatedAsyncioTestCase):
    """Tests for resolve_mr_discussion method."""

    @patch("preloop.sync.trackers.gitlab.gitlab.Gitlab")
    async def test_resolve_mr_discussion_success(self, mock_gitlab_constructor):
        """Test successful discussion resolution."""
        mock_gl_instance = MagicMock()
        mock_gitlab_constructor.return_value = mock_gl_instance
        mock_gl_instance.auth.return_value = None

        mock_project = MagicMock()
        mock_gl_instance.projects.get.return_value = mock_project

        mock_mr = MagicMock()
        mock_project.mergerequests.get.return_value = mock_mr

        mock_discussion = MagicMock()
        mock_discussion.id = "discussion-1"
        mock_discussion.resolved = False
        mock_mr.discussions.get.return_value = mock_discussion

        tracker = GitLabTracker("tracker-1", "api-key", {"project_id": "proj-1"})
        result = await tracker.resolve_mr_discussion("1", "discussion-1", True)

        self.assertEqual(result["id"], "discussion-1")
        self.assertTrue(result["resolved"])
        mock_discussion.save.assert_called_once()

    @patch("preloop.sync.trackers.gitlab.gitlab.Gitlab")
    async def test_unresolve_mr_discussion_success(self, mock_gitlab_constructor):
        """Test successful discussion unresolve."""
        mock_gl_instance = MagicMock()
        mock_gitlab_constructor.return_value = mock_gl_instance
        mock_gl_instance.auth.return_value = None

        mock_project = MagicMock()
        mock_gl_instance.projects.get.return_value = mock_project

        mock_mr = MagicMock()
        mock_project.mergerequests.get.return_value = mock_mr

        mock_discussion = MagicMock()
        mock_discussion.id = "discussion-1"
        mock_discussion.resolved = True
        mock_mr.discussions.get.return_value = mock_discussion

        tracker = GitLabTracker("tracker-1", "api-key", {"project_id": "proj-1"})
        result = await tracker.resolve_mr_discussion("1", "discussion-1", False)

        self.assertEqual(result["id"], "discussion-1")
        self.assertFalse(result["resolved"])
        mock_discussion.save.assert_called_once()

    @patch("preloop.sync.trackers.gitlab.gitlab.Gitlab")
    async def test_resolve_mr_discussion_no_project_id(self, mock_gitlab_constructor):
        """Test resolve fails without project_id."""
        mock_gl_instance = MagicMock()
        mock_gitlab_constructor.return_value = mock_gl_instance
        mock_gl_instance.auth.return_value = None

        tracker = GitLabTracker("tracker-1", "api-key", {})

        with self.assertRaises(TrackerResponseError) as context:
            await tracker.resolve_mr_discussion("1", "discussion-1", True)

        self.assertIn("Project ID not found", str(context.exception))

    @patch("preloop.sync.trackers.gitlab.gitlab.Gitlab")
    async def test_resolve_mr_discussion_api_error(self, mock_gitlab_constructor):
        """Test resolve handles API errors."""
        mock_gl_instance = MagicMock()
        mock_gitlab_constructor.return_value = mock_gl_instance
        mock_gl_instance.auth.return_value = None

        mock_project = MagicMock()
        mock_gl_instance.projects.get.return_value = mock_project

        mock_mr = MagicMock()
        mock_mr.discussions.get.side_effect = Exception("Discussion not found")
        mock_project.mergerequests.get.return_value = mock_mr

        tracker = GitLabTracker("tracker-1", "api-key", {"project_id": "proj-1"})

        with self.assertRaises(TrackerResponseError) as context:
            await tracker.resolve_mr_discussion("1", "invalid-id", True)

        self.assertIn("Failed to resolve MR discussion", str(context.exception))


class TestCreateMrDiscussion(unittest.IsolatedAsyncioTestCase):
    """Tests for create_mr_discussion method."""

    @patch("preloop.sync.trackers.gitlab.gitlab.Gitlab")
    async def test_create_mr_discussion_success(self, mock_gitlab_constructor):
        """Test successful MR discussion creation."""
        mock_gl_instance = MagicMock()
        mock_gitlab_constructor.return_value = mock_gl_instance
        mock_gl_instance.auth.return_value = None

        mock_project = MagicMock()
        mock_gl_instance.projects.get.return_value = mock_project

        mock_mr = MagicMock()
        mock_project.mergerequests.get.return_value = mock_mr

        mock_discussion = MagicMock()
        mock_discussion.id = "new-discussion-1"
        mock_discussion.individual_note = True
        mock_discussion.notes = [
            {
                "id": 201,
                "body": "This is a new comment",
                "author": {"username": "user1"},
            }
        ]
        mock_mr.discussions.create.return_value = mock_discussion

        tracker = GitLabTracker("tracker-1", "api-key", {"project_id": "proj-1"})
        result = await tracker.create_mr_discussion("1", "This is a new comment")

        self.assertEqual(result["id"], "new-discussion-1")
        self.assertTrue(result["individual_note"])
        self.assertEqual(len(result["notes"]), 1)
        self.assertEqual(result["notes"][0]["body"], "This is a new comment")
        mock_mr.discussions.create.assert_called_once_with(
            {"body": "This is a new comment"}
        )

    @patch("preloop.sync.trackers.gitlab.gitlab.Gitlab")
    async def test_create_mr_discussion_with_position(self, mock_gitlab_constructor):
        """Test MR discussion creation with diff position."""
        mock_gl_instance = MagicMock()
        mock_gitlab_constructor.return_value = mock_gl_instance
        mock_gl_instance.auth.return_value = None

        mock_project = MagicMock()
        mock_gl_instance.projects.get.return_value = mock_project

        mock_mr = MagicMock()
        mock_project.mergerequests.get.return_value = mock_mr

        mock_discussion = MagicMock()
        mock_discussion.id = "new-discussion-2"
        mock_discussion.individual_note = False
        mock_discussion.notes = [
            {
                "id": 202,
                "body": "Line comment",
                "author": {"username": "reviewer"},
            }
        ]
        mock_mr.discussions.create.return_value = mock_discussion

        position = {
            "base_sha": "abc123",
            "start_sha": "def456",
            "head_sha": "ghi789",
            "position_type": "text",
            "new_path": "src/main.py",
            "new_line": 42,
            "old_path": "src/main.py",
            "old_line": 40,
        }

        tracker = GitLabTracker("tracker-1", "api-key", {"project_id": "proj-1"})
        result = await tracker.create_mr_discussion("1", "Line comment", position)

        self.assertEqual(result["id"], "new-discussion-2")
        self.assertEqual(result["notes"][0]["body"], "Line comment")

        # Verify the position was included in the create call
        create_call_args = mock_mr.discussions.create.call_args[0][0]
        self.assertEqual(create_call_args["body"], "Line comment")
        self.assertIn("position", create_call_args)
        self.assertEqual(create_call_args["position"]["base_sha"], "abc123")
        self.assertEqual(create_call_args["position"]["new_path"], "src/main.py")
        self.assertEqual(create_call_args["position"]["new_line"], 42)
        self.assertEqual(create_call_args["position"]["old_path"], "src/main.py")
        self.assertEqual(create_call_args["position"]["old_line"], 40)

    @patch("preloop.sync.trackers.gitlab.gitlab.Gitlab")
    async def test_create_mr_discussion_no_project_id(self, mock_gitlab_constructor):
        """Test create discussion fails without project_id."""
        mock_gl_instance = MagicMock()
        mock_gitlab_constructor.return_value = mock_gl_instance
        mock_gl_instance.auth.return_value = None

        tracker = GitLabTracker("tracker-1", "api-key", {})

        with self.assertRaises(TrackerResponseError) as context:
            await tracker.create_mr_discussion("1", "Comment body")

        self.assertIn("Project ID not found", str(context.exception))

    @patch("preloop.sync.trackers.gitlab.gitlab.Gitlab")
    async def test_create_mr_discussion_api_error(self, mock_gitlab_constructor):
        """Test create discussion handles API errors."""
        mock_gl_instance = MagicMock()
        mock_gitlab_constructor.return_value = mock_gl_instance
        mock_gl_instance.auth.return_value = None

        mock_project = MagicMock()
        mock_gl_instance.projects.get.return_value = mock_project

        mock_mr = MagicMock()
        mock_mr.discussions.create.side_effect = Exception("API Error")
        mock_project.mergerequests.get.return_value = mock_mr

        tracker = GitLabTracker("tracker-1", "api-key", {"project_id": "proj-1"})

        with self.assertRaises(TrackerResponseError) as context:
            await tracker.create_mr_discussion("1", "Comment body")

        self.assertIn("Failed to create MR discussion", str(context.exception))

    @patch("preloop.sync.trackers.gitlab.gitlab.Gitlab")
    async def test_create_mr_discussion_with_minimal_position(
        self, mock_gitlab_constructor
    ):
        """Test MR discussion creation with minimal position (no old_path/old_line)."""
        mock_gl_instance = MagicMock()
        mock_gitlab_constructor.return_value = mock_gl_instance
        mock_gl_instance.auth.return_value = None

        mock_project = MagicMock()
        mock_gl_instance.projects.get.return_value = mock_project

        mock_mr = MagicMock()
        mock_project.mergerequests.get.return_value = mock_mr

        mock_discussion = MagicMock()
        mock_discussion.id = "new-discussion-3"
        mock_discussion.individual_note = False
        mock_discussion.notes = []  # Empty notes to test edge case
        mock_mr.discussions.create.return_value = mock_discussion

        position = {
            "base_sha": "abc123",
            "start_sha": "def456",
            "head_sha": "ghi789",
            "new_path": "src/new_file.py",
            "new_line": 10,
        }

        tracker = GitLabTracker("tracker-1", "api-key", {"project_id": "proj-1"})
        result = await tracker.create_mr_discussion("1", "New file comment", position)

        self.assertEqual(result["id"], "new-discussion-3")

        # Verify position doesn't include old_path/old_line when not provided
        create_call_args = mock_mr.discussions.create.call_args[0][0]
        self.assertNotIn("old_path", create_call_args["position"])
        self.assertNotIn("old_line", create_call_args["position"])


if __name__ == "__main__":
    unittest.main()
