#!/usr/bin/env python3

"""
Tests for the annotate_params.py script.

These are the unit tests for the python script that fetches online ArduPilot
parameter documentation (if not cached) and adds it to the specified file or
to all *.param and *.parm files in the specified directory.

SPDX-FileCopyrightText: 2024-2026 Amilcar do Carmo Lucas <amilcar.lucas@iav.de>

SPDX-License-Identifier: GPL-3.0-or-later
"""

import contextlib
import os
import tempfile
import unittest
from collections.abc import Generator
from unittest import mock
from unittest.mock import MagicMock, Mock, patch
from xml.etree import ElementTree as ET

import pytest
from defusedxml import ElementTree as DET  # noqa: N814

from ardupilot_methodic_configurator.annotate_params import (
    BASE_URL,
    PARAM_DEFINITION_XML_FILE,
    create_doc_dict,
    extract_parameter_name_and_validate,
    format_columns,
    get_xml_data,
    get_xml_url,
    main,
    missionplanner_sort,
    parse_arguments,
    print_read_only_params,
    remove_prefix,
    split_into_lines,
    update_parameter_documentation,
)
from ardupilot_methodic_configurator.data_model_par_dict import Par, ParDict

# pylint: disable=protected-access


@pytest.fixture
def mock_update() -> Generator[MagicMock, None, None]:
    with patch("ardupilot_methodic_configurator.annotate_params.update_parameter_documentation") as mock_fun:
        yield mock_fun


@pytest.fixture
def mock_get_xml_dir() -> Generator[MagicMock, None, None]:
    with patch("ardupilot_methodic_configurator.annotate_params.get_xml_dir") as mock_fun:
        yield mock_fun


@pytest.fixture
def mock_get_xml_url() -> Generator[MagicMock, None, None]:
    with patch("ardupilot_methodic_configurator.annotate_params.get_xml_url") as mock_fun:
        yield mock_fun


