"""Tests for tool_utils module."""

import json
from typing import Any
from unittest.mock import MagicMock

import pytest

from src.tools.jira_executor import CommandResult
from src.tools.tool_utils import (
    _build_jql_from_params,
    _convert_adf_to_text,
    add_comment,
    add_to_sprint,
    assign_to_me,
    create_ticket,
    edit_ticket,
    get_ticket,
    list_sprints,
    list_tickets,
    move_ticket,
    open_ticket_in_browser,
    remove_from_sprint,
    update_ticket_description,
)


class TestBuildJqlFromParams:
    """Tests for _build_jql_from_params function."""

    def test_raw_jql_takes_precedence(self) -> None:
        """Test that raw JQL overrides other params."""
        jql = _build_jql_from_params(
            jql="project = TEST",
            assigned_to_me=True,
            status="Open",
        )
        assert jql == "project = TEST"

    def test_assigned_to_me_filter(self) -> None:
        """Test assigned_to_me filter."""
        jql = _build_jql_from_params(assigned_to_me=True)
        assert jql == "assignee = currentUser()"

    def test_unassigned_filter(self) -> None:
        """Test unassigned filter."""
        jql = _build_jql_from_params(unassigned=True)
        assert jql == "assignee is EMPTY"

    def test_status_filter(self) -> None:
        """Test status filter with normalization."""
        jql = _build_jql_from_params(status="in progress")
        assert jql == 'status = "In Progress"'

    def test_status_filter_custom(self) -> None:
        """Test status filter with custom status."""
        jql = _build_jql_from_params(status="Custom Status")
        assert jql == 'status = "Custom Status"'

    def test_project_filter(self) -> None:
        """Test project filter."""
        jql = _build_jql_from_params(project="TEST")
        assert jql == "project = TEST"

    def test_created_recently_filter(self) -> None:
        """Test created_recently filter."""
        jql = _build_jql_from_params(created_recently=True)
        assert jql == "created >= -7d"

    def test_updated_recently_filter(self) -> None:
        """Test updated_recently filter."""
        jql = _build_jql_from_params(updated_recently=True)
        assert jql == "updated >= -7d"

    def test_combined_filters(self) -> None:
        """Test multiple filters combined with AND."""
        jql = _build_jql_from_params(
            assigned_to_me=True,
            status="Open",
            project="TEST",
        )
        assert "assignee = currentUser()" in jql
        assert 'status = "Open"' in jql
        assert "project = TEST" in jql
        assert " AND " in jql

    def test_no_filters_returns_none(self) -> None:
        """Test that no filters returns None."""
        jql = _build_jql_from_params()
        assert jql is None

    def test_assigned_to_me_overrides_unassigned(self) -> None:
        """Test that assigned_to_me takes precedence over unassigned."""
        jql = _build_jql_from_params(assigned_to_me=True, unassigned=True)
        assert "currentUser()" in jql
        assert "EMPTY" not in jql


