"""
Tests for new Jira tracker methods (get_issue and get_comments).
"""

import unittest
from unittest.mock import AsyncMock, patch
from unittest import IsolatedAsyncioTestCase
from datetime import datetime

from preloop.sync.trackers.jira import JiraTracker
from preloop.sync.exceptions import TrackerResponseError


class TestJiraTrackerNewMethods(IsolatedAsyncioTestCase):
    """Test Jira tracker's new get_issue and get_comments methods."""

    def setUp(self):
        """Set up test fixtures."""
        self.connection_details = {
            "url": "https://test.atlassian.net",
            "username": "test@example.com",
            "project_key": "TEST",
        }
        # Mock the JIRA client to avoid real API calls during initialization
        with patch("preloop.sync.trackers.jira.JIRA"):
            self.tracker = JiraTracker(
                "tracker-1", "api-token", self.connection_details
            )
        # Set base_url attribute that is used in URL construction
        self.tracker.base_url = "https://test.atlassian.net"

    async def test_get_issue_success(self):
        """Test successful issue retrieval."""
        # Arrange
        issue_data = {
            "id": "12345",
            "key": "TEST-123",
            "fields": {
                "summary": "Test Issue",
                "description": "Issue description",
                "status": {"id": "1", "name": "Open", "statusCategory": {"key": "new"}},
                "created": "2023-01-01T10:00:00.000+0000",
                "updated": "2023-01-02T11:00:00.000+0000",
                "labels": ["bug", "critical"],
                "assignee": {
                    "accountId": "123",
                    "displayName": "testuser",
                    "avatarUrls": {"48x48": "https://example.com/avatar.png"},
                },
                "reporter": {
                    "accountId": "456",
                    "displayName": "reporter",
                    "avatarUrls": {"48x48": "https://example.com/reporter.png"},
                },
            },
        }

        self.tracker._make_request = AsyncMock(return_value=issue_data)

        # Act
        result = await self.tracker.get_issue("TEST-123")

        # Assert - Now result is an Issue object, not a dict
        self.assertEqual(result.id, "12345")
        self.assertEqual(result.key, "TEST-123")
        self.assertEqual(result.title, "Test Issue")
        self.assertEqual(result.description, "Issue description")
        self.assertEqual(result.status.name, "Open")
        self.assertEqual(result.labels, ["bug", "critical"])
        self.assertIsNotNone(result.assignee)
        self.assertEqual(result.assignee.name, "testuser")

        # Verify API call
        self.tracker._make_request.assert_called_once_with(
            "GET", "issue/TEST-123", api_version="3"
        )

    async def test_get_issue_not_found(self):
        """Test error when issue is not found."""
        # Arrange
        self.tracker._make_request = AsyncMock(
            side_effect=TrackerResponseError("404 Not Found")
        )

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

        self.assertIn("Issue TEST-999 not found", str(context.exception))

    async def test_get_issue_no_assignee(self):
        """Test issue retrieval when no assignee is set."""
        # Arrange
        issue_data = {
            "id": "12345",
            "key": "TEST-123",
            "fields": {
                "summary": "Test Issue",
                "description": "Issue description",
                "status": {"id": "1", "name": "Open", "statusCategory": {"key": "new"}},
                "created": "2023-01-01T10:00:00.000+0000",
                "updated": "2023-01-02T11:00:00.000+0000",
                "labels": [],
                "assignee": None,  # No assignee
            },
        }

        self.tracker._make_request = AsyncMock(return_value=issue_data)

        # Act
        result = await self.tracker.get_issue("TEST-123")

        # Assert - Now result is an Issue object
        self.assertIsNone(result.assignee)

    async def test_get_comments_success(self):
        """Test successful comments retrieval."""
        # Arrange
        comments_data = {
            "comments": [
                {
                    "id": "1001",
                    "body": "First comment",
                    "created": "2023-01-01T12:00:00.000+0000",
                    "updated": "2023-01-01T12:00:00.000+0000",
                    "author": {
                        "accountId": "101",
                        "displayName": "Test User 1",
                        "avatarUrls": {"48x48": "https://avatar1.png"},
                    },
                },
                {
                    "id": "1002",
                    "body": "Second comment",
                    "created": "2023-01-01T13:00:00.000+0000",
                    "updated": "2023-01-01T13:00:00.000+0000",
                    "author": {
                        "accountId": "102",
                        "displayName": "Test User 2",
                        "avatarUrls": {"48x48": "https://avatar2.png"},
                    },
                },
            ]
        }

        self.tracker._make_request = AsyncMock(return_value=comments_data)

        # Act
        result = await self.tracker.get_comments("TEST-123")

        # 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, "Test User 1")
        self.assertEqual(result[0].author.avatar_url, "https://avatar1.png")
        self.assertEqual(
            result[0].url,
            "https://test.atlassian.net/browse/TEST-123?focusedCommentId=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, "Test User 2")

        # Verify API call
        self.tracker._make_request.assert_called_once_with(
            "GET", "issue/TEST-123/comment", api_version="3"
        )

    async def test_get_comments_not_found(self):
        """Test error when issue is not found for comments."""
        # Arrange
        self.tracker._make_request = AsyncMock(
            side_effect=TrackerResponseError("404 Not Found")
        )

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

        self.assertIn("Issue TEST-999 not found", str(context.exception))

    async def test_get_comments_no_author(self):
        """Test comments retrieval when comment has no author."""
        # Arrange
        comments_data = {
            "comments": [
                {
                    "id": "1001",
                    "body": "Anonymous comment",
                    "created": "2023-01-01T12:00:00.000+0000",
                    "updated": "2023-01-01T12:00:00.000+0000",
                    "author": {},  # Empty author dict instead of None
                }
            ]
        }

        self.tracker._make_request = AsyncMock(return_value=comments_data)

        # Act
        result = await self.tracker.get_comments("TEST-123")

        # Assert
        self.assertEqual(len(result), 1)
        self.assertEqual(result[0].id, "1001")
        self.assertEqual(result[0].body, "Anonymous comment")
        # When author is empty dict, it should create an IssueUser with default "Anonymous" name
        self.assertIsNotNone(result[0].author)
        self.assertEqual(result[0].author.id, "")
        self.assertEqual(result[0].author.name, "Anonymous")

    async def test_get_comments_empty_list(self):
        """Test comments retrieval when no comments exist."""
        # Arrange
        comments_data = {"comments": []}

        self.tracker._make_request = AsyncMock(return_value=comments_data)

        # Act
        result = await self.tracker.get_comments("TEST-123")

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

    async def test_get_issue_datetime_parsing_fallback(self):
        """Test datetime parsing fallback when date format is invalid."""
        # Arrange
        issue_data = {
            "id": "12345",
            "key": "TEST-123",
            "fields": {
                "summary": "Test Issue",
                "description": "Issue description",
                "status": {"id": "1", "name": "Open", "statusCategory": {"key": "new"}},
                "created": "invalid-date",  # Invalid date format
                "updated": "also-invalid",  # Invalid date format
                "labels": [],
                "assignee": None,
            },
        }

        self.tracker._make_request = AsyncMock(return_value=issue_data)

        # Act - This should raise an error as our parser expects valid ISO format
        # The _parse_jira_datetime method will return None for invalid dates,
        # but _map_jira_issue expects valid dates and uses datetime.now() as fallback
        result = await self.tracker.get_issue("TEST-123")

        # Assert - should not raise error and use current datetime as fallback
        self.assertIsInstance(result.created_at, datetime)
        self.assertIsInstance(result.updated_at, datetime)

    async def test_create_issue_success(self):
        """Test successful issue creation."""
        # Arrange
        from preloop.schemas.tracker_models import IssueCreate

        issue_create = IssueCreate(
            title="New Issue",
            description="Issue description",
            priority="High",
            labels=["bug"],
        )

        creation_response = {"id": "12345", "key": "TEST-123"}
        issue_data = {
            "id": "12345",
            "key": "TEST-123",
            "fields": {
                "summary": "New Issue",
                "description": "Issue description",
                "status": {"id": "1", "name": "Open", "statusCategory": {"key": "new"}},
                "created": "2023-01-01T10:00:00.000+0000",
                "updated": "2023-01-01T10:00:00.000+0000",
                "labels": ["bug"],
                "priority": {"id": "2", "name": "High"},
                "project": {"key": "TEST"},
            },
        }

        self.tracker._make_request = AsyncMock()
        self.tracker._make_request.side_effect = [creation_response, issue_data]

        # Act
        result = await self.tracker.create_issue("TEST", issue_create)

        # Assert
        self.assertEqual(result.id, "12345")
        self.assertEqual(result.key, "TEST-123")
        self.assertEqual(result.title, "New Issue")

        # Verify create request was made
        self.tracker._make_request.assert_any_call(
            "POST", "issue", json_data={"fields": unittest.mock.ANY}
        )

    async def test_update_issue_success(self):
        """Test successful issue update."""
        # Arrange
        from preloop.schemas.tracker_models import IssueUpdate

        issue_update = IssueUpdate(
            title="Updated Issue",
            description="Updated description",
            status="In Progress",
        )

        updated_issue_data = {
            "id": "12345",
            "key": "TEST-123",
            "fields": {
                "summary": "Updated Issue",
                "description": "Updated description",
                "status": {
                    "id": "2",
                    "name": "In Progress",
                    "statusCategory": {"key": "indeterminate"},
                },
                "created": "2023-01-01T10:00:00.000+0000",
                "updated": "2023-01-02T10:00:00.000+0000",
                "project": {"key": "TEST"},
            },
        }

        transitions_response = {
            "transitions": [
                {"id": "21", "to": {"name": "In Progress"}},
                {"id": "31", "to": {"name": "Done"}},
            ]
        }

        self.tracker._make_request = AsyncMock()
        self.tracker._make_request.side_effect = [
            None,  # PUT request
            transitions_response,  # GET transitions
            None,  # POST transition
            updated_issue_data,  # GET updated issue
        ]

        # Act
        result = await self.tracker.update_issue("TEST-123", issue_update)

        # Assert
        self.assertEqual(result.title, "Updated Issue")
        self.assertEqual(result.status.name, "In Progress")

    async def test_add_comment_success(self):
        """Test successful comment addition."""
        # Arrange
        comment_data = {
            "id": "1001",
            "body": {"type": "doc", "content": [{"type": "paragraph"}]},
            "created": "2023-01-01T12:00:00.000+0000",
            "updated": "2023-01-01T12:00:00.000+0000",
            "author": {
                "accountId": "123",
                "displayName": "Test User",
                "avatarUrls": {"48x48": "https://avatar.png"},
            },
        }

        self.tracker._make_request = AsyncMock(return_value=comment_data)

        # Act
        result = await self.tracker.add_comment("TEST-123", "Test comment")

        # Assert
        self.assertEqual(result.id, "1001")
        self.assertEqual(result.author.name, "Test User")

        # Verify API call
        self.tracker._make_request.assert_called_once_with(
            "POST", "issue/TEST-123/comment", json_data=unittest.mock.ANY
        )

    async def test_add_relation_success(self):
        """Test successful issue relation creation."""
        # Arrange
        self.tracker._make_request = AsyncMock(return_value=None)

        # Act
        result = await self.tracker.add_relation("TEST-123", "TEST-124", "blocks")

        # Assert
        self.assertTrue(result)

        # Verify API call
        self.tracker._make_request.assert_called_once_with(
            "POST", "issueLink", json_data=unittest.mock.ANY
        )

    async def test_search_issues_success(self):
        """Test successful issue search."""
        # Arrange
        from preloop.schemas.tracker_models import IssueFilter

        filter_params = IssueFilter(query="bug", status=["Open"], labels=["critical"])

        search_response = {
            "total": 1,
            "issues": [
                {
                    "id": "12345",
                    "key": "TEST-123",
                    "fields": {
                        "summary": "Bug Issue",
                        "description": "Bug description",
                        "status": {
                            "id": "1",
                            "name": "Open",
                            "statusCategory": {"key": "new"},
                        },
                        "created": "2023-01-01T10:00:00.000+0000",
                        "updated": "2023-01-02T11:00:00.000+0000",
                        "labels": ["critical"],
                        "project": {"key": "TEST"},
                    },
                }
            ],
        }

        self.tracker._make_request = AsyncMock(return_value=search_response)

        # Act
        issues, total = await self.tracker.search_issues(
            "TEST", filter_params, limit=10
        )

        # Assert
        self.assertEqual(total, 1)
        self.assertEqual(len(issues), 1)
        self.assertEqual(issues[0].key, "TEST-123")

        # Verify JQL query was built correctly
        call_args = self.tracker._make_request.call_args
        jql_data = call_args[1]["json_data"]
        self.assertIn("project = 'TEST'", jql_data["jql"])
        self.assertIn("bug", jql_data["jql"])

    async def test_get_project_metadata_success(self):
        """Test successful project metadata retrieval."""
        # Arrange
        project_data = {
            "id": "10000",
            "key": "TEST",
            "name": "Test Project",
            "description": "Test project description",
        }

        statuses_data = [
            {
                "statuses": [
                    {"id": "1", "name": "Open", "statusCategory": {"key": "new"}},
                    {
                        "id": "2",
                        "name": "In Progress",
                        "statusCategory": {"key": "indeterminate"},
                    },
                ]
            }
        ]

        priorities_data = [
            {"id": "1", "name": "Highest"},
            {"id": "2", "name": "High"},
        ]

        self.tracker._make_request = AsyncMock()
        self.tracker._make_request.side_effect = [
            project_data,
            statuses_data,
            priorities_data,
        ]

        # Act
        result = await self.tracker.get_project_metadata("TEST")

        # Assert
        self.assertEqual(result.key, "TEST")
        self.assertEqual(result.name, "Test Project")
        self.assertGreater(len(result.statuses), 0)
        self.assertGreater(len(result.priorities), 0)

    async def test_update_issue_with_nested_json_description(self):
        """Test updating issue with nested JSON in description (regression test for nested JSON bug)."""
        # Arrange
        from preloop.schemas.tracker_models import IssueUpdate

        # Simulate a description that comes from the database with nested JSON
        # This is the exact pattern that was causing the "Operation value must be a string" error
        nested_json_description = {
            "type": "doc",
            "version": 1,
            "content": [
                {
                    "type": "paragraph",
                    "content": [
                        {
                            "type": "text",
                            "text": '{"type": "doc", "version": 1, "content": [{"type": "paragraph", "content": [{"type": "text", "text": "Actual description text"}]}]}',
                        }
                    ],
                }
            ],
        }

        # Create IssueUpdate with string, then patch it to have a dict
        # This simulates what happens when Pydantic/API parsing converts JSON strings to dicts
        issue_update = IssueUpdate(
            title="Test Issue",
            description="placeholder",  # Valid string for validation
        )
        # Now manually set it to the problematic dict (simulating post-parsing state)
        issue_update.description = nested_json_description

        updated_issue_data = {
            "id": "12345",
            "key": "TEST-123",
            "fields": {
                "summary": "Test Issue",
                "description": {
                    "type": "doc",
                    "version": 1,
                    "content": [
                        {
                            "type": "paragraph",
                            "content": [
                                {"type": "text", "text": "Actual description text"}
                            ],
                        }
                    ],
                },
                "status": {"id": "1", "name": "Open", "statusCategory": {"key": "new"}},
                "created": "2023-01-01T10:00:00.000+0000",
                "updated": "2023-01-02T10:00:00.000+0000",
                "project": {"key": "TEST"},
            },
        }

        self.tracker._make_request = AsyncMock()
        self.tracker._make_request.side_effect = [
            None,  # PUT request
            updated_issue_data,  # GET updated issue
        ]

        # Act
        result = await self.tracker.update_issue("TEST-123", issue_update)

        # Assert
        self.assertEqual(result.title, "Test Issue")
        # The description should be unwrapped to just the actual text
        self.assertEqual(result.description, "Actual description text")

        # Verify the PUT request was called with properly unwrapped description
        put_call = self.tracker._make_request.call_args_list[0]
        fields_data = put_call[1]["json_data"]["fields"]

        # The description should be plain text (not ADF dict, not nested JSON strings)
        # Jira API v2 expects plain text, so the tracker converts ADF to plain text
        self.assertIn("description", fields_data)
        desc = fields_data["description"]
        self.assertIsInstance(desc, str)

        # Verify it's been unwrapped to just the actual text
        self.assertEqual(desc, "Actual description text")
        # Verify it's NOT still JSON (no escaped quotes or nested structures)
        self.assertNotIn('\\"', desc)
        self.assertNotIn('{"type":', desc)

    async def test_update_issue_with_string_nested_json_description(self):
        """Test updating issue when description is a JSON string (from API serialization)."""
        # Arrange
        from preloop.schemas.tracker_models import IssueUpdate

        # Simulate description as a JSON string (like what might come from Pydantic serialization)
        nested_json_string = '{"type": "doc", "version": 1, "content": [{"type": "paragraph", "content": [{"type": "text", "text": "{\\"type\\": \\"doc\\", \\"version\\": 1, \\"content\\": [{\\"type\\": \\"paragraph\\", \\"content\\": [{\\"type\\": \\"text\\", \\"text\\": \\"Real text\\"}]}]}"}]}]}'

        issue_update = IssueUpdate(
            title="Test Issue",
            description=nested_json_string,  # Pass as string
        )

        updated_issue_data = {
            "id": "12345",
            "key": "TEST-123",
            "fields": {
                "summary": "Test Issue",
                "description": {
                    "type": "doc",
                    "version": 1,
                    "content": [
                        {
                            "type": "paragraph",
                            "content": [{"type": "text", "text": "Real text"}],
                        }
                    ],
                },
                "status": {"id": "1", "name": "Open", "statusCategory": {"key": "new"}},
                "created": "2023-01-01T10:00:00.000+0000",
                "updated": "2023-01-02T10:00:00.000+0000",
                "project": {"key": "TEST"},
            },
        }

        self.tracker._make_request = AsyncMock()
        self.tracker._make_request.side_effect = [
            None,  # PUT request
            updated_issue_data,  # GET updated issue
        ]

        # Act
        result = await self.tracker.update_issue("TEST-123", issue_update)

        # Assert
        self.assertEqual(result.title, "Test Issue")
        self.assertEqual(result.description, "Real text")

        # Verify the PUT request unwrapped the nested JSON properly
        put_call = self.tracker._make_request.call_args_list[0]
        fields_data = put_call[1]["json_data"]["fields"]

        # Description should be plain text (Jira API v2 expects plain text)
        desc = fields_data["description"]
        self.assertIsInstance(desc, str)
        self.assertEqual(desc, "Real text")
        self.assertNotIn('\\"', desc)