class TestParamDocsUpdate(unittest.TestCase):  # pylint: disable=missing-class-docstring, too-many-public-methods
    def setUp(self) -> None:
        # Create a temporary directory
        self.temp_dir = tempfile.mkdtemp()

        # Create a temporary file
        # pylint: disable=consider-using-with
        self.temp_file = tempfile.NamedTemporaryFile(delete=False)  # noqa: SIM115
        # pylint: enable=consider-using-with

        # Create a dictionary of parameter documentation
        self.doc_dict = {
            "PARAM1": {
                "humanName": "Param 1",
                "documentation": ["Documentation for Param 1"],
                "fields": {"Field1": "Value1", "Field2": "Value2"},
                "values": {"Code1": "Value1", "Code2": "Value2"},
            },
            "PARAM2": {
                "humanName": "Param 2",
                "documentation": ["Documentation for Param 2"],
                "fields": {"Field3": "Value3", "Field4": "Value4"},
                "values": {"Code3": "Value3", "Code4": "Value4"},
            },
            "PARAM_1": {
                "humanName": "Param _ 1",
                "documentation": ["Documentation for Param_1"],
                "fields": {"Field_1": "Value_1", "Field_2": "Value_2"},
                "values": {"Code_1": "Value_1", "Code_2": "Value_2"},
            },
        }

    @patch("builtins.open", new_callable=mock.mock_open, read_data="<root></root>")
    @patch("os.path.isfile")
    @patch("ardupilot_methodic_configurator.data_model_par_dict.ParDict.load_param_file_into_dict")
    def test_get_xml_data_local_file(  # type: ignore[misc]
        self,
        mock_load_param: MagicMock,
        mock_isfile: MagicMock,
        mock_open_: MagicMock,
    ) -> None:
        # Mock the isfile function to return True
        mock_isfile.return_value = True

        # Mock the load_param_file_into_dict function to raise FileNotFoundError
        mock_load_param.side_effect = FileNotFoundError

        # Call the function with a local file
        result = get_xml_data("/path/to/local/file/", ".", "test.xml", "ArduCopter")

        # Check the result
        assert isinstance(result, ET.Element)

        # Assert that the file was opened correctly
        mock_open_.assert_called_once_with(os.path.join(".", "test.xml"), encoding="utf-8")

    @patch("ardupilot_methodic_configurator.annotate_params.download_file_from_url")
    def test_get_xml_data_remote_file(self, mock_download: MagicMock) -> None:  # type: ignore[misc]
        # Mock successful download
        mock_download.return_value = True

        # Remove the test.xml file if it exists
        with contextlib.suppress(FileNotFoundError):
            os.remove("test.xml")

        # Mock open to return dummy XML data since we aren't actually downloading a file
        with patch("builtins.open", mock.mock_open(read_data="<root></root>")):
            # Call the function with a remote file
            result = get_xml_data("http://example.com/", ".", "test.xml", "ArduCopter")

        # Check the result
        assert isinstance(result, ET.Element)

        # Assert that download_file_from_url was called once with correct arguments
        mock_download.assert_called_once_with("http://example.com/test.xml", os.path.join(".", "test.xml"))

    @patch("os.path.isfile")
    @patch("ardupilot_methodic_configurator.data_model_par_dict.ParDict.load_param_file_into_dict")
    def test_get_xml_data_script_dir_file(  # type: ignore[misc]
        self,
        mock_load_param: MagicMock,
        mock_isfile: MagicMock,
    ) -> None:
        # Mock the isfile function to return False for the current directory and True for the script directory
        def side_effect(_filename: str) -> bool:
            return True

        mock_isfile.side_effect = side_effect

        # Mock the load_param_file_into_dict function to raise FileNotFoundError
        mock_load_param.side_effect = FileNotFoundError

        # Mock the open function to return a dummy XML string
        mock_open = mock.mock_open(read_data="<root></root>")
        with patch("builtins.open", mock_open):
            # Call the function with a filename that exists in the script directory
            result = get_xml_data(BASE_URL, ".", PARAM_DEFINITION_XML_FILE, "ArduCopter")

        # Check the result
        assert isinstance(result, ET.Element)

        # Assert that the file was opened correctly
        mock_open.assert_called_once_with(os.path.join(".", PARAM_DEFINITION_XML_FILE), encoding="utf-8")

    @patch("ardupilot_methodic_configurator.annotate_params.download_file_from_url")
    def test_get_xml_data_download_failure(self, mock_download: MagicMock) -> None:  # type: ignore[misc]
        # Mock failed download
        mock_download.return_value = False

        # Remove the test.xml file if it exists
        with contextlib.suppress(FileNotFoundError):
            os.remove("test.xml")

        # Call the function with a remote file, expect exit
        with pytest.raises(SystemExit):
            get_xml_data("http://example.com/", ".", "test.xml", "ArduCopter")

    @patch("ardupilot_methodic_configurator.annotate_params.download_file_from_url")
    def test_get_xml_data_valid_xml(self, mock_download: MagicMock) -> None:  # type: ignore[misc]
        # Mock success
        mock_download.return_value = True

        # Mock open to return valid XML
        with patch("builtins.open", mock.mock_open(read_data="<root></root>")):
            result = get_xml_data("http://example.com/", ".", "test.xml", "ArduCopter")

        # Check the result
        assert isinstance(result, ET.Element)

    @patch("ardupilot_methodic_configurator.annotate_params.download_file_from_url")
    def test_get_xml_data_invalid_xml(self, mock_download: MagicMock) -> None:  # type: ignore[misc]
        # Mock success
        mock_download.return_value = True

        # Mock open to return INVALID XML
        with patch("builtins.open", mock.mock_open(read_data="<root><invalid></root>")), pytest.raises(ET.ParseError):
            get_xml_data("http://example.com/", ".", "test.xml", "ArduCopter")

    @patch("ardupilot_methodic_configurator.annotate_params.download_file_from_url")
    @patch("os.path.isfile")
    def test_get_xml_data_missing_file(self, mock_isfile: MagicMock, mock_download: MagicMock) -> None:  # type: ignore[misc]
        # Mock the isfile function to return False
        mock_isfile.return_value = False
        # Mock download failure
        mock_download.return_value = False

        # Remove the test.xml file if it exists
        with contextlib.suppress(FileNotFoundError):
            os.remove("test.xml")

        # Call the function with a local file
        with pytest.raises(SystemExit):
            get_xml_data("/path/to/local/file/", ".", "test.xml", "ArduCopter")

    def test_remove_prefix(self) -> None:
        # Test case 1: Normal operation
        assert remove_prefix("prefix_test", "prefix_") == "test"

        # Test case 2: Prefix not present
        assert remove_prefix("test", "prefix_") == "test"

        # Test case 3: Empty string
        assert remove_prefix("", "prefix_") == ""

    def test_split_into_lines(self) -> None:
        # Test case 1: Normal operation
        string_to_split = "This is a test string. It should be split into several lines."
        maximum_line_length = 12
        expected_output = ["This is a", "test string.", "It should be", "split into", "several", "lines."]
        assert split_into_lines(string_to_split, maximum_line_length) == expected_output

        # Test case 2: String shorter than maximum line length
        string_to_split = "Short"
        maximum_line_length = 10
        expected_output = ["Short"]
        assert split_into_lines(string_to_split, maximum_line_length) == expected_output

        # Test case 3: Empty string
        string_to_split = ""
        maximum_line_length = 10
        expected_output = []
        assert split_into_lines(string_to_split, maximum_line_length) == expected_output

    def test_create_doc_dict(self) -> None:
        # Mock XML data
        xml_data = """
        <root>
            <param name="PARAM1" humanName="Param 1" documentation="Documentation for Param 1">
                <field name="Field1">Value1</field>
                <field name="Field2">Value2</field>
                <values>
                    <value code="Code1">Value1</value>
                    <value code="Code2">Value2</value>
                </values>
            </param>
            <param name="PARAM2" humanName="Param 2" documentation="Documentation for Param 2">
                <field name="Units">m/s</field>
                <field name="UnitText">meters per second</field>
                <values>
                    <value code="Code3">Value3</value>
                    <value code="Code4">Value4</value>
                </values>
            </param>
        </root>
        """
        root = DET.fromstring(xml_data)

        # Expected output
        expected_output = {
            "PARAM1": {
                "humanName": "Param 1",
                "documentation": ["Documentation for Param 1"],
                "fields": {"Field1": "Value1", "Field2": "Value2"},
                "values": {"Code1": "Value1", "Code2": "Value2"},
            },
            "PARAM2": {
                "humanName": "Param 2",
                "documentation": ["Documentation for Param 2"],
                "fields": {"Units": "m/s (meters per second)"},
                "values": {"Code3": "Value3", "Code4": "Value4"},
            },
        }

        # Call the function with the mock XML data
        result = create_doc_dict(root, "VehicleType")

        # Check the result
        assert result == expected_output

    def test_format_columns(self) -> None:
        # Define the input
        values = {
            "Key1": "Value1",
            "Key2": "Value2",
            "Key3": "Value3",
            "Key4": "Value4",
            "Key5": "Value5",
            "Key6": "Value6",
            "Key7": "Value7",
            "Key8": "Value8",
            "Key9": "Value9",
            "Key10": "Value10",
            "Key11": "Value11",
            "Key12": "Value12",
        }

        # Define the expected output
        expected_output = [
            "Key1: Value1                                         Key7: Value7",
            "Key2: Value2                                         Key8: Value8",
            "Key3: Value3                                         Key9: Value9",
            "Key4: Value4                                         Key10: Value10",
            "Key5: Value5                                         Key11: Value11",
            "Key6: Value6                                         Key12: Value12",
        ]

        # Call the function with the input
        result = format_columns(values)

        # Check the result
        assert result == expected_output

        assert not format_columns({})

    def test_update_parameter_documentation(self) -> None:
        # Write some initial content to the temporary file
        with open(self.temp_file.name, "w", encoding="utf-8") as file:
            file.write("PARAM1 100\n")

        # Call the function with the temporary file
        update_parameter_documentation(self.doc_dict, self.temp_file.name)

        # Read the updated content from the temporary file
        with open(self.temp_file.name, encoding="utf-8") as file:
            updated_content = file.read()

        # Check if the file has been updated correctly
        assert "Param 1" in updated_content
        assert "Documentation for Param 1" in updated_content
        assert "Field1: Value1" in updated_content
        assert "Field2: Value2" in updated_content
        assert "Code1: Value1" in updated_content
        assert "Code2: Value2" in updated_content

    def test_update_parameter_documentation_sorting_none(self) -> None:
        # Write some initial content to the temporary file
        # With stray leading and trailing whitespaces
        with open(self.temp_file.name, "w", encoding="utf-8") as file:
            file.write("PARAM2 100\n PARAM_1 100 \nPARAM3 3\nPARAM4 4\nPARAM5 5\nPARAM1 100\n")

        # Call the function with the temporary file
        update_parameter_documentation(self.doc_dict, self.temp_file.name)

        # Read the updated content from the temporary file
        with open(self.temp_file.name, encoding="utf-8") as file:
            updated_content = file.read()

        expected_content = """# Param 2
# Documentation for Param 2
# Field3: Value3
# Field4: Value4
# Code3: Value3
# Code4: Value4
PARAM2 100

# Param _ 1
# Documentation for Param_1
# Field_1: Value_1
# Field_2: Value_2
# Code_1: Value_1
# Code_2: Value_2
PARAM_1 100
PARAM3 3
PARAM4 4
PARAM5 5

# Param 1
# Documentation for Param 1
# Field1: Value1
# Field2: Value2
# Code1: Value1
# Code2: Value2
PARAM1 100
"""
        assert updated_content == expected_content

    def test_update_parameter_documentation_sorting_missionplanner(self) -> None:
        # Write some initial content to the temporary file
        with open(self.temp_file.name, "w", encoding="utf-8") as file:
            file.write("PARAM2 100 # ignore, me\nPARAM_1\t100\nPARAM1,100\n")

        # Call the function with the temporary file
        update_parameter_documentation(self.doc_dict, self.temp_file.name, "missionplanner")

        # Read the updated content from the temporary file
        with open(self.temp_file.name, encoding="utf-8") as file:
            updated_content = file.read()

        expected_content = """# Param _ 1
# Documentation for Param_1
# Field_1: Value_1
# Field_2: Value_2
# Code_1: Value_1
# Code_2: Value_2
PARAM_1\t100

# Param 1
# Documentation for Param 1
# Field1: Value1
# Field2: Value2
# Code1: Value1
# Code2: Value2
PARAM1,100

# Param 2
# Documentation for Param 2
# Field3: Value3
# Field4: Value4
# Code3: Value3
# Code4: Value4
PARAM2 100 # ignore, me
"""
        assert updated_content == expected_content

    def test_update_parameter_documentation_sorting_mavproxy(self) -> None:
        # Write some initial content to the temporary file
        with open(self.temp_file.name, "w", encoding="utf-8") as file:
            file.write("PARAM2 100\nPARAM_1\t100\nPARAM1,100\n")

        # Call the function with the temporary file
        update_parameter_documentation(self.doc_dict, self.temp_file.name, "mavproxy")

        # Read the updated content from the temporary file
        with open(self.temp_file.name, encoding="utf-8") as file:
            updated_content = file.read()

        expected_content = """# Param 1
# Documentation for Param 1
# Field1: Value1
# Field2: Value2
# Code1: Value1
# Code2: Value2
PARAM1,100

# Param 2
# Documentation for Param 2
# Field3: Value3
# Field4: Value4
# Code3: Value3
# Code4: Value4
PARAM2 100

# Param _ 1
# Documentation for Param_1
# Field_1: Value_1
# Field_2: Value_2
# Code_1: Value_1
# Code_2: Value_2
PARAM_1\t100
"""
        assert updated_content == expected_content

    def test_update_parameter_documentation_invalid_line_format(self) -> None:
        # Write some initial content to the temporary file with an invalid line format
        with open(self.temp_file.name, "w", encoding="utf-8") as file:
            file.write("%INVALID_LINE_FORMAT\n")

        # Call the function with the temporary file
        with pytest.raises(SystemExit) as cm:
            update_parameter_documentation(self.doc_dict, self.temp_file.name)

        # Check if the SystemExit exception contains the expected message
        assert cm.value.code == "Invalid line in input file"

    @patch("logging.Logger.info")
    def test_print_read_only_params(self, mock_info: MagicMock) -> None:  # type: ignore[misc]
        # Mock XML data
        xml_data = """
        <root>
            <param name="PARAM1" humanName="Param 1" documentation="Documentation for Param 1">
                <field name="ReadOnly">True</field>
                <field name="Field1">Value1</field>
                <field name="Field2">Value2</field>
                <values>
                    <value code="Code1">Value1</value>
                    <value code="Code2">Value2</value>
                </values>
            </param>
            <param name="PARAM2" humanName="Param 2" documentation="Documentation for Param 2">
                <field name="Field3">Value3</field>
                <field name="Field4">Value4</field>
                <values>
                    <value code="Code3">Value3</value>
                    <value code="Code4">Value4</value>
                </values>
            </param>
        </root>
        """
        root = DET.fromstring(xml_data)
        doc_dict = create_doc_dict(root, "VehicleType")

        # Call the function with the mock XML data
        print_read_only_params(doc_dict)

        # Check if the parameter name was logged
        mock_info.assert_has_calls([mock.call("ReadOnly parameters:"), mock.call("PARAM1")])

    def test_update_parameter_documentation_invalid_target(self) -> None:
        with pytest.raises(ValueError, match=r"Target 'invalid_target' is neither a file nor a directory\."):
            update_parameter_documentation(self.doc_dict, "invalid_target")

    def test_invalid_parameter_name(self) -> None:
        # Write some initial content to the temporary file
        with open(self.temp_file.name, "w", encoding="utf-8") as file:
            file.write("INVALID_$PARAM 100\n")

        # Call the function with the temporary file
        with pytest.raises(SystemExit):
            update_parameter_documentation(self.doc_dict, self.temp_file.name)

    def test_update_parameter_documentation_too_long_parameter_name(self) -> None:
        # Write some initial content to the temporary file
        with open(self.temp_file.name, "w", encoding="utf-8") as file:
            file.write("TOO_LONG_PARAMETER_NAME 100\n")

        # Call the function with the temporary file
        with pytest.raises(SystemExit):
            update_parameter_documentation(self.doc_dict, self.temp_file.name)

    @patch("logging.Logger.warning")
    def test_missing_parameter_documentation(self, mock_warning: MagicMock) -> None:  # type: ignore[misc]
        # Write some initial content to the temporary file
        with open(self.temp_file.name, "w", encoding="utf-8") as file:
            file.write("MISSING_DOC_PARA 100\n")

        # Call the function with the temporary file
        update_parameter_documentation(self.doc_dict, self.temp_file.name)

        # Check if the warnings were logged
        mock_warning.assert_has_calls(
            [
                mock.call("Read file %s with %d parameters, but only %s of which got documented", self.temp_file.name, 1, 0),
                mock.call("No documentation found for: %s", "MISSING_DOC_PARA"),
            ]
        )

    def test_empty_parameter_file(self) -> None:
        # Call the function with the temporary file
        update_parameter_documentation(self.doc_dict, self.temp_file.name)

        # Read the updated content from the temporary file
        with open(self.temp_file.name, encoding="utf-8") as file:
            updated_content = file.read()

        # Check if the file is still empty
        assert updated_content == ""

    def test_get_xml_url_valid_vehicles(self) -> None:
        """Test get_xml_url with all valid vehicle types."""
        vehicle_types = ["ArduCopter", "ArduPlane", "Rover", "ArduSub", "AntennaTracker", "AP_Periph", "Blimp", "Heli", "SITL"]
        for vehicle in vehicle_types:
            url = get_xml_url(vehicle, "4.3")
            assert url.startswith(BASE_URL)
            assert "stable-4.3" in url
            assert url.endswith("/")

    def test_get_xml_url_invalid_vehicle(self) -> None:
        """Test get_xml_url with invalid vehicle type."""
        with pytest.raises(ValueError, match=r"Vehicle type 'InvalidVehicle' is not supported\."):
            get_xml_url("InvalidVehicle", "4.3")

    def test_split_into_lines_edge_cases(self) -> None:
        """Test split_into_lines with edge cases."""
        # Test with various line lengths
        # Function will return largest possible chunks based on max length
        assert split_into_lines("a b c", 2) == ["a", "b", "c"]
        assert split_into_lines("", 10) == []

    def test_format_columns_edge_cases(self) -> None:
        """Test format_columns with edge cases."""
        # Empty dictionary
        assert not format_columns({})

        # Single item
        assert format_columns({"Key": "Value"}) == ["Key: Value"]

        # Test with different max widths
        values = {"K1": "V1", "K2": "V2"}
        assert len(format_columns(values, max_width=20)[0]) <= 20

        # Test with many columns
        many_values = {f"Key{i}": f"Value{i}" for i in range(20)}
        result = format_columns(many_values, max_width=200, max_columns=5)
        assert all(len(line) <= 200 for line in result)

    def test_create_doc_dict_edge_cases(self) -> None:
        """Test create_doc_dict with edge cases."""
        # Test with empty XML
        empty_root = ET.Element("root")
        assert not create_doc_dict(empty_root, "ArduCopter")

        # Test with missing attributes
        param = ET.SubElement(empty_root, "param")
        assert not create_doc_dict(empty_root, "ArduCopter")

        # Test with minimal valid param
        param.set("name", "TEST_PARAM")
        param.set("humanName", "Test Parameter")
        param.set("documentation", "Test documentation")
        doc_dict = create_doc_dict(empty_root, "ArduCopter")
        assert "TEST_PARAM" in doc_dict
        assert doc_dict["TEST_PARAM"]["humanName"] == "Test Parameter"

    @patch("os.path.isfile")
    def test_update_parameter_documentation_sorting(self, mock_isfile: MagicMock) -> None:  # type: ignore[misc]
        """Test parameter sorting in update_parameter_documentation."""
        # Mock file existence check
        mock_isfile.return_value = True

        test_content = "PARAM_Z 100\nPARAM_A 200\nPARAM_M 300\n"

        # Create a real temporary file for testing
        with tempfile.NamedTemporaryFile(mode="w", delete=False) as temp_file:
            temp_file.write(test_content)
            temp_file_name = temp_file.name

        doc = {
            "PARAM_Z": {"humanName": "Z", "documentation": ["Z doc"], "fields": {}, "values": {}},
            "PARAM_A": {"humanName": "A", "documentation": ["A doc"], "fields": {}, "values": {}},
            "PARAM_M": {"humanName": "M", "documentation": ["M doc"], "fields": {}, "values": {}},
        }

        try:
            # Test MissionPlanner sorting
            update_parameter_documentation(doc, temp_file_name, "missionplanner")

            with open(temp_file_name, encoding="utf-8") as f:
                content = f.read()

            # Verify content and order
            assert "PARAM_A" in content
            assert "PARAM_M" in content
            assert "PARAM_Z" in content
            assert content.index("PARAM_A") < content.index("PARAM_M") < content.index("PARAM_Z")

            # Test MAVProxy sorting
            # Reset file content
            with open(temp_file_name, "w", encoding="utf-8") as f:
                f.write(test_content)

            update_parameter_documentation(doc, temp_file_name, "mavproxy")

            with open(temp_file_name, encoding="utf-8") as f:
                content = f.read()

            # Verify content for MAVProxy format
            assert "PARAM_A" in content
            assert "PARAM_M" in content
            assert "PARAM_Z" in content

        finally:
            # Clean up
            os.unlink(temp_file_name)

    def test_extract_parameter_name_and_validate_invalid_cases(self) -> None:
        """Test parameter name validation with invalid cases."""
        # Test invalid parameter name pattern
        with pytest.raises(SystemExit):
            extract_parameter_name_and_validate("invalid_param 100", "test.param", 1)

        # Test too long parameter name
        with pytest.raises(SystemExit):
            extract_parameter_name_and_validate("VERY_LONG_PARAMETER_NAME_THAT_EXCEEDS_LIMIT 100", "test.param", 1)

        # Test invalid separator
        with pytest.raises(SystemExit):
            extract_parameter_name_and_validate("PARAM:100", "test.param", 1)

    def test_par_class_methods(self) -> None:
        """Test Par class methods."""
        # Test equality
        par1 = Par(100.0, "comment1")
        par2 = Par(100.0, "comment1")
        par3 = Par(200.0, "comment2")

        assert par1 == par2
        assert par1 != par3
        assert par1 != "not a Par object"

        # Test load_param_file with invalid values
        with tempfile.NamedTemporaryFile(mode="w", delete=False) as tf:
            tf.write("PARAM1 invalid_value\n")
            tf.flush()

            with pytest.raises(SystemExit):
                ParDict.load_param_file_into_dict(tf.name)

    def test_format_params_methods(self) -> None:
        """Test ParDict._format_params method."""
        param_dict = ParDict({"PARAM1": Par(100.0, "comment1"), "PARAM2": Par(200.0), "PARAM3": Par(300.0)})

        # Test MissionPlanner format
        mp_format = param_dict._format_params("missionplanner")
        assert any("PARAM1,100" in line for line in mp_format)
        assert any("# comment1" in line for line in mp_format)

        # Test MAVProxy format
        mavproxy_format = param_dict._format_params("mavproxy")
        # Use correct spacing format - 16 chars for name, 8 for value
        assert any("PARAM1           100.000000" in line for line in mavproxy_format)
        assert any("# comment1" in line for line in mavproxy_format)

        # Test invalid format
        with pytest.raises(SystemExit):
            param_dict._format_params("invalid_format")


