"""Tests for the Jira Fields mixin."""

from typing import Any
from unittest.mock import MagicMock

import pytest

from mcp_atlassian.jira import JiraFetcher
from mcp_atlassian.jira.fields import FieldsMixin


class TestFieldsMixin:
    """Tests for the FieldsMixin class."""

    @pytest.fixture
    def fields_mixin(self, jira_fetcher: JiraFetcher) -> FieldsMixin:
        """Create a FieldsMixin instance with mocked dependencies."""
        mixin = jira_fetcher
        mixin._field_ids_cache = None
        return mixin

    @pytest.fixture
    def mock_fields(self):
        """Return mock field data."""
        return [
            {"id": "summary", "name": "Summary", "schema": {"type": "string"}},
            {"id": "description", "name": "Description", "schema": {"type": "string"}},
            {"id": "status", "name": "Status", "schema": {"type": "status"}},
            {"id": "assignee", "name": "Assignee", "schema": {"type": "user"}},
            {
                "id": "customfield_10010",
                "name": "Epic Link",
                "schema": {
                    "type": "string",
                    "custom": "com.pyxis.greenhopper.jira:gh-epic-link",
                },
            },
            {
                "id": "customfield_10011",
                "name": "Epic Name",
                "schema": {
                    "type": "string",
                    "custom": "com.pyxis.greenhopper.jira:gh-epic-label",
                },
            },
            {
                "id": "customfield_10012",
                "name": "Story Points",
                "schema": {"type": "number"},
            },
        ]

    def test_get_field_ids_cache(self, fields_mixin: FieldsMixin, mock_fields):
        """Test get_fields uses cache when available."""
        # Set up the cache
        fields_mixin._field_ids_cache = mock_fields

        # Call the method
        result = fields_mixin.get_fields()

        # Verify cache was used
        assert result == mock_fields
        fields_mixin.jira.get_all_fields.assert_not_called()

    def test_get_fields_refresh(self, fields_mixin: FieldsMixin, mock_fields):
        """Test get_fields refreshes data when requested."""
        # Set up the cache
        fields_mixin._field_ids_cache = [{"id": "old_data", "name": "old data"}]

        # Mock the API response
        fields_mixin.jira.get_all_fields.return_value = mock_fields

        # Call the method with refresh=True
        result = fields_mixin.get_fields(refresh=True)

        # Verify API was called
        fields_mixin.jira.get_all_fields.assert_called_once()
        assert result == mock_fields
        # Verify cache was updated
        assert fields_mixin._field_ids_cache == mock_fields

    def test_get_fields_from_api(
        self, fields_mixin: FieldsMixin, mock_fields: list[dict[str, Any]]
    ):
        """Test get_fields fetches from API when no cache exists."""
        # Mock the API response
        fields_mixin.jira.get_all_fields.return_value = mock_fields

        # Call the method
        result = fields_mixin.get_fields()

        # Verify API was called
        fields_mixin.jira.get_all_fields.assert_called_once()
        assert result == mock_fields
        # Verify cache was created
        assert fields_mixin._field_ids_cache == mock_fields

    def test_get_fields_error(self, fields_mixin: FieldsMixin):
        """Test get_fields handles errors gracefully."""

        # Mock API error
        fields_mixin.jira.get_all_fields.side_effect = Exception("API error")

        # Call the method
        result = fields_mixin.get_fields()

        # Verify empty list is returned on error
        assert result == []

    def test_get_field_id_by_exact_match(self, fields_mixin: FieldsMixin, mock_fields):
        """Test get_field_id finds field by exact name match."""
        # Set up the fields
        fields_mixin.get_fields = MagicMock(return_value=mock_fields)

        # Call the method
        result = fields_mixin.get_field_id("Summary")

        # Verify the result
        assert result == "summary"

    def test_get_field_id_case_insensitive(
        self, fields_mixin: FieldsMixin, mock_fields
    ):
        """Test get_field_id is case-insensitive."""
        # Set up the fields
        fields_mixin.get_fields = MagicMock(return_value=mock_fields)

        # Call the method with different case
        result = fields_mixin.get_field_id("summary")

        # Verify the result
        assert result == "summary"

    def test_get_field_id_exact_match_case_insensitive(
        self, fields_mixin: FieldsMixin, mock_fields
    ):
        """Test get_field_id finds field by exact match (case-insensitive) using the map."""
        # Set up the fields
        fields_mixin.get_fields = MagicMock(return_value=mock_fields)
        # Ensure the map is generated based on the mock fields for this test
        fields_mixin._generate_field_map(force_regenerate=True)

        # Call the method with exact name (case-insensitive)
        result = fields_mixin.get_field_id("epic link")

        # Verify the result (should find Epic Link as first match)
        assert result == "customfield_10010"

    def test_get_field_id_not_found(self, fields_mixin: FieldsMixin, mock_fields):
        """Test get_field_id returns None when field not found."""
        # Set up the fields
        fields_mixin.get_fields = MagicMock(return_value=mock_fields)

        # Call the method with non-existent field
        result = fields_mixin.get_field_id("NonExistent")

        # Verify the result
        assert result is None

    def test_get_field_id_error(self, fields_mixin: FieldsMixin):
        """Test get_field_id handles errors gracefully."""
        # Make get_fields raise an exception
        fields_mixin.get_fields = MagicMock(
            side_effect=Exception("Error getting fields")
        )

        # Call the method
        result = fields_mixin.get_field_id("Summary")

        # Verify None is returned on error
        assert result is None

    def test_get_field_by_id(self, fields_mixin: FieldsMixin, mock_fields):
        """Test get_field_by_id retrieves field definition correctly."""
        # Set up the fields
        fields_mixin.get_fields = MagicMock(return_value=mock_fields)

        # Call the method
        result = fields_mixin.get_field_by_id("customfield_10012")

        # Verify the result
        assert result == mock_fields[6]  # The Story Points field
        assert result["name"] == "Story Points"

    def test_get_field_by_id_not_found(self, fields_mixin: FieldsMixin, mock_fields):
        """Test get_field_by_id returns None when field not found."""
        # Set up the fields
        fields_mixin.get_fields = MagicMock(return_value=mock_fields)

        # Call the method with non-existent ID
        result = fields_mixin.get_field_by_id("customfield_99999")

        # Verify the result
        assert result is None

    def test_get_custom_fields(self, fields_mixin: FieldsMixin, mock_fields):
        """Test get_custom_fields returns only custom fields."""
        # Set up the fields
        fields_mixin.get_fields = MagicMock(return_value=mock_fields)

        # Call the method
        result = fields_mixin.get_custom_fields()

        # Verify the result
        assert len(result) == 3
        assert all(field["id"].startswith("customfield_") for field in result)
        assert result[0]["name"] == "Epic Link"
        assert result[1]["name"] == "Epic Name"
        assert result[2]["name"] == "Story Points"

    def test_get_required_fields(self, fields_mixin: FieldsMixin):
        """Test get_required_fields retrieves required fields correctly."""
        # Mock the response for get_project_issue_types
        mock_issue_types = [
            {"id": "10001", "name": "Bug"},
            {"id": "10002", "name": "Task"},
        ]
        fields_mixin.get_project_issue_types = MagicMock(return_value=mock_issue_types)

        # Mock the response for issue_createmeta_fieldtypes
        # Real API returns paginated "values" array
        mock_field_meta = {
            "maxResults": 50,
            "startAt": 0,
            "total": 3,
            "isLast": True,
            "values": [
                {
                    "required": True,
                    "schema": {"type": "string", "system": "summary"},
                    "name": "Summary",
                    "fieldId": "summary",
                    "autoCompleteUrl": "",
                    "hasDefaultValue": False,
                    "operations": ["set"],
                    "allowedValues": [],
                },
                {
                    "required": False,
                    "schema": {"type": "string", "system": "description"},
                    "name": "Description",
                    "fieldId": "description",
                },
                {
                    "required": True,
                    "schema": {"type": "string", "custom": "some.custom.type"},
                    "name": "Epic Link",
                    "fieldId": "customfield_10010",
                },
            ],
        }
        fields_mixin.jira.issue_createmeta_fieldtypes.return_value = mock_field_meta

        # Call the method
        result = fields_mixin.get_required_fields("Bug", "TEST")

        # Verify the result
        assert len(result) == 2
        assert "summary" in result
        assert result["summary"]["required"] is True
        assert "customfield_10010" in result
        assert result["customfield_10010"]["required"] is True
        assert "description" not in result
        # Verify the correct API was called
        fields_mixin.get_project_issue_types.assert_called_once_with("TEST")
        fields_mixin.jira.issue_createmeta_fieldtypes.assert_called_once_with(
            project="TEST", issue_type_id="10001"
        )

    def test_get_required_fields_not_found(self, fields_mixin: FieldsMixin):
        """Test get_required_fields handles project/issue type not found."""
        # Scenario 1: Issue type not found in project
        mock_issue_types = [{"id": "10002", "name": "Task"}]  # "Bug" is missing
        fields_mixin.get_project_issue_types = MagicMock(return_value=mock_issue_types)
        fields_mixin.jira.issue_createmeta_fieldtypes = MagicMock()

        # Call the method
        result = fields_mixin.get_required_fields("Bug", "TEST")
        # Verify issue type lookup was attempted, but field meta was not called
        fields_mixin.get_project_issue_types.assert_called_once_with("TEST")
        fields_mixin.jira.issue_createmeta_fieldtypes.assert_not_called()

        # Verify the result
        assert result == {}

    def test_get_required_fields_error(self, fields_mixin: FieldsMixin):
        """Test get_required_fields handles errors gracefully."""
        # Mock the response for get_project_issue_types
        mock_issue_types = [
            {"id": "10001", "name": "Bug"},
        ]
        fields_mixin.get_project_issue_types = MagicMock(return_value=mock_issue_types)
        # Mock issue_createmeta_fieldtypes to raise an error
        fields_mixin.jira.issue_createmeta_fieldtypes.side_effect = Exception(
            "API error"
        )

        # Call the method
        result = fields_mixin.get_required_fields("Bug", "TEST")

        # Verify the result
        assert result == {}
        # Verify the correct API was called (which then raised the error)
        fields_mixin.jira.issue_createmeta_fieldtypes.assert_called_once_with(
            project="TEST", issue_type_id="10001"
        )

    def test_get_jira_field_ids_cached(self, fields_mixin: FieldsMixin):
        """Test get_field_ids_to_epic returns cached field IDs."""
        # Set up the cache
        fields_mixin._field_ids_cache = [
            {"id": "summary", "name": "Summary"},
            {"id": "description", "name": "Description"},
        ]

        # Call the method
        result = fields_mixin.get_field_ids_to_epic()

        # Verify the result
        assert result == {
            "Summary": "summary",
            "Description": "description",
        }

    def test_get_jira_field_ids_from_fields(
        self, fields_mixin: FieldsMixin, mock_fields: list[dict]
    ):
        """Test get_field_ids_to_epic extracts field IDs from field definitions."""
        # Set up the fields
        fields_mixin.get_fields = MagicMock(return_value=mock_fields)
        # Ensure field map is generated
        fields_mixin._generate_field_map(force_regenerate=True)

        # Call the method
        result = fields_mixin.get_field_ids_to_epic()

        # Verify that epic-specific fields are properly identified
        assert "epic_link" in result
        assert "Epic Link" in result
        assert result["epic_link"] == "customfield_10010"
        assert "epic_name" in result
        assert "Epic Name" in result
        assert result["epic_name"] == "customfield_10011"

    def test_get_jira_field_ids_error(self, fields_mixin: FieldsMixin):
        """Test get_field_ids_to_epic handles errors gracefully."""
        # Ensure no cache exists
        fields_mixin._field_ids_cache = None

        # Make get_fields raise an exception
        fields_mixin.get_fields = MagicMock(
            side_effect=Exception("Error getting fields")
        )

        # Call the method
        result = fields_mixin.get_field_ids_to_epic()

        # Verify the result
        assert result == {}

    def test_is_custom_field(self, fields_mixin: FieldsMixin):
        """Test is_custom_field correctly identifies custom fields."""
        # Test with custom field
        assert fields_mixin.is_custom_field("customfield_10010") is True

        # Test with standard field
        assert fields_mixin.is_custom_field("summary") is False

    @pytest.mark.parametrize(
        "is_cloud, user_field, resolved_id",
        [
            pytest.param(True, "accountId", "account123", id="cloud"),
            pytest.param(False, "name", "jdoe", id="server"),
        ],
    )
    def test_format_field_value_user_field(
        self,
        fields_mixin: FieldsMixin,
        mock_fields,
        is_cloud: bool,
        user_field: str,
        resolved_id: str,
    ):
        """Test format_field_value formats user fields correctly."""
        fields_mixin.get_field_by_id = MagicMock(
            return_value=mock_fields[3]
        )  # The Assignee field
        fields_mixin._get_account_id = MagicMock(return_value=resolved_id)
        fields_mixin.config = MagicMock()
        fields_mixin.config.is_cloud = is_cloud

        result = fields_mixin.format_field_value("assignee", "johndoe")

        assert result == {user_field: resolved_id}
        fields_mixin._get_account_id.assert_called_once_with("johndoe")

    def test_format_field_value_array_field(self, fields_mixin: FieldsMixin):
        """Test format_field_value formats array fields correctly."""
        # Set up the mocks
        mock_array_field = {
            "id": "labels",
            "name": "Labels",
            "schema": {"type": "array"},
        }
        fields_mixin.get_field_by_id = MagicMock(return_value=mock_array_field)

        # Test with single value (should convert to list)
        result = fields_mixin.format_field_value("labels", "bug")
        assert result == ["bug"]

        # Test with list value (should keep as list)
        result = fields_mixin.format_field_value("labels", ["bug", "feature"])
        assert result == ["bug", "feature"]

    def test_format_field_value_priority_field(self, fields_mixin: FieldsMixin):
        """Test format_field_value formats priority correctly with {name: ...}."""
        mock_priority_field = {
            "id": "priority",
            "name": "Priority",
            "schema": {"type": "priority"},
        }
        fields_mixin.get_field_by_id = MagicMock(return_value=mock_priority_field)

        result = fields_mixin.format_field_value("priority", "High")
        assert result == {"name": "High"}

        already_formatted = {"name": "Medium"}
        result = fields_mixin.format_field_value("priority", already_formatted)
        assert result == already_formatted

    def test_format_field_value_option_field(self, fields_mixin: FieldsMixin):
        """Test format_field_value formats option fields with {value: ...}."""
        mock_option_field = {
            "id": "customfield_10024",
            "name": "Severity",
            "schema": {"type": "option", "custom": "radiobuttons"},
        }
        fields_mixin.get_field_by_id = MagicMock(return_value=mock_option_field)

        result = fields_mixin.format_field_value("customfield_10024", "Critical")
        assert result == {"value": "Critical"}

        already_formatted = {"value": "Medium"}
        result = fields_mixin.format_field_value("customfield_10024", already_formatted)
        assert result == already_formatted

    def test_format_field_value_unknown_field(self, fields_mixin: FieldsMixin):
        """Test format_field_value returns value as-is for unknown fields."""
        # Set up the mocks
        fields_mixin.get_field_by_id = MagicMock(return_value=None)

        # Call the method with unknown field
        test_value = "test value"
        result = fields_mixin.format_field_value("unknown", test_value)

        # Verify the value is returned as-is
        assert result == test_value

    def test_search_fields_empty_keyword(self, fields_mixin: FieldsMixin, mock_fields):
        """Test search_fields returns first N fields when keyword is empty."""
        # Set up the fields
        fields_mixin.get_fields = MagicMock(return_value=mock_fields)

        # Call with empty keyword and limit=3
        result = fields_mixin.search_fields("", limit=3)

        # Verify first 3 fields are returned
        assert len(result) == 3
        assert result == mock_fields[:3]

    def test_search_fields_exact_match(self, fields_mixin: FieldsMixin, mock_fields):
        """Test search_fields finds exact matches with high relevance."""
        # Set up the fields
        fields_mixin.get_fields = MagicMock(return_value=mock_fields)

        # Search for "Story Points"
        result = fields_mixin.search_fields("Story Points")

        # Verify Story Points field is first result
        assert len(result) > 0
        assert result[0]["name"] == "Story Points"
        assert result[0]["id"] == "customfield_10012"

    def test_search_fields_partial_match(self, fields_mixin: FieldsMixin, mock_fields):
        """Test search_fields finds partial matches."""
        # Set up the fields
        fields_mixin.get_fields = MagicMock(return_value=mock_fields)

        # Search for "Epic"
        result = fields_mixin.search_fields("Epic")

        # Verify Epic-related fields are in results
        epic_fields = [field["name"] for field in result[:2]]  # Top 2 results
        assert "Epic Link" in epic_fields
        assert "Epic Name" in epic_fields

    def test_search_fields_case_insensitive(
        self, fields_mixin: FieldsMixin, mock_fields
    ):
        """Test search_fields is case insensitive."""
        # Set up the fields
        fields_mixin.get_fields = MagicMock(return_value=mock_fields)

        # Search with different cases
        result_lower = fields_mixin.search_fields("story points")
        result_upper = fields_mixin.search_fields("STORY POINTS")
        result_mixed = fields_mixin.search_fields("Story Points")

        # Verify all searches find the same field
        assert len(result_lower) > 0
        assert len(result_upper) > 0
        assert len(result_mixed) > 0
        assert result_lower[0]["id"] == result_upper[0]["id"] == result_mixed[0]["id"]
        assert result_lower[0]["name"] == "Story Points"

    def test_search_fields_with_limit(self, fields_mixin: FieldsMixin, mock_fields):
        """Test search_fields respects the limit parameter."""
        # Set up the fields
        fields_mixin.get_fields = MagicMock(return_value=mock_fields)

        # Search with limit=2
        result = fields_mixin.search_fields("field", limit=2)

        # Verify only 2 results are returned
        assert len(result) == 2

    def test_search_fields_error(self, fields_mixin: FieldsMixin):
        """Test search_fields handles errors gracefully."""
        # Make get_fields raise an exception
        fields_mixin.get_fields = MagicMock(
            side_effect=Exception("Error getting fields")
        )

        # Call the method
        result = fields_mixin.search_fields("test")

        # Verify empty list is returned on error
        assert result == []