class TestConvertAdfToText:
    """Tests for _convert_adf_to_text function."""

    def test_simple_paragraph(self) -> None:
        """Test converting simple paragraph."""
        adf: dict[str, Any] = {
            "type": "doc",
            "version": 1,
            "content": [
                {
                    "type": "paragraph",
                    "content": [{"type": "text", "text": "Hello world"}],
                }
            ],
        }
        result = _convert_adf_to_text(adf)
        assert result == "Hello world"

    def test_multiple_paragraphs(self) -> None:
        """Test converting multiple paragraphs."""
        adf: dict[str, Any] = {
            "type": "doc",
            "version": 1,
            "content": [
                {
                    "type": "paragraph",
                    "content": [{"type": "text", "text": "First paragraph"}],
                },
                {
                    "type": "paragraph",
                    "content": [{"type": "text", "text": "Second paragraph"}],
                },
            ],
        }
        result = _convert_adf_to_text(adf)
        assert "First paragraph" in result
        assert "Second paragraph" in result

    def test_heading(self) -> None:
        """Test converting heading."""
        adf: dict[str, Any] = {
            "type": "doc",
            "version": 1,
            "content": [
                {
                    "type": "heading",
                    "attrs": {"level": 2},
                    "content": [{"type": "text", "text": "My Heading"}],
                }
            ],
        }
        result = _convert_adf_to_text(adf)
        assert result == "## My Heading"

    def test_bullet_list(self) -> None:
        """Test converting bullet list."""
        adf: dict[str, Any] = {
            "type": "doc",
            "version": 1,
            "content": [
                {
                    "type": "bulletList",
                    "content": [
                        {
                            "type": "listItem",
                            "content": [
                                {
                                    "type": "paragraph",
                                    "content": [
                                        {"type": "text", "text": "Item 1"}
                                    ],
                                }
                            ],
                        },
                        {
                            "type": "listItem",
                            "content": [
                                {
                                    "type": "paragraph",
                                    "content": [
                                        {"type": "text", "text": "Item 2"}
                                    ],
                                }
                            ],
                        },
                    ],
                }
            ],
        }
        result = _convert_adf_to_text(adf)
        assert "- Item 1" in result
        assert "- Item 2" in result

    def test_ordered_list(self) -> None:
        """Test converting ordered list."""
        adf: dict[str, Any] = {
            "type": "doc",
            "version": 1,
            "content": [
                {
                    "type": "orderedList",
                    "attrs": {"start": 1},
                    "content": [
                        {
                            "type": "listItem",
                            "content": [
                                {
                                    "type": "paragraph",
                                    "content": [
                                        {"type": "text", "text": "First"}
                                    ],
                                }
                            ],
                        },
                        {
                            "type": "listItem",
                            "content": [
                                {
                                    "type": "paragraph",
                                    "content": [
                                        {"type": "text", "text": "Second"}
                                    ],
                                }
                            ],
                        },
                    ],
                }
            ],
        }
        result = _convert_adf_to_text(adf)
        assert "1. First" in result
        assert "2. Second" in result

    def test_code_block(self) -> None:
        """Test converting code block."""
        adf: dict[str, Any] = {
            "type": "doc",
            "version": 1,
            "content": [
                {
                    "type": "codeBlock",
                    "attrs": {"language": "python"},
                    "content": [{"type": "text", "text": "print('hello')"}],
                }
            ],
        }
        result = _convert_adf_to_text(adf)
        assert "```python" in result
        assert "print('hello')" in result
        assert "```" in result

    def test_text_marks_bold(self) -> None:
        """Test converting bold text."""
        adf: dict[str, Any] = {
            "type": "doc",
            "version": 1,
            "content": [
                {
                    "type": "paragraph",
                    "content": [
                        {
                            "type": "text",
                            "text": "bold text",
                            "marks": [{"type": "strong"}],
                        }
                    ],
                }
            ],
        }
        result = _convert_adf_to_text(adf)
        assert "**bold text**" in result

    def test_text_marks_italic(self) -> None:
        """Test converting italic text."""
        adf: dict[str, Any] = {
            "type": "doc",
            "version": 1,
            "content": [
                {
                    "type": "paragraph",
                    "content": [
                        {
                            "type": "text",
                            "text": "italic text",
                            "marks": [{"type": "em"}],
                        }
                    ],
                }
            ],
        }
        result = _convert_adf_to_text(adf)
        assert "*italic text*" in result

    def test_text_marks_code(self) -> None:
        """Test converting inline code."""
        adf: dict[str, Any] = {
            "type": "doc",
            "version": 1,
            "content": [
                {
                    "type": "paragraph",
                    "content": [
                        {
                            "type": "text",
                            "text": "code",
                            "marks": [{"type": "code"}],
                        }
                    ],
                }
            ],
        }
        result = _convert_adf_to_text(adf)
        assert "`code`" in result

    def test_text_marks_strikethrough(self) -> None:
        """Test converting strikethrough text."""
        adf: dict[str, Any] = {
            "type": "doc",
            "version": 1,
            "content": [
                {
                    "type": "paragraph",
                    "content": [
                        {
                            "type": "text",
                            "text": "deleted",
                            "marks": [{"type": "strike"}],
                        }
                    ],
                }
            ],
        }
        result = _convert_adf_to_text(adf)
        assert "~~deleted~~" in result

    def test_horizontal_rule(self) -> None:
        """Test converting horizontal rule."""
        adf: dict[str, Any] = {
            "type": "doc",
            "version": 1,
            "content": [{"type": "rule"}],
        }
        result = _convert_adf_to_text(adf)
        assert "---" in result

    def test_blockquote(self) -> None:
        """Test converting blockquote."""
        adf: dict[str, Any] = {
            "type": "doc",
            "version": 1,
            "content": [
                {
                    "type": "blockquote",
                    "content": [
                        {
                            "type": "paragraph",
                            "content": [
                                {"type": "text", "text": "Quoted text"}
                            ],
                        }
                    ],
                }
            ],
        }
        result = _convert_adf_to_text(adf)
        assert "> Quoted text" in result

    def test_hard_break(self) -> None:
        """Test converting hard break."""
        adf: dict[str, Any] = {
            "type": "doc",
            "version": 1,
            "content": [
                {
                    "type": "paragraph",
                    "content": [
                        {"type": "text", "text": "Line 1"},
                        {"type": "hardBreak"},
                        {"type": "text", "text": "Line 2"},
                    ],
                }
            ],
        }
        result = _convert_adf_to_text(adf)
        assert "Line 1\nLine 2" in result

    def test_empty_document(self) -> None:
        """Test converting empty document."""
        adf: dict[str, Any] = {
            "type": "doc",
            "version": 1,
            "content": [],
        }
        result = _convert_adf_to_text(adf)
        assert result == ""


