"""Tests for the Jira Comments mixin."""

from unittest.mock import Mock

import pytest

from mcp_atlassian.jira.comments import CommentsMixin


class TestCommentsMixin:
    """Tests for the CommentsMixin class."""

    @pytest.fixture
    def comments_mixin(self, jira_client):
        """Create a CommentsMixin instance with mocked dependencies."""
        mixin = CommentsMixin(config=jira_client.config)
        mixin.jira = jira_client.jira

        # Set up a mock preprocessor with markdown_to_jira method
        mixin.preprocessor = Mock()
        mixin.preprocessor.markdown_to_jira = Mock(
            return_value="*This* is _Jira_ formatted"
        )

        # Mock the clean_text method
        mixin._clean_text = Mock(side_effect=lambda x: x)

        return mixin

    def test_get_issue_comments_basic(self, comments_mixin):
        """Test get_issue_comments with basic data."""
        # Setup mock response
        comments_mixin.jira.issue_get_comments.return_value = {
            "comments": [
                {
                    "id": "10001",
                    "body": "This is a comment",
                    "created": "2024-01-01T10:00:00.000+0000",
                    "updated": "2024-01-01T11:00:00.000+0000",
                    "author": {"displayName": "John Doe"},
                }
            ]
        }

        # Call the method
        result = comments_mixin.get_issue_comments("TEST-123")

        # Verify
        comments_mixin.jira.issue_get_comments.assert_called_once_with("TEST-123")
        assert len(result) == 1
        assert result[0]["id"] == "10001"
        assert result[0]["body"] == "This is a comment"
        assert result[0]["created"] == "2024-01-01 10:00:00+00:00"  # Parsed date
        assert result[0]["author"] == "John Doe"

    def test_get_issue_comments_with_limit(self, comments_mixin):
        """Test get_issue_comments with limit parameter."""
        # Setup mock response with multiple comments
        comments_mixin.jira.issue_get_comments.return_value = {
            "comments": [
                {
                    "id": "10001",
                    "body": "First comment",
                    "created": "2024-01-01T10:00:00.000+0000",
                    "author": {"displayName": "John Doe"},
                },
                {
                    "id": "10002",
                    "body": "Second comment",
                    "created": "2024-01-02T10:00:00.000+0000",
                    "author": {"displayName": "Jane Smith"},
                },
                {
                    "id": "10003",
                    "body": "Third comment",
                    "created": "2024-01-03T10:00:00.000+0000",
                    "author": {"displayName": "Bob Johnson"},
                },
            ]
        }

        # Call the method with limit=2
        result = comments_mixin.get_issue_comments("TEST-123", limit=2)

        # Verify
        comments_mixin.jira.issue_get_comments.assert_called_once_with("TEST-123")
        assert len(result) == 2  # Only 2 comments should be returned
        assert result[0]["id"] == "10001"
        assert result[1]["id"] == "10002"
        # Third comment shouldn't be included due to limit

    def test_get_issue_comments_with_missing_fields(self, comments_mixin):
        """Test get_issue_comments with missing fields in the response."""
        # Setup mock response with missing fields
        comments_mixin.jira.issue_get_comments.return_value = {
            "comments": [
                {
                    "id": "10001",
                    # Missing body field
                    "created": "2024-01-01T10:00:00.000+0000",
                    # Missing author field
                },
                {
                    # Missing id field
                    "body": "Second comment",
                    # Missing created field
                    "author": {},  # Empty author object
                },
                {
                    "id": "10003",
                    "body": "Third comment",
                    "created": "2024-01-03T10:00:00.000+0000",
                    "author": {"name": "user123"},  # Using name instead of displayName
                },
            ]
        }

        # Call the method
        result = comments_mixin.get_issue_comments("TEST-123")

        # Verify
        assert len(result) == 3
        assert result[0]["id"] == "10001"
        assert result[0]["body"] == ""  # Should default to empty string
        assert result[0]["author"] == "Unknown"  # Should default to Unknown

        assert (
            "id" not in result[1] or not result[1]["id"]
        )  # Should be missing or empty
        assert result[1]["author"] == "Unknown"  # Should default to Unknown

        assert (
            result[2]["author"] == "Unknown"
        )  # Should use Unknown when only name is available

    def test_get_issue_comments_with_empty_response(self, comments_mixin):
        """Test get_issue_comments with an empty response."""
        # Setup mock response with no comments
        comments_mixin.jira.issue_get_comments.return_value = {"comments": []}

        # Call the method
        result = comments_mixin.get_issue_comments("TEST-123")

        # Verify
        assert len(result) == 0  # Should return an empty list

    def test_get_issue_comments_with_error(self, comments_mixin):
        """Test get_issue_comments with an error response."""
        # Setup mock to raise exception
        comments_mixin.jira.issue_get_comments.side_effect = Exception("API Error")

        # Verify it raises the wrapped exception
        with pytest.raises(Exception, match="Error getting comments"):
            comments_mixin.get_issue_comments("TEST-123")

    def test_add_comment_basic(self, comments_mixin):
        """Test add_comment with basic data (Cloud → ADF via v3 API)."""
        # Setup mock response for v3 API path
        mock_response = {
            "id": "10001",
            "body": "This is a comment",
            "created": "2024-01-01T10:00:00.000+0000",
            "author": {"displayName": "John Doe"},
        }
        comments_mixin._post_api3 = Mock(return_value=mock_response)

        # Call the method
        result = comments_mixin.add_comment("TEST-123", "Test comment")

        # On Cloud, ADF goes through _post_api3 (not issue_add_comment)
        comments_mixin._post_api3.assert_called_once()
        call_args = comments_mixin._post_api3.call_args
        assert call_args[0][0] == "issue/TEST-123/comment"
        adf_body = call_args[0][1]["body"]
        assert isinstance(adf_body, dict)
        assert adf_body["version"] == 1
        assert adf_body["type"] == "doc"
        # preprocessor.markdown_to_jira should NOT be called on Cloud
        comments_mixin.preprocessor.markdown_to_jira.assert_not_called()
        assert result["id"] == "10001"
        assert result["body"] == "This is a comment"
        assert result["created"] == "2024-01-01 10:00:00+00:00"
        assert result["author"] == "John Doe"

    def test_add_comment_with_markdown_conversion(self, comments_mixin):
        """Test add_comment with markdown conversion (Cloud → ADF via v3)."""
        # Setup mock response for v3 API path
        mock_response = {
            "id": "10001",
            "body": "Heading and content",
            "created": "2024-01-01T10:00:00.000+0000",
            "author": {"displayName": "John Doe"},
        }
        comments_mixin._post_api3 = Mock(return_value=mock_response)

        markdown_comment = "# Heading 1\n\nThis is **bold** text."

        # Call the method
        result = comments_mixin.add_comment("TEST-123", markdown_comment)

        # On Cloud, should produce ADF via v3 API, not call preprocessor
        call_args = comments_mixin._post_api3.call_args
        adf_body = call_args[0][1]["body"]
        assert isinstance(adf_body, dict)
        assert adf_body["version"] == 1
        comments_mixin.preprocessor.markdown_to_jira.assert_not_called()
        assert result["body"] == "Heading and content"

    def test_add_comment_with_empty_comment(self, comments_mixin):
        """Test add_comment with an empty comment (Cloud → minimal ADF)."""
        # Setup mock response for v3 API path
        mock_response = {
            "id": "10001",
            "body": "",
            "created": "2024-01-01T10:00:00.000+0000",
            "author": {"displayName": "John Doe"},
        }
        comments_mixin._post_api3 = Mock(return_value=mock_response)

        # Call the method with empty comment
        result = comments_mixin.add_comment("TEST-123", "")

        # On Cloud, empty string produces a minimal ADF dict via v3 API
        call_args = comments_mixin._post_api3.call_args
        adf_body = call_args[0][1]["body"]
        assert isinstance(adf_body, dict)
        assert adf_body["version"] == 1
        comments_mixin.preprocessor.markdown_to_jira.assert_not_called()
        assert result["body"] == ""

    def test_add_comment_with_restricted_visibility(self, comments_mixin):
        """Test add_comment with visibility set (Cloud → ADF via v3)."""
        # Setup mock response for v3 API path
        mock_response = {
            "id": "10001",
            "body": "This is a comment",
            "created": "2024-01-01T10:00:00.000+0000",
            "author": {"displayName": "John Doe"},
        }
        comments_mixin._post_api3 = Mock(return_value=mock_response)

        # Call the method
        result = comments_mixin.add_comment(
            "TEST-123", "Test comment", {"type": "group", "value": "restricted"}
        )

        # Verify ADF via v3 API with visibility
        call_args = comments_mixin._post_api3.call_args
        assert call_args[0][0] == "issue/TEST-123/comment"
        payload = call_args[0][1]
        assert isinstance(payload["body"], dict)
        assert payload["body"]["version"] == 1
        assert payload["visibility"] == {"type": "group", "value": "restricted"}
        comments_mixin.preprocessor.markdown_to_jira.assert_not_called()
        assert result["id"] == "10001"
        assert result["body"] == "This is a comment"
        assert result["created"] == "2024-01-01 10:00:00+00:00"
        assert result["author"] == "John Doe"

    def test_add_comment_with_error(self, comments_mixin):
        """Test add_comment with an error response."""
        # Setup mock to raise exception (Cloud uses _post_api3)
        comments_mixin._post_api3 = Mock(side_effect=Exception("API Error"))

        # Verify it raises the wrapped exception
        with pytest.raises(Exception, match="Error adding comment"):
            comments_mixin.add_comment("TEST-123", "Test comment")

    def test_edit_comment_basic(self, comments_mixin):
        """Test edit_comment with basic data (Cloud → ADF via v3)."""
        # Setup mock response for v3 API path
        mock_response = {
            "id": "10001",
            "body": "This is an updated comment",
            "updated": "2024-01-01T12:00:00.000+0000",
            "author": {"displayName": "John Doe"},
        }
        comments_mixin._put_api3 = Mock(return_value=mock_response)

        # Call the method
        result = comments_mixin.edit_comment("TEST-123", "10001", "Updated comment")

        # On Cloud, ADF goes through _put_api3
        comments_mixin._put_api3.assert_called_once()
        call_args = comments_mixin._put_api3.call_args
        assert call_args[0][0] == "issue/TEST-123/comment/10001"
        adf_body = call_args[0][1]["body"]
        assert isinstance(adf_body, dict)
        assert adf_body["version"] == 1
        comments_mixin.preprocessor.markdown_to_jira.assert_not_called()
        assert result["id"] == "10001"
        assert result["body"] == "This is an updated comment"
        assert result["updated"] == "2024-01-01 12:00:00+00:00"
        assert result["author"] == "John Doe"

    def test_edit_comment_with_markdown_conversion(self, comments_mixin):
        """Test edit_comment with markdown conversion (Cloud → ADF via v3)."""
        # Setup mock response for v3 API path
        mock_response = {
            "id": "10001",
            "body": "Updated content",
            "updated": "2024-01-01T12:00:00.000+0000",
            "author": {"displayName": "John Doe"},
        }
        comments_mixin._put_api3 = Mock(return_value=mock_response)

        markdown_comment = "# Updated Heading\n\nThis is **updated** text."

        # Call the method
        result = comments_mixin.edit_comment("TEST-123", "10001", markdown_comment)

        # On Cloud, should produce ADF via v3 API
        call_args = comments_mixin._put_api3.call_args
        adf_body = call_args[0][1]["body"]
        assert isinstance(adf_body, dict)
        assert adf_body["version"] == 1
        comments_mixin.preprocessor.markdown_to_jira.assert_not_called()
        assert result["body"] == "Updated content"

    def test_edit_comment_with_empty_comment(self, comments_mixin):
        """Test edit_comment with an empty comment (Cloud → minimal ADF)."""
        # Setup mock response for v3 API path
        mock_response = {
            "id": "10001",
            "body": "",
            "updated": "2024-01-01T12:00:00.000+0000",
            "author": {"displayName": "John Doe"},
        }
        comments_mixin._put_api3 = Mock(return_value=mock_response)

        # Call the method with empty comment
        result = comments_mixin.edit_comment("TEST-123", "10001", "")

        # On Cloud, empty string produces a minimal ADF dict via v3 API
        call_args = comments_mixin._put_api3.call_args
        adf_body = call_args[0][1]["body"]
        assert isinstance(adf_body, dict)
        assert adf_body["version"] == 1
        comments_mixin.preprocessor.markdown_to_jira.assert_not_called()
        assert result["body"] == ""

    def test_edit_comment_with_restricted_visibility(self, comments_mixin):
        """Test edit_comment with visibility set (Cloud → ADF via v3)."""
        # Setup mock response for v3 API path
        mock_response = {
            "id": "10001",
            "body": "This is an updated comment",
            "updated": "2024-01-01T12:00:00.000+0000",
            "author": {"displayName": "John Doe"},
        }
        comments_mixin._put_api3 = Mock(return_value=mock_response)

        # Call the method
        result = comments_mixin.edit_comment(
            "TEST-123",
            "10001",
            "Updated comment",
            {"type": "group", "value": "restricted"},
        )

        # Verify ADF via v3 API with visibility
        call_args = comments_mixin._put_api3.call_args
        assert call_args[0][0] == "issue/TEST-123/comment/10001"
        payload = call_args[0][1]
        assert isinstance(payload["body"], dict)
        assert payload["body"]["version"] == 1
        assert payload["visibility"] == {"type": "group", "value": "restricted"}
        comments_mixin.preprocessor.markdown_to_jira.assert_not_called()
        assert result["id"] == "10001"
        assert result["body"] == "This is an updated comment"
        assert result["updated"] == "2024-01-01 12:00:00+00:00"
        assert result["author"] == "John Doe"

    def test_edit_comment_with_error(self, comments_mixin):
        """Test edit_comment with an error response."""
        # Setup mock to raise exception (Cloud uses _put_api3)
        comments_mixin._put_api3 = Mock(side_effect=Exception("API Error"))

        # Verify it raises the wrapped exception
        with pytest.raises(Exception, match="Error editing comment"):
            comments_mixin.edit_comment("TEST-123", "10001", "Updated comment")

    def test_markdown_to_jira_cloud(self, comments_mixin):
        """Test _markdown_to_jira returns ADF dict on Cloud."""
        result = comments_mixin._markdown_to_jira("Markdown text")
        # Cloud config → ADF dict
        assert isinstance(result, dict)
        assert result["version"] == 1
        assert result["type"] == "doc"
        comments_mixin.preprocessor.markdown_to_jira.assert_not_called()

    def test_markdown_to_jira_cloud_empty(self, comments_mixin):
        """Test _markdown_to_jira with empty text on Cloud returns ADF."""
        result = comments_mixin._markdown_to_jira("")
        assert isinstance(result, dict)
        assert result["version"] == 1
        comments_mixin.preprocessor.markdown_to_jira.assert_not_called()

    # --- Server/DC path tests ---

    @pytest.fixture
    def server_comments_mixin(self, jira_config_factory):
        """Create a CommentsMixin configured for Server/DC."""
        config = jira_config_factory(url="https://jira.example.com")
        mixin = CommentsMixin(config=config)
        mixin.jira = Mock()
        mixin.preprocessor = Mock()
        mixin.preprocessor.markdown_to_jira = Mock(return_value="h1. Hello")
        mixin._clean_text = Mock(side_effect=lambda x: x)
        return mixin

    def test_markdown_to_jira_server_returns_string(self, server_comments_mixin):
        """Server/DC path returns wiki markup string."""
        result = server_comments_mixin._markdown_to_jira("# Hello")
        assert isinstance(result, str)
        assert result == "h1. Hello"
        server_comments_mixin.preprocessor.markdown_to_jira.assert_called_once()

    def test_add_comment_server_sends_string(self, server_comments_mixin):
        """Server/DC add_comment sends wiki markup string to API."""
        server_comments_mixin.jira.issue_add_comment.return_value = {
            "id": "10001",
            "body": "h1. Hello",
            "created": "2024-01-01T10:00:00.000+0000",
            "author": {"displayName": "Test User"},
        }
        result = server_comments_mixin.add_comment("TEST-123", "# Hello")
        call_args = server_comments_mixin.jira.issue_add_comment.call_args
        comment_arg = call_args[0][1]
        assert isinstance(comment_arg, str)
        assert result["body"] == "h1. Hello"

    def test_edit_comment_server_sends_string(self, server_comments_mixin):
        """Server/DC edit_comment sends wiki markup string to API."""
        server_comments_mixin.jira.issue_edit_comment.return_value = {
            "id": "10001",
            "body": "h1. Updated",
            "updated": "2024-01-01T11:00:00.000+0000",
            "author": {"displayName": "Test User"},
        }
        server_comments_mixin.preprocessor.markdown_to_jira.return_value = "h1. Updated"
        result = server_comments_mixin.edit_comment("TEST-123", "10001", "# Updated")
        call_args = server_comments_mixin.jira.issue_edit_comment.call_args
        comment_arg = call_args[0][2]
        assert isinstance(comment_arg, str)
        assert result["body"] == "h1. Updated"

    # --- ServiceDesk API (internal/public comments) tests ---

    SERVICEDESK_COMMENT_RESPONSE = {
        "id": 10001,
        "body": "Test comment",
        "public": True,
        "created": {
            "iso8601": "2024-01-01T10:00:00.000+0000",
            "jira": "2024-01-01T10:00:00.000+0000",
            "friendly": "Today 10:00 AM",
            "epochMillis": 1704099600000,
        },
        "author": {
            "accountId": "test-id",
            "displayName": "Test User",
        },
    }

    def test_add_comment_servicedesk_public(self, comments_mixin):
        """public=True routes through ServiceDesk API."""
        response = {**self.SERVICEDESK_COMMENT_RESPONSE, "public": True}
        comments_mixin.jira.post.return_value = response

        result = comments_mixin.add_comment("TEST-123", "Test comment", public=True)

        comments_mixin.jira.post.assert_called_once()
        call_args = comments_mixin.jira.post.call_args
        assert "rest/servicedeskapi/request/TEST-123/comment" in str(call_args)
        assert call_args[1]["data"] == {
            "body": "Test comment",
            "public": True,
        }
        # Verify experimental header is included
        headers = call_args[1]["headers"]
        assert headers["X-ExperimentalApi"] == "opt-in"
        assert result["public"] is True
        assert result["id"] == "10001"
        assert result["author"] == "Test User"

    def test_add_comment_servicedesk_internal(self, comments_mixin):
        """public=False routes through ServiceDesk API as internal."""
        response = {**self.SERVICEDESK_COMMENT_RESPONSE, "public": False}
        comments_mixin.jira.post.return_value = response

        result = comments_mixin.add_comment("TEST-123", "Internal note", public=False)

        call_args = comments_mixin.jira.post.call_args
        assert call_args[1]["data"] == {
            "body": "Internal note",
            "public": False,
        }
        assert result["public"] is False

    def test_add_comment_servicedesk_cloud(self, comments_mixin):
        """public=True on Cloud uses ServiceDesk API, not ADF/v3."""
        response = {**self.SERVICEDESK_COMMENT_RESPONSE}
        comments_mixin.jira.post.return_value = response
        comments_mixin._post_api3 = Mock()

        comments_mixin.add_comment("TEST-123", "Test", public=True)

        # ServiceDesk path should use jira.post, NOT _post_api3
        comments_mixin.jira.post.assert_called_once()
        comments_mixin._post_api3.assert_not_called()

    def test_add_comment_servicedesk_403(self, comments_mixin):
        """public=True on non-JSM project gives clear 403 error."""
        comments_mixin.jira.post.side_effect = Exception("403 Client Error: Forbidden")

        with pytest.raises(Exception, match="not a JSM service desk issue"):
            comments_mixin.add_comment("TEST-123", "Test", public=True)

    def test_add_comment_servicedesk_404(self, comments_mixin):
        """public=True on non-existent issue gives clear 404 error."""
        comments_mixin.jira.post.side_effect = Exception("404 Client Error: Not Found")

        with pytest.raises(Exception, match="not a JSM service desk issue"):
            comments_mixin.add_comment("TEST-123", "Test", public=True)

    def test_add_comment_public_with_visibility_raises(self, comments_mixin):
        """public + visibility together raises ValueError."""
        with pytest.raises(ValueError, match="Cannot use both"):
            comments_mixin.add_comment(
                "TEST-123",
                "Test",
                visibility={"type": "group", "value": "jira-users"},
                public=True,
            )

    def test_add_comment_public_none_uses_jira_api(self, comments_mixin):
        """public=None (default) uses normal Jira API path."""
        mock_response = {
            "id": "10001",
            "body": "Normal comment",
            "created": "2024-01-01T10:00:00.000+0000",
            "author": {"displayName": "John Doe"},
        }
        comments_mixin._post_api3 = Mock(return_value=mock_response)

        result = comments_mixin.add_comment("TEST-123", "Normal comment")

        # Should go through normal Jira path (ADF on Cloud)
        comments_mixin._post_api3.assert_called_once()
        # ServiceDesk post should NOT be called
        comments_mixin.jira.post.assert_not_called()
        assert result["id"] == "10001"