class TestFormatFieldValueForWrite:
    """Tests for _format_field_value_for_write on FieldsMixin."""

    @pytest.fixture
    def mixin(self, jira_fetcher: JiraFetcher) -> FieldsMixin:
        """Create a FieldsMixin instance with mocked dependencies."""
        fetcher = jira_fetcher
        fetcher._get_account_id = MagicMock(return_value="resolved-id")
        fetcher.config = MagicMock()
        fetcher.config.is_cloud = True
        return fetcher

    # -- Priority --------------------------------------------------------

    @pytest.mark.parametrize(
        "test_id, field_id, value, field_definition, expected",
        [
            (
                "priority_string",
                "priority",
                "High",
                {"name": "Priority", "schema": {"type": "priority"}},
                {"name": "High"},
            ),
            (
                "priority_dict",
                "priority",
                {"name": "High"},
                {"name": "Priority", "schema": {"type": "priority"}},
                {"name": "High"},
            ),
        ],
    )
    def test_priority(
        self, mixin, test_id, field_id, value, field_definition, expected
    ):
        result = mixin._format_field_value_for_write(field_id, value, field_definition)
        assert result == expected

    # -- Labels ----------------------------------------------------------

    @pytest.mark.parametrize(
        "test_id, field_id, value, field_definition, expected",
        [
            (
                "labels_list",
                "labels",
                ["a", "b"],
                {"name": "Labels", "schema": {"type": "array", "items": "string"}},
                ["a", "b"],
            ),
            (
                "labels_csv",
                "labels",
                "a,b",
                {"name": "Labels", "schema": {"type": "array", "items": "string"}},
                ["a", "b"],
            ),
        ],
    )
    def test_labels(self, mixin, test_id, field_id, value, field_definition, expected):
        result = mixin._format_field_value_for_write(field_id, value, field_definition)
        assert result == expected

    # -- Components / fixVersions ----------------------------------------

    @pytest.mark.parametrize(
        "test_id, field_id, value, field_definition, expected",
        [
            (
                "components_strings",
                "components",
                ["UI", "API"],
                {
                    "name": "Component/s",
                    "schema": {"type": "array", "items": "component"},
                },
                [{"name": "UI"}, {"name": "API"}],
            ),
            (
                "components_dicts",
                "components",
                [{"name": "UI"}],
                {
                    "name": "Component/s",
                    "schema": {"type": "array", "items": "component"},
                },
                [{"name": "UI"}],
            ),
            (
                "fixversions",
                "fixVersions",
                ["1.0"],
                {
                    "name": "Fix Version/s",
                    "schema": {"type": "array", "items": "version"},
                },
                [{"name": "1.0"}],
            ),
        ],
    )
    def test_name_wrapped_arrays(
        self, mixin, test_id, field_id, value, field_definition, expected
    ):
        result = mixin._format_field_value_for_write(field_id, value, field_definition)
        assert result == expected

    # -- Reporter (Cloud vs Server) --------------------------------------

    @pytest.mark.parametrize(
        "is_cloud, user_field, user_input, resolved_id",
        [
            pytest.param(
                True,
                "accountId",
                "user@ex.com",
                "cloud-acc-id",
                id="cloud",
            ),
            pytest.param(
                False,
                "name",
                "jdoe",
                "jdoe",
                id="server",
            ),
        ],
    )
    def test_reporter(
        self,
        mixin,
        is_cloud: bool,
        user_field: str,
        user_input: str,
        resolved_id: str,
    ):
        mixin.config.is_cloud = is_cloud
        mixin._get_account_id = MagicMock(return_value=resolved_id)
        result = mixin._format_field_value_for_write(
            "reporter",
            user_input,
            {"name": "Reporter", "schema": {"type": "user"}},
        )
        assert result == {user_field: resolved_id}
        mixin._get_account_id.assert_called_once_with(user_input)

    # -- Duedate ---------------------------------------------------------

    def test_duedate_valid(self, mixin):
        result = mixin._format_field_value_for_write("duedate", "2026-03-01", None)
        assert result == "2026-03-01"

    def test_duedate_invalid(self, mixin):
        result = mixin._format_field_value_for_write("duedate", 12345, None)
        assert result is None

    # -- Cascading select ------------------------------------------------

    @pytest.mark.parametrize(
        "test_id, value, expected",
        [
            (
                "cascading_tuple",
                ("NA", "US"),
                {"value": "NA", "child": {"value": "US"}},
            ),
            (
                "cascading_dict",
                {"value": "NA", "child": {"value": "US"}},
                {"value": "NA", "child": {"value": "US"}},
            ),
            (
                "cascading_string",
                "NA",
                {"value": "NA"},
            ),
        ],
    )
    def test_cascading_select(self, mixin, test_id, value, expected):
        field_def = {
            "name": "Region",
            "schema": {"type": "option-with-child", "custom": "cascadingselect"},
        }
        result = mixin._format_field_value_for_write(
            "customfield_10020", value, field_def
        )
        assert result == expected

    # -- Multi-select ----------------------------------------------------

    @pytest.mark.parametrize(
        "test_id, value, expected",
        [
            (
                "multiselect_strings",
                ["opt1", "opt2"],
                [{"value": "opt1"}, {"value": "opt2"}],
            ),
            (
                "multiselect_dicts",
                [{"value": "opt1"}],
                [{"value": "opt1"}],
            ),
            (
                "multiselect_csv",
                "opt1,opt2",
                [{"value": "opt1"}, {"value": "opt2"}],
            ),
        ],
    )
    def test_multiselect(self, mixin, test_id, value, expected):
        field_def = {
            "name": "Categories",
            "schema": {"type": "array", "items": "option", "custom": "multiselect"},
        }
        result = mixin._format_field_value_for_write(
            "customfield_10021", value, field_def
        )
        assert result == expected

    # -- Custom user field -----------------------------------------------

    @pytest.mark.parametrize(
        "is_cloud, user_field, user_input, resolved_id",
        [
            pytest.param(
                True,
                "accountId",
                "user@ex.com",
                "cloud-acc-id",
                id="cloud",
            ),
            pytest.param(
                False,
                "name",
                "jdoe",
                "jdoe",
                id="server",
            ),
        ],
    )
    def test_custom_user(
        self,
        mixin,
        is_cloud: bool,
        user_field: str,
        user_input: str,
        resolved_id: str,
    ):
        mixin.config.is_cloud = is_cloud
        mixin._get_account_id = MagicMock(return_value=resolved_id)
        field_def = {
            "name": "Reviewer",
            "schema": {"type": "user", "custom": "userpicker"},
        }
        result = mixin._format_field_value_for_write(
            "customfield_10022", user_input, field_def
        )
        assert result == {user_field: resolved_id}

    def test_custom_user_unresolvable(self, mixin):
        """Unresolvable custom user field returns None instead of raising."""
        mixin._get_account_id = MagicMock(side_effect=ValueError("User not found"))
        field_def = {
            "name": "Reviewer",
            "schema": {"type": "user", "custom": "userpicker"},
        }
        result = mixin._format_field_value_for_write(
            "customfield_10022", "nobody@ex.com", field_def
        )
        assert result is None

    # -- Custom date field -----------------------------------------------

    def test_custom_date_valid(self, mixin):
        field_def = {"name": "Target Date", "schema": {"type": "date"}}
        result = mixin._format_field_value_for_write(
            "customfield_10023", "2026-03-01", field_def
        )
        assert result == "2026-03-01"

    def test_custom_date_invalid(self, mixin):
        field_def = {"name": "Target Date", "schema": {"type": "date"}}
        result = mixin._format_field_value_for_write(
            "customfield_10023", 12345, field_def
        )
        assert result is None

    # -- Generic option (radio/select) -----------------------------------

    @pytest.mark.parametrize(
        "test_id, value, expected",
        [
            ("option_string", "Critical", {"value": "Critical"}),
            ("option_dict", {"value": "Critical"}, {"value": "Critical"}),
        ],
    )
    def test_option_field(self, mixin, test_id, value, expected):
        field_def = {
            "name": "Severity",
            "schema": {"type": "option", "custom": "radiobuttons"},
        }
        result = mixin._format_field_value_for_write(
            "customfield_10024", value, field_def
        )
        assert result == expected

    # -- Unknown field passthrough ---------------------------------------

    def test_unknown_passthrough(self, mixin):
        result = mixin._format_field_value_for_write(
            "customfield_99999", "anything", None
        )
        assert result == "anything"


class TestDatetimeTimezoneFormat:
    """Test datetime field formatting produces Jira-compatible tz offsets."""

    @pytest.fixture
    def fields_mixin(self, jira_fetcher: "JiraFetcher") -> FieldsMixin:
        """Create a FieldsMixin instance with mocked dependencies."""
        mixin = jira_fetcher
        mixin._field_ids_cache = None
        return mixin

    @pytest.mark.parametrize(
        "input_value,expected",
        [
            pytest.param(
                "2026-01-21T15:00:00.000+0000",
                "2026-01-21T15:00:00.000+0000",
                id="already-basic-format",
            ),
            pytest.param(
                "2026-01-21T15:00:00+00:00",
                "2026-01-21T15:00:00.000+0000",
                id="extended-to-basic",
            ),
            pytest.param(
                "2026-01-21T15:00:00.000+0530",
                "2026-01-21T15:00:00.000+0530",
                id="non-utc-preserved",
            ),
            pytest.param(
                "2026-01-21T15:00:00-0800",
                "2026-01-21T15:00:00.000-0800",
                id="negative-offset-basic",
            ),
            pytest.param(
                "2026-01-21T15:00:00",
                "2026-01-21T15:00:00.000",
                id="naive-no-tz",
            ),
            pytest.param(
                "2026-01-21",
                "2026-01-21T00:00:00.000",
                id="date-only-to-midnight",
            ),
            pytest.param(
                "invalid-date",
                "invalid-date",
                id="unparseable-passthrough",
            ),
            pytest.param(
                "",
                "",
                id="empty-string",
            ),
        ],
    )
    def test_datetime_timezone_format(
        self, fields_mixin: FieldsMixin, input_value: str, expected: str
    ):
        """Datetime fields must use ±HHMM (basic) format, not ±HH:MM."""
        mock_datetime_field = {
            "id": "customfield_10050",
            "name": "Due DateTime",
            "schema": {"type": "datetime"},
        }
        fields_mixin.get_field_by_id = MagicMock(return_value=mock_datetime_field)

        result = fields_mixin.format_field_value("customfield_10050", input_value)
        assert result == expected


class TestChecklistFieldFormatting:
    """Tests for checklist field formatting in _format_field_value_for_write."""

    CHECKLIST_FIELD_DEF = {
        "id": "customfield_11003",
        "name": "Definition of Done",
        "schema": {
            "type": "string",
            "custom": "com.okapya.jira.checklist:checklist",
            "customId": 11003,
        },
    }

    CHECKLIST_ARRAY_FIELD_DEF = {
        "id": "customfield_11004",
        "name": "Checklist",
        "schema": {
            "type": "array",
            "items": "checklist-item",
            "custom": "com.okapya.jira.checklist:checklist",
            "customId": 11004,
        },
    }

    @pytest.fixture
    def mixin(self, jira_fetcher: "JiraFetcher") -> FieldsMixin:
        """Create a FieldsMixin instance with mocked dependencies."""
        fetcher = jira_fetcher
        fetcher.config = MagicMock()
        fetcher.config.is_cloud = True
        return fetcher

    @pytest.mark.parametrize(
        "test_id, value, expected",
        [
            pytest.param(
                "list_to_markdown",
                ["Task A", "Task B"],
                "* Task A\n* Task B",
                id="list_to_markdown",
            ),
            pytest.param(
                "list_with_checked_tuples",
                [("Task A", True), ("Task B", False)],
                "* [x] Task A\n* Task B",
                id="list_with_checked_tuples",
            ),
            pytest.param(
                "dict_list",
                [{"name": "Task A", "checked": True}],
                "* [x] Task A",
                id="dict_list",
            ),
            pytest.param(
                "string_passthrough",
                "* [x] done\n* todo",
                "* [x] done\n* todo",
                id="string_passthrough",
            ),
            pytest.param(
                "empty_list",
                [],
                "",
                id="empty_list",
            ),
        ],
    )
    def test_checklist_formatting(self, mixin, test_id, value, expected):
        """Checklist fields should be converted to markdown string format."""
        result = mixin._format_field_value_for_write(
            "customfield_11003", value, self.CHECKLIST_FIELD_DEF
        )
        assert result == expected

    @pytest.mark.parametrize(
        "test_id, value, expected",
        [
            pytest.param(
                "array_dict_list_passthrough",
                [{"name": "A", "checked": True}, {"name": "B", "checked": False}],
                [{"name": "A", "checked": True}, {"name": "B", "checked": False}],
                id="array_dict_list_passthrough",
            ),
            pytest.param(
                "array_string_list_passthrough",
                ["Task A", "Task B"],
                ["Task A", "Task B"],
                id="array_string_list_passthrough",
            ),
            pytest.param(
                "array_string_passthrough",
                "* [x] done\n* todo",
                "* [x] done\n* todo",
                id="array_string_passthrough",
            ),
            pytest.param(
                "array_empty_list_passthrough",
                [],
                [],
                id="array_empty_list_passthrough",
            ),
        ],
    )
    def test_checklist_array_passthrough(self, mixin, test_id, value, expected):
        """Array-type checklist fields (Server/DC) should pass through unchanged."""
        result = mixin._format_field_value_for_write(
            "customfield_11004", value, self.CHECKLIST_ARRAY_FIELD_DEF
        )
        assert result == expected

    def test_non_checklist_string_field_unaffected(self, mixin):
        """Non-checklist string fields should not be affected by checklist logic."""
        non_checklist_def = {
            "id": "customfield_99999",
            "name": "Some Text Field",
            "schema": {"type": "string"},
        }
        value = ["a", "b"]
        result = mixin._format_field_value_for_write(
            "customfield_99999", value, non_checklist_def
        )
        # Should pass through unchanged (no checklist conversion)
        assert result == ["a", "b"]