class TestListTickets:
    """Tests for list_tickets function."""

    def test_list_tickets_success(
        self, mock_execute_jira_command: MagicMock
    ) -> None:
        """Test listing tickets successfully."""
        mock_execute_jira_command.return_value = CommandResult(
            stdout="TEST-1\tTest ticket 1\tOpen\tHigh\tBug\tJohn Doe\nTEST-2\tTest ticket 2\tDone\tMedium\tStory\t",
            stderr="",
            exit_code=0,
        )

        tickets = list_tickets()

        assert len(tickets) == 2
        assert tickets[0].key == "TEST-1"
        assert tickets[0].status == "Open"
        assert tickets[0].assignee == "John Doe"
        assert tickets[1].key == "TEST-2"
        assert tickets[1].assignee is None

    def test_list_tickets_with_filters(
        self, mock_execute_jira_command: MagicMock
    ) -> None:
        """Test listing tickets with filters."""
        mock_execute_jira_command.return_value = CommandResult(
            stdout="TEST-1\tTest\tOpen\tHigh\tBug\t",
            stderr="",
            exit_code=0,
        )

        list_tickets(assigned_to_me=True, status="Open", project="TEST")

        # Verify the command args include JQL.
        call_args = mock_execute_jira_command.call_args[0][0]
        assert "--jql" in call_args

    def test_list_tickets_no_results(
        self, mock_execute_jira_command: MagicMock
    ) -> None:
        """Test listing tickets with no results."""
        mock_execute_jira_command.return_value = CommandResult(
            stdout="",
            stderr="No result found",
            exit_code=1,
        )

        tickets = list_tickets()

        assert tickets == []

    def test_list_tickets_with_limit(
        self, mock_execute_jira_command: MagicMock
    ) -> None:
        """Test listing tickets with limit."""
        mock_execute_jira_command.return_value = CommandResult(
            stdout="TEST-1\tTest\tOpen\tHigh\tBug\t",
            stderr="",
            exit_code=0,
        )

        list_tickets(limit=10)

        call_args = mock_execute_jira_command.call_args[0][0]
        assert "--paginate" in call_args
        assert "0:10" in call_args

    def test_list_tickets_with_ordering(
        self, mock_execute_jira_command: MagicMock
    ) -> None:
        """Test listing tickets with ordering."""
        mock_execute_jira_command.return_value = CommandResult(
            stdout="TEST-1\tTest\tOpen\tHigh\tBug\t",
            stderr="",
            exit_code=0,
        )

        list_tickets(order_by="created", order_direction="asc")

        call_args = mock_execute_jira_command.call_args[0][0]
        assert "--order-by" in call_args
        assert "created" in call_args
        assert "--reverse" in call_args

    def test_list_tickets_error(
        self, mock_execute_jira_command: MagicMock
    ) -> None:
        """Test listing tickets with error."""
        mock_execute_jira_command.return_value = CommandResult(
            stdout="",
            stderr="Permission denied",
            exit_code=1,
        )

        with pytest.raises(ValueError) as exc_info:
            list_tickets()

        assert "Failed to list tickets" in str(exc_info.value)


class TestGetTicket:
    """Tests for get_ticket function."""

    def test_get_ticket_success(
        self,
        mock_execute_jira_command: MagicMock,
        sample_raw_ticket_json: dict[str, Any],
    ) -> None:
        """Test getting ticket successfully."""
        mock_execute_jira_command.return_value = CommandResult(
            stdout=json.dumps(sample_raw_ticket_json),
            stderr="",
            exit_code=0,
        )

        ticket = get_ticket("TEST-123")

        assert ticket.key == "TEST-123"
        assert ticket.summary == "Test ticket summary"
        assert ticket.status == "Open"
        assert ticket.assignee == "John Doe"
        assert len(ticket.comments) == 1

    def test_get_ticket_with_comments(
        self,
        mock_execute_jira_command: MagicMock,
        sample_raw_ticket_json: dict[str, Any],
    ) -> None:
        """Test getting ticket with comments count."""
        mock_execute_jira_command.return_value = CommandResult(
            stdout=json.dumps(sample_raw_ticket_json),
            stderr="",
            exit_code=0,
        )

        get_ticket("TEST-123", comments=10)

        call_args = mock_execute_jira_command.call_args[0][0]
        assert "--comments" in call_args
        assert "10" in call_args

    def test_get_ticket_not_found(
        self, mock_execute_jira_command: MagicMock
    ) -> None:
        """Test getting nonexistent ticket."""
        mock_execute_jira_command.return_value = CommandResult(
            stdout="",
            stderr="Issue does not exist",
            exit_code=1,
        )

        with pytest.raises(ValueError) as exc_info:
            get_ticket("INVALID-999")

        assert "Failed to get ticket" in str(exc_info.value)

    def test_get_ticket_invalid_json(
        self, mock_execute_jira_command: MagicMock
    ) -> None:
        """Test getting ticket with invalid JSON response."""
        mock_execute_jira_command.return_value = CommandResult(
            stdout="not json",
            stderr="",
            exit_code=0,
        )

        with pytest.raises(ValueError) as exc_info:
            get_ticket("TEST-123")

        assert "Failed to parse" in str(exc_info.value)


class TestCreateTicket:
    """Tests for create_ticket function."""

    def test_create_ticket_success(
        self, mock_execute_jira_command: MagicMock
    ) -> None:
        """Test creating ticket successfully."""
        mock_execute_jira_command.return_value = CommandResult(
            stdout='{"key": "TEST-456", "self": "https://jira.example.com/rest/api/2/issue/12345"}',
            stderr="",
            exit_code=0,
        )

        result = create_ticket(
            project="TEST",
            issue_type="Bug",
            summary="New bug",
        )

        assert result.success is True
        assert result.ticket_key == "TEST-456"

    def test_create_ticket_with_all_options(
        self, mock_execute_jira_command: MagicMock
    ) -> None:
        """Test creating ticket with all options."""
        mock_execute_jira_command.return_value = CommandResult(
            stdout='{"key": "TEST-456", "self": "https://example.com"}',
            stderr="",
            exit_code=0,
        )

        create_ticket(
            project="TEST",
            issue_type="Story",
            summary="New story",
            description="Description text",
            priority="High",
            assignee="john.doe",
            labels=["label1", "label2"],
            components=["comp1"],
        )

        call_args = mock_execute_jira_command.call_args[0][0]
        assert "--priority" in call_args
        assert "High" in call_args
        assert "--assignee" in call_args
        assert "--label" in call_args
        assert "--component" in call_args

    def test_create_ticket_failure(
        self, mock_execute_jira_command: MagicMock
    ) -> None:
        """Test creating ticket with failure."""
        mock_execute_jira_command.return_value = CommandResult(
            stdout="",
            stderr="Project not found",
            exit_code=1,
        )

        result = create_ticket(
            project="INVALID",
            issue_type="Bug",
            summary="Test",
        )

        assert result.success is False
        assert "Project not found" in result.error


class TestMoveTicket:
    """Tests for move_ticket function."""

    def test_move_ticket_success(
        self, mock_execute_jira_command: MagicMock
    ) -> None:
        """Test moving ticket successfully."""
        # First call: get current status.
        # Second call: move ticket.
        mock_execute_jira_command.side_effect = [
            CommandResult(
                stdout="TEST-123\tOpen",
                stderr="",
                exit_code=0,
            ),
            CommandResult(
                stdout="",
                stderr="",
                exit_code=0,
            ),
        ]

        result = move_ticket("TEST-123", "In Progress")

        assert result.success is True
        assert result.previous_status == "Open"
        assert result.new_status == "In Progress"

    def test_move_ticket_normalizes_status(
        self, mock_execute_jira_command: MagicMock
    ) -> None:
        """Test moving ticket normalizes status."""
        mock_execute_jira_command.side_effect = [
            CommandResult(stdout="TEST-123\tOpen", stderr="", exit_code=0),
            CommandResult(stdout="", stderr="", exit_code=0),
        ]

        result = move_ticket("TEST-123", "in progress")

        assert result.new_status == "In Progress"

    def test_move_ticket_get_status_fails(
        self, mock_execute_jira_command: MagicMock
    ) -> None:
        """Test moving ticket when getting status fails."""
        mock_execute_jira_command.return_value = CommandResult(
            stdout="",
            stderr="Ticket not found",
            exit_code=1,
        )

        with pytest.raises(ValueError) as exc_info:
            move_ticket("INVALID-123", "Done")

        assert "Failed to get current status" in str(exc_info.value)


class TestAddComment:
    """Tests for add_comment function."""

    def test_add_comment_success(
        self, mock_execute_jira_command: MagicMock
    ) -> None:
        """Test adding comment successfully."""
        mock_execute_jira_command.return_value = CommandResult(
            stdout="",
            stderr="",
            exit_code=0,
        )

        result = add_comment("TEST-123", "This is a comment")

        assert result.success is True
        assert result.ticket_key == "TEST-123"

        # Verify stdin_input was passed.
        call_kwargs = mock_execute_jira_command.call_args[1]
        assert call_kwargs["stdin_input"] == "This is a comment"

    def test_add_comment_failure(
        self, mock_execute_jira_command: MagicMock
    ) -> None:
        """Test adding comment with failure."""
        mock_execute_jira_command.return_value = CommandResult(
            stdout="",
            stderr="Permission denied",
            exit_code=1,
        )

        with pytest.raises(ValueError) as exc_info:
            add_comment("TEST-123", "Comment")

        assert "Failed to add comment" in str(exc_info.value)


class TestAssignToMe:
    """Tests for assign_to_me function."""

    def test_assign_to_me_success(
        self, mock_execute_jira_command: MagicMock
    ) -> None:
        """Test assigning ticket to current user successfully."""
        mock_execute_jira_command.side_effect = [
            CommandResult(
                stdout="john.doe@example.com", stderr="", exit_code=0
            ),
            CommandResult(stdout="", stderr="", exit_code=0),
        ]

        result = assign_to_me("TEST-123")

        assert result.success is True
        assert result.assignee == "john.doe@example.com"
        assert result.ticket_key == "TEST-123"

    def test_assign_to_me_get_user_fails(
        self, mock_execute_jira_command: MagicMock
    ) -> None:
        """Test assigning when getting current user fails."""
        mock_execute_jira_command.return_value = CommandResult(
            stdout="",
            stderr="Auth error",
            exit_code=1,
        )

        with pytest.raises(ValueError) as exc_info:
            assign_to_me("TEST-123")

        assert "Failed to get current user" in str(exc_info.value)

    def test_assign_to_me_empty_user(
        self, mock_execute_jira_command: MagicMock
    ) -> None:
        """Test assigning when current user is empty."""
        mock_execute_jira_command.return_value = CommandResult(
            stdout="",
            stderr="",
            exit_code=0,
        )

        with pytest.raises(ValueError) as exc_info:
            assign_to_me("TEST-123")

        assert "Unable to determine current user" in str(exc_info.value)


class TestOpenTicketInBrowser:
    """Tests for open_ticket_in_browser function."""

    def test_open_ticket_success(
        self, mock_execute_jira_command: MagicMock
    ) -> None:
        """Test opening ticket in browser successfully."""
        mock_execute_jira_command.return_value = CommandResult(
            stdout="",
            stderr="",
            exit_code=0,
        )

        result = open_ticket_in_browser("TEST-123")

        assert "Successfully opened" in result
        assert "TEST-123" in result

    def test_open_ticket_failure(
        self, mock_execute_jira_command: MagicMock
    ) -> None:
        """Test opening ticket with failure."""
        mock_execute_jira_command.return_value = CommandResult(
            stdout="",
            stderr="Error opening browser",
            exit_code=1,
        )

        with pytest.raises(ValueError) as exc_info:
            open_ticket_in_browser("TEST-123")

        assert "Failed to open ticket" in str(exc_info.value)


class TestUpdateTicketDescription:
    """Tests for update_ticket_description function."""

    def test_update_description_success(
        self, mock_execute_jira_command: MagicMock
    ) -> None:
        """Test updating ticket description successfully."""
        mock_execute_jira_command.return_value = CommandResult(
            stdout="",
            stderr="",
            exit_code=0,
        )

        result = update_ticket_description("TEST-123", "New description")

        assert result.success is True
        assert result.ticket_key == "TEST-123"

        # Verify stdin_input was passed.
        call_kwargs = mock_execute_jira_command.call_args[1]
        assert call_kwargs["stdin_input"] == "New description"

    def test_update_description_failure(
        self, mock_execute_jira_command: MagicMock
    ) -> None:
        """Test updating description with failure."""
        mock_execute_jira_command.return_value = CommandResult(
            stdout="",
            stderr="Permission denied",
            exit_code=1,
        )

        with pytest.raises(ValueError) as exc_info:
            update_ticket_description("TEST-123", "Description")

        assert "Failed to update ticket" in str(exc_info.value)