class AnnotateParamsTest(unittest.TestCase):
    """Test annotate parameters."""

    def test_arg_parser_valid_arguments(self) -> None:
        test_args = ["annotate_params", "--vehicle-type", "ArduCopter", "--sort", "none", "parameters"]
        with patch("sys.argv", test_args):
            args = parse_arguments()
            assert args.vehicle_type == "ArduCopter"
            assert args.sort == "none"
            assert args.target == "parameters"
            assert args.verbose is False
            assert args.max_line_length == 100

    def test_arg_parser_invalid_vehicle_type(self) -> None:
        test_args = ["annotate_params", "--vehicle-type", "InvalidType", "--sort", "none", "parameters"]
        with patch("sys.argv", test_args), pytest.raises(SystemExit):
            parse_arguments()

    def test_arg_parser_invalid_sort_option(self) -> None:
        test_args = ["annotate_params", "--vehicle-type", "ArduCopter", "--sort", "invalid", "parameters"]
        with patch("sys.argv", test_args), pytest.raises(SystemExit):
            parse_arguments()

    def test_arg_parser_invalid_line_length_option(self) -> None:
        test_args = ["annotate_params", "--vehicle-type", "ArduCopter", "--sort", "none", "-m", "invalid", "parameters"]
        with patch("sys.argv", test_args), pytest.raises(SystemExit):
            parse_arguments()