class TestListSprints:
    """Tests for list_sprints function."""

    def test_list_sprints_success(
        self, mock_execute_jira_command: MagicMock
    ) -> None:
        """Test listing sprints successfully."""
        # Column order: id, name, start, end, state.
        mock_execute_jira_command.return_value = CommandResult(
            stdout="123\tSprint 1\t2024-01-01\t2024-01-14\tactive\n456\tSprint 2\t2024-01-15\t2024-01-28\tfuture",
            stderr="",
            exit_code=0,
        )

        result = list_sprints(board_id=1)

        assert len(result.sprints) == 2
        assert result.sprints[0].id == 123
        assert result.sprints[0].name == "Sprint 1"
        assert result.sprints[0].state == "active"
        assert result.sprints[0].start_date == "2024-01-01"
        assert result.sprints[0].end_date == "2024-01-14"
        assert result.sprints[1].id == 456
        assert result.sprints[1].name == "Sprint 2"
        assert result.sprints[1].state == "future"

    def test_list_sprints_with_state_filter(
        self, mock_execute_jira_command: MagicMock
    ) -> None:
        """Test listing sprints with state filter."""
        # Column order: id, name, start, end, state.
        mock_execute_jira_command.return_value = CommandResult(
            stdout="123\tSprint 1\t2024-01-01\t2024-01-14\tactive",
            stderr="",
            exit_code=0,
        )

        list_sprints(board_id=1, state="active")

        call_args = mock_execute_jira_command.call_args[0][0]
        assert "--state" in call_args
        assert "active" in call_args

    def test_list_sprints_with_limit(
        self, mock_execute_jira_command: MagicMock
    ) -> None:
        """Test listing sprints with limit."""
        # Column order: id, name, start, end, state.
        mock_execute_jira_command.return_value = CommandResult(
            stdout="123\tSprint 1\t2024-01-01\t2024-01-14\tactive",
            stderr="",
            exit_code=0,
        )

        list_sprints(board_id=1, limit=10)

        call_args = mock_execute_jira_command.call_args[0][0]
        assert "--paginate" in call_args
        assert "0:10" in call_args

    def test_list_sprints_no_results(
        self, mock_execute_jira_command: MagicMock
    ) -> None:
        """Test listing sprints with no results."""
        mock_execute_jira_command.return_value = CommandResult(
            stdout="",
            stderr="No result found",
            exit_code=1,
        )

        result = list_sprints(board_id=1)

        assert result.sprints == []

    def test_list_sprints_no_sprints_found(
        self, mock_execute_jira_command: MagicMock
    ) -> None:
        """Test listing sprints with 'no sprints' message."""
        mock_execute_jira_command.return_value = CommandResult(
            stdout="",
            stderr="no sprints found for board",
            exit_code=1,
        )

        result = list_sprints(board_id=1)

        assert result.sprints == []

    def test_list_sprints_error(
        self, mock_execute_jira_command: MagicMock
    ) -> None:
        """Test listing sprints with error."""
        mock_execute_jira_command.return_value = CommandResult(
            stdout="",
            stderr="Permission denied",
            exit_code=1,
        )

        with pytest.raises(ValueError) as exc_info:
            list_sprints(board_id=1)

        assert "Failed to list sprints" in str(exc_info.value)

    def test_list_sprints_partial_columns(
        self, mock_execute_jira_command: MagicMock
    ) -> None:
        """Test listing sprints with only required columns."""
        # Column order: id, name, start (partial - missing end and state).
        mock_execute_jira_command.return_value = CommandResult(
            stdout="123\tSprint 1\t2024-01-01",
            stderr="",
            exit_code=0,
        )

        result = list_sprints(board_id=1)

        assert len(result.sprints) == 1
        assert result.sprints[0].id == 123
        assert result.sprints[0].name == "Sprint 1"
        assert result.sprints[0].start_date == "2024-01-01"
        assert result.sprints[0].end_date is None
        assert result.sprints[0].state == "unknown"
        assert result.sprints[0].end_date is None


class TestAddToSprint:
    """Tests for add_to_sprint function."""

    def test_add_to_sprint_success(
        self, mock_execute_jira_command: MagicMock
    ) -> None:
        """Test adding ticket to sprint successfully."""
        mock_execute_jira_command.return_value = CommandResult(
            stdout="",
            stderr="",
            exit_code=0,
        )

        result = add_to_sprint("TEST-123", sprint_id=456)

        assert result.success is True
        assert result.ticket_key == "TEST-123"
        assert result.sprint_id == 456
        assert "Successfully added TEST-123 to sprint 456" in result.message

    def test_add_to_sprint_command_args(
        self, mock_execute_jira_command: MagicMock
    ) -> None:
        """Test add_to_sprint command arguments."""
        mock_execute_jira_command.return_value = CommandResult(
            stdout="",
            stderr="",
            exit_code=0,
        )

        add_to_sprint("TEST-123", sprint_id=789)

        call_args = mock_execute_jira_command.call_args[0][0]
        assert "sprint" in call_args
        assert "add" in call_args
        assert "789" in call_args
        assert "TEST-123" in call_args

    def test_add_to_sprint_failure(
        self, mock_execute_jira_command: MagicMock
    ) -> None:
        """Test adding ticket to sprint with failure."""
        mock_execute_jira_command.return_value = CommandResult(
            stdout="",
            stderr="Sprint not found",
            exit_code=1,
        )

        with pytest.raises(ValueError) as exc_info:
            add_to_sprint("TEST-123", sprint_id=999)

        assert "Failed to add TEST-123 to sprint 999" in str(exc_info.value)


class TestRemoveFromSprint:
    """Tests for remove_from_sprint function."""

    def test_remove_from_sprint_success(
        self, mock_execute_jira_command: MagicMock
    ) -> None:
        """Test removing ticket from sprint successfully."""
        mock_execute_jira_command.return_value = CommandResult(
            stdout="",
            stderr="",
            exit_code=0,
        )

        result = remove_from_sprint("TEST-123")

        assert result.success is True
        assert result.ticket_key == "TEST-123"
        assert "Successfully removed TEST-123 from its sprint" in result.message

    def test_remove_from_sprint_command_args(
        self, mock_execute_jira_command: MagicMock
    ) -> None:
        """Test remove_from_sprint command arguments."""
        mock_execute_jira_command.return_value = CommandResult(
            stdout="",
            stderr="",
            exit_code=0,
        )

        remove_from_sprint("TEST-456")

        call_args = mock_execute_jira_command.call_args[0][0]
        assert "issue" in call_args
        assert "edit" in call_args
        assert "TEST-456" in call_args
        assert "--custom" in call_args
        assert "sprint=" in call_args
        assert "--no-input" in call_args

    def test_remove_from_sprint_failure(
        self, mock_execute_jira_command: MagicMock
    ) -> None:
        """Test removing ticket from sprint with failure."""
        mock_execute_jira_command.return_value = CommandResult(
            stdout="",
            stderr="Ticket not found",
            exit_code=1,
        )

        with pytest.raises(ValueError) as exc_info:
            remove_from_sprint("INVALID-999")

        assert "Failed to remove INVALID-999 from sprint" in str(exc_info.value)


class TestEditTicket:
    """Tests for edit_ticket function."""

    def test_edit_ticket_summary(
        self, mock_execute_jira_command: MagicMock
    ) -> None:
        """Test editing ticket summary."""
        mock_execute_jira_command.return_value = CommandResult(
            stdout="",
            stderr="",
            exit_code=0,
        )

        result = edit_ticket("TEST-123", summary="New summary")

        assert result.success is True
        assert result.ticket_key == "TEST-123"
        assert "summary" in result.updated_fields

        call_args = mock_execute_jira_command.call_args[0][0]
        assert "--summary" in call_args
        assert "New summary" in call_args

    def test_edit_ticket_priority(
        self, mock_execute_jira_command: MagicMock
    ) -> None:
        """Test editing ticket priority."""
        mock_execute_jira_command.return_value = CommandResult(
            stdout="",
            stderr="",
            exit_code=0,
        )

        result = edit_ticket("TEST-123", priority="High")

        assert result.success is True
        assert "priority" in result.updated_fields

        call_args = mock_execute_jira_command.call_args[0][0]
        assert "--priority" in call_args
        assert "High" in call_args

    def test_edit_ticket_assignee(
        self, mock_execute_jira_command: MagicMock
    ) -> None:
        """Test editing ticket assignee."""
        mock_execute_jira_command.return_value = CommandResult(
            stdout="",
            stderr="",
            exit_code=0,
        )

        result = edit_ticket("TEST-123", assignee="john.doe")

        assert result.success is True
        assert "assignee" in result.updated_fields

        call_args = mock_execute_jira_command.call_args[0][0]
        assert "--assignee" in call_args
        assert "john.doe" in call_args

    def test_edit_ticket_unassign(
        self, mock_execute_jira_command: MagicMock
    ) -> None:
        """Test unassigning ticket."""
        mock_execute_jira_command.return_value = CommandResult(
            stdout="",
            stderr="",
            exit_code=0,
        )

        result = edit_ticket("TEST-123", assignee="")

        assert result.success is True
        assert "assignee" in result.updated_fields

        call_args = mock_execute_jira_command.call_args[0][0]
        assert "--assignee" in call_args
        assert "x" in call_args

    def test_edit_ticket_labels(
        self, mock_execute_jira_command: MagicMock
    ) -> None:
        """Test editing ticket labels."""
        mock_execute_jira_command.return_value = CommandResult(
            stdout="",
            stderr="",
            exit_code=0,
        )

        result = edit_ticket("TEST-123", labels=["bug", "urgent"])

        assert result.success is True
        assert "labels" in result.updated_fields

        call_args = mock_execute_jira_command.call_args[0][0]
        label_count = call_args.count("--label")
        assert label_count == 2

    def test_edit_ticket_add_labels(
        self, mock_execute_jira_command: MagicMock
    ) -> None:
        """Test adding labels to ticket."""
        mock_execute_jira_command.return_value = CommandResult(
            stdout="",
            stderr="",
            exit_code=0,
        )

        result = edit_ticket("TEST-123", add_labels=["new-label"])

        assert result.success is True
        assert "labels (added)" in result.updated_fields

        call_args = mock_execute_jira_command.call_args[0][0]
        assert "--label" in call_args
        assert "+new-label" in call_args

    def test_edit_ticket_remove_labels(
        self, mock_execute_jira_command: MagicMock
    ) -> None:
        """Test removing labels from ticket."""
        mock_execute_jira_command.return_value = CommandResult(
            stdout="",
            stderr="",
            exit_code=0,
        )

        result = edit_ticket("TEST-123", remove_labels=["old-label"])

        assert result.success is True
        assert "labels (removed)" in result.updated_fields

        call_args = mock_execute_jira_command.call_args[0][0]
        assert "--label" in call_args
        assert "-old-label" in call_args

    def test_edit_ticket_components(
        self, mock_execute_jira_command: MagicMock
    ) -> None:
        """Test editing ticket components."""
        mock_execute_jira_command.return_value = CommandResult(
            stdout="",
            stderr="",
            exit_code=0,
        )

        result = edit_ticket("TEST-123", components=["backend", "api"])

        assert result.success is True
        assert "components" in result.updated_fields

        call_args = mock_execute_jira_command.call_args[0][0]
        component_count = call_args.count("--component")
        assert component_count == 2

    def test_edit_ticket_fix_versions(
        self, mock_execute_jira_command: MagicMock
    ) -> None:
        """Test editing ticket fix versions."""
        mock_execute_jira_command.return_value = CommandResult(
            stdout="",
            stderr="",
            exit_code=0,
        )

        result = edit_ticket("TEST-123", fix_versions=["1.0.0", "1.1.0"])

        assert result.success is True
        assert "fix_versions" in result.updated_fields

        call_args = mock_execute_jira_command.call_args[0][0]
        version_count = call_args.count("--fix-version")
        assert version_count == 2

    def test_edit_ticket_parent(
        self, mock_execute_jira_command: MagicMock
    ) -> None:
        """Test editing ticket parent."""
        mock_execute_jira_command.return_value = CommandResult(
            stdout="",
            stderr="",
            exit_code=0,
        )

        result = edit_ticket("TEST-123", parent="TEST-100")

        assert result.success is True
        assert "parent" in result.updated_fields

        call_args = mock_execute_jira_command.call_args[0][0]
        assert "--parent" in call_args
        assert "TEST-100" in call_args

    def test_edit_ticket_custom_fields(
        self, mock_execute_jira_command: MagicMock
    ) -> None:
        """Test editing ticket custom fields."""
        mock_execute_jira_command.return_value = CommandResult(
            stdout="",
            stderr="",
            exit_code=0,
        )

        result = edit_ticket(
            "TEST-123",
            custom_fields={"customfield_10001": "value1", "story_points": "5"},
        )

        assert result.success is True
        assert "custom:customfield_10001" in result.updated_fields
        assert "custom:story_points" in result.updated_fields

        call_args = mock_execute_jira_command.call_args[0][0]
        custom_count = call_args.count("--custom")
        assert custom_count == 2

    def test_edit_ticket_multiple_fields(
        self, mock_execute_jira_command: MagicMock
    ) -> None:
        """Test editing multiple ticket fields at once."""
        mock_execute_jira_command.return_value = CommandResult(
            stdout="",
            stderr="",
            exit_code=0,
        )

        result = edit_ticket(
            "TEST-123",
            summary="Updated summary",
            priority="Critical",
            assignee="jane.doe",
            labels=["critical-bug"],
        )

        assert result.success is True
        assert "summary" in result.updated_fields
        assert "priority" in result.updated_fields
        assert "assignee" in result.updated_fields
        assert "labels" in result.updated_fields

    def test_edit_ticket_no_fields_specified(
        self, mock_execute_jira_command: MagicMock
    ) -> None:
        """Test editing ticket with no fields specified."""
        result = edit_ticket("TEST-123")

        assert result.success is False
        assert "No fields specified to update" in result.message
        assert result.updated_fields == []

        # Verify command was not called.
        mock_execute_jira_command.assert_not_called()

    def test_edit_ticket_failure(
        self, mock_execute_jira_command: MagicMock
    ) -> None:
        """Test editing ticket with failure."""
        mock_execute_jira_command.return_value = CommandResult(
            stdout="",
            stderr="Permission denied",
            exit_code=1,
        )

        with pytest.raises(ValueError) as exc_info:
            edit_ticket("TEST-123", summary="New summary")

        assert "Failed to edit ticket TEST-123" in str(exc_info.value)