class TestAnnotateParamsExceptionHandling(unittest.TestCase):
    """Test parameter exception handling."""

    @pytest.mark.usefixtures("mock_update", "mock_get_xml_dir", "mock_get_xml_url")
    @patch("builtins.open", new_callable=mock.mock_open)
    def test_main_ioerror(self, mock_file: MagicMock) -> None:  # type: ignore[misc]
        with patch("ardupilot_methodic_configurator.annotate_params.parse_arguments") as mock_arg_parser:
            mock_arg_parser.return_value = Mock(
                vehicle_type="ArduCopter",
                firmware_version="4.0",
                target=".",
                sort="none",
                delete_documentation_annotations=False,
                verbose=False,
            )
            mock_file.side_effect = OSError("Mocked IO Error")

            with pytest.raises(SystemExit) as cm:
                main()

            assert cm.value.code in [1, 2]

    @pytest.mark.usefixtures("mock_update", "mock_get_xml_dir", "mock_get_xml_url")
    @patch("builtins.open", new_callable=mock.mock_open)
    def test_main_oserror(self, mock_file: MagicMock) -> None:  # type: ignore[misc]
        with patch("ardupilot_methodic_configurator.annotate_params.parse_arguments") as mock_arg_parser:
            mock_arg_parser.return_value = Mock(
                vehicle_type="ArduCopter",
                firmware_version="4.0",
                target=".",
                sort="none",
                delete_documentation_annotations=False,
                verbose=False,
            )
            mock_file.side_effect = OSError("Mocked OS Error")

            with pytest.raises(SystemExit) as cm:
                main()

            assert cm.value.code in [1, 2]

    @patch("ardupilot_methodic_configurator.annotate_params.get_xml_url")
    def test_get_xml_url_exception(self, mock_get_xml_url_: MagicMock) -> None:  # type: ignore[misc]
        mock_get_xml_url_.side_effect = ValueError("Mocked Value Error")
        with pytest.raises(ValueError, match=r"Vehicle type 'NonExistingVehicle' is not supported\."):
            get_xml_url("NonExistingVehicle", "4.0")

    def test_extract_parameter_name_and_validate_edge_cases(self) -> None:
        """Test extract_parameter_name_and_validate with various edge cases."""
        # Valid parameter names with different separators
        assert extract_parameter_name_and_validate("PARAM1,100", "test.param", 1) == "PARAM1"
        assert extract_parameter_name_and_validate("PARAM2 100", "test.param", 1) == "PARAM2"
        assert extract_parameter_name_and_validate("PARAM3\t100", "test.param", 1) == "PARAM3"

        # Parameter name must start with capital letter
        with pytest.raises(SystemExit):
            extract_parameter_name_and_validate("param4 100", "test.param", 1)

        # Parameter name can't have special characters
        with pytest.raises(SystemExit):
            extract_parameter_name_and_validate("PARAM-5 100", "test.param", 1)

        # Parameter must be followed by a valid separator
        with pytest.raises(SystemExit):
            extract_parameter_name_and_validate("PARAM6:100", "test.param", 1)

    def test_par_class_methods_comprehensive(self) -> None:
        """Test Par class methods more comprehensively."""
        # Test Par.__init__ and equality
        par1 = Par(10.5, "comment")
        par2 = Par(10.5, "comment")
        par3 = Par(10.5, "different comment")
        par4 = Par(11.0, "comment")

        assert par1 == par2
        assert par1 != par3
        assert par1 != par4
        assert par1 != "not a Par object"

        # Test load_param_file_into_dict with various formats
        with tempfile.NamedTemporaryFile(mode="w", delete=False) as tf:
            tf.write("PARAM1,10.5 # comment1\n")
            tf.write("PARAM2 20.75 # comment2\n")
            tf.write("PARAM3\t30.25\n")  # tab separator, no comment
            tf.write("# This is a full line comment\n")
            tf.write("\n")  # empty line
            tf_name = tf.name

        param_dict = ParDict.load_param_file_into_dict(tf_name)
        os.unlink(tf_name)

        assert len(param_dict) == 3
        assert param_dict["PARAM1"].value == 10.5
        assert param_dict["PARAM1"].comment == "comment1"
        assert param_dict["PARAM2"].value == 20.75
        assert param_dict["PARAM2"].comment == "comment2"
        assert param_dict["PARAM3"].value == 30.25
        assert param_dict["PARAM3"].comment is None

        # Test load_param_file_into_dict with duplicate parameters
        with tempfile.NamedTemporaryFile(mode="w", delete=False) as tf:
            tf.write("PARAM1,10.5\n")
            tf.write("PARAM1,20.5\n")  # Duplicate parameter
            tf_name = tf.name

        with pytest.raises(SystemExit):
            ParDict.load_param_file_into_dict(tf_name)
        os.unlink(tf_name)

    def test_missionplanner_sort_function(self) -> None:
        """Test the missionplanner_sort function."""
        # Basic sorting
        params = ["Z_PARAM", "A_PARAM", "C_PARAM", "B_PARAM"]
        sorted_params = sorted(params, key=missionplanner_sort)
        assert sorted_params == ["A_PARAM", "B_PARAM", "C_PARAM", "Z_PARAM"]

        # Complex sorting with underscores
        params = ["RC_FEEL", "RC_MAP_ROLL", "RC_MAP_PITCH", "RC_1_MIN"]
        sorted_params = sorted(params, key=missionplanner_sort)
        assert sorted_params == ["RC_1_MIN", "RC_FEEL", "RC_MAP_PITCH", "RC_MAP_ROLL"]

        # Sort with line content
        lines = ["RC_FEEL 100", "RC_MAP_ROLL 200", "RC_MAP_PITCH 300", "RC_1_MIN 400"]
        sorted_lines = sorted(lines, key=missionplanner_sort)
        assert sorted_lines == ["RC_1_MIN 400", "RC_FEEL 100", "RC_MAP_PITCH 300", "RC_MAP_ROLL 200"]

    @patch("logging.Logger.warning")
    def test_update_parameter_documentation_edge_cases(self, mock_warning) -> None:  # pylint: disable=unused-argument
        """Test update_parameter_documentation with edge cases."""
        # Create a simplified doc_dict for testing
        doc_dict = {
            "PARAM1": {
                "humanName": "Parameter 1",
                "documentation": ["Documentation for Parameter 1"],
                "fields": {},
                "values": {},
            },
            "PARAM2": {
                "humanName": "Parameter 2",
                "documentation": ["Documentation for Parameter 2"],
                "fields": {},
                "values": {},
            },
        }

        # Create a temporary directory
        with tempfile.TemporaryDirectory() as temp_dir:
            # Create an empty file
            empty_file = os.path.join(temp_dir, "empty.param")
            with open(empty_file, "w", encoding="utf-8") as f:
                pass

            # Create file with only comments and whitespace
            comments_file = os.path.join(temp_dir, "comments.param")
            with open(comments_file, "w", encoding="utf-8") as f:
                f.write("# This is a comment\n\n# Another comment\n")

            # Create file with comments and parameters
            mixed_file = os.path.join(temp_dir, "mixed.param")
            with open(mixed_file, "w", encoding="utf-8") as f:
                f.write("# A comment\nPARAM1,100\n\n# Another comment\nPARAM2 200\n")

            # Test with empty file
            update_parameter_documentation(doc_dict, empty_file)
            with open(empty_file, encoding="utf-8") as f:
                content = f.read()
            assert content == ""

            # Test with comments-only file
            update_parameter_documentation(doc_dict, comments_file)
            with open(comments_file, encoding="utf-8") as f:
                content = f.read()
            assert content == ""

            # Test with mixed content and delete_documentation_annotations=True
            update_parameter_documentation(doc_dict, mixed_file, delete_documentation_annotations=True)
            with open(mixed_file, encoding="utf-8") as f:
                content = f.read()
            assert "# Parameter 1" not in content
            assert "PARAM1,100" in content
            assert "PARAM2 200" in content

            # Write the file again with annotations
            with open(mixed_file, "w", encoding="utf-8") as f:
                f.write("# A comment\nPARAM1,100\n\n# Another comment\nPARAM2 200\n")

            # Test with mixed content and regular annotations
            update_parameter_documentation(doc_dict, mixed_file)
            with open(mixed_file, encoding="utf-8") as f:
                content = f.read()
            assert "# Parameter 1" in content
            assert "# Documentation for Parameter 1" in content
            assert "PARAM1,100" in content
            assert "# Parameter 2" in content
            assert "# Documentation for Parameter 2" in content
            assert "PARAM2 200" in content

    def test_extract_parameter_name_and_validate_comprehensive(self) -> None:
        """Comprehensive testing of parameter name extraction and validation."""
        # Valid parameter names with different separators
        assert extract_parameter_name_and_validate("PARAM1,100", "test.param", 1) == "PARAM1"
        assert extract_parameter_name_and_validate("PARAM2 100", "test.param", 1) == "PARAM2"
        assert extract_parameter_name_and_validate("PARAM3\t100", "test.param", 1) == "PARAM3"
        assert extract_parameter_name_and_validate("A123_XYZ,100", "test.param", 1) == "A123_XYZ"

        # Parameter name with trailing whitespace before separator
        assert extract_parameter_name_and_validate("PARAM4   \t100", "test.param", 1) == "PARAM4"

        # Invalid parameter format (not starting with uppercase letter)
        with pytest.raises(SystemExit):
            extract_parameter_name_and_validate("param5 100", "test.param", 1)

        # Invalid parameter format (invalid character)
        with pytest.raises(SystemExit):
            extract_parameter_name_and_validate("PARAM-5 100", "test.param", 1)

        # Invalid separator
        with pytest.raises(SystemExit):
            extract_parameter_name_and_validate("PARAM6:100", "test.param", 1)

        # Parameter name too long
        with pytest.raises(SystemExit):
            extract_parameter_name_and_validate("ABCDEFGHIJKLMNOPQR 100", "test.param", 1)

        # Empty line
        with pytest.raises(SystemExit):
            extract_parameter_name_and_validate("", "test.param", 1)


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