"""
Tests for custom property support on edit_schematic_component,
set_schematic_component_property, and remove_schematic_component_property.

Custom properties are arbitrary key/value fields attached to a placed schematic
symbol — used for BOM / sourcing metadata such as MPN, Manufacturer,
DigiKey_PN, LCSC, JLCPCB_PN, Voltage, Tolerance, Dielectric, etc.
"""

import re
import sys
from pathlib import Path
from typing import Any

import pytest

sys.path.insert(0, str(Path(__file__).parent.parent / "python"))


TEMPLATE_SCH = Path(__file__).parent.parent / "python" / "templates" / "empty.kicad_sch"

# Minimal placed-symbol block embedded into the test schematic
PLACED_RESISTOR_BLOCK = """\
  (symbol (lib_id "Device:R") (at 50 50 0) (unit 1)
    (in_bom yes) (on_board yes) (dnp no)
    (uuid "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee")
    (property "Reference" "R1" (at 51.27 47.46 0)
      (effects (font (size 1.27 1.27)))
    )
    (property "Value" "10k" (at 51.27 52.54 0)
      (effects (font (size 1.27 1.27)))
    )
    (property "Footprint" "Resistor_SMD:R_0603_1608Metric" (at 50 50 0)
      (effects (font (size 1.27 1.27)) hide)
    )
    (property "Datasheet" "~" (at 50 50 0)
      (effects (font (size 1.27 1.27)) hide)
    )
  )
"""

# Multi-line placed-symbol block in the format KiCad emits for symbols whose
# library entry has been rescued / customised — these carry an extra
# (lib_name "...") child BEFORE (lib_id "..."). Reproduced from a real user
# schematic that exposed a regex bug where (symbol (lib_id "...")) was the
# only matched form and `(symbol (lib_name "...") (lib_id "..."))` placed
# components were invisible to get/edit/delete.
PLACED_RESISTOR_BLOCK_LIBNAME_FIRST = """\
  (symbol
    (lib_name "RESISTOR_0603_4")
    (lib_id "MF_Passives:RESISTOR_0603")
    (at 132.08 44.45 90)
    (unit 1)
    (in_bom yes)
    (on_board yes)
    (dnp no)
    (uuid "bbbbbbbb-cccc-dddd-eeee-ffffffffffff")
    (property "Reference" "R7"
      (at 132.08 40.894 90)
      (effects (font (size 1.143 1.143)))
    )
    (property "Value" "499k"
      (at 132.08 42.418 90)
      (effects (font (size 1.143 1.143)))
    )
    (property "Footprint" "MF_Passives_R0603"
      (at 134.366 38.1 0)
      (effects (font (size 1.27 1.27)) (hide yes))
    )
    (property "Datasheet" "~"
      (at 132.08 44.45 0)
      (effects (font (size 1.27 1.27)) (hide yes))
    )
  )
"""


def _make_test_schematic(tmp_dir: Path, extra_block: str = "") -> Path:
    """Copy empty.kicad_sch into tmp_dir, optionally appending a placed symbol block."""
    dest = tmp_dir / "test.kicad_sch"
    src_content = TEMPLATE_SCH.read_text(encoding="utf-8")
    if extra_block:
        src_content = src_content.rstrip()
        if src_content.endswith(")"):
            src_content = src_content[:-1] + "\n" + extra_block + ")\n"
    dest.write_text(src_content, encoding="utf-8")
    return dest


# ---------------------------------------------------------------------------
# Pure unit tests — exercise the static helpers in isolation.
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestStaticHelpers:
    """Tests for _escape_sexpr_string and _find_matching_paren."""

    def _iface(self) -> Any:
        from kicad_interface import KiCADInterface

        return KiCADInterface

    def test_escape_handles_quotes(self) -> None:
        cls = self._iface()
        assert cls._escape_sexpr_string('a"b') == 'a\\"b'

    def test_escape_handles_backslashes(self) -> None:
        cls = self._iface()
        assert cls._escape_sexpr_string("a\\b") == "a\\\\b"

    def test_escape_handles_both(self) -> None:
        cls = self._iface()
        # Order matters: backslashes are doubled first, then quotes
        assert cls._escape_sexpr_string('a"b\\c') == 'a\\"b\\\\c'

    def test_escape_passes_normal_text(self) -> None:
        cls = self._iface()
        assert cls._escape_sexpr_string("RC0603FR-0710KL") == "RC0603FR-0710KL"

    def test_find_matching_paren_simple(self) -> None:
        cls = self._iface()
        s = "(abc)"
        assert cls._find_matching_paren(s, 0) == 4

    def test_find_matching_paren_nested(self) -> None:
        cls = self._iface()
        s = "(a (b (c) d) e)"
        assert cls._find_matching_paren(s, 0) == 14
        assert cls._find_matching_paren(s, 3) == 11
        assert cls._find_matching_paren(s, 6) == 8

    def test_find_matching_paren_no_match(self) -> None:
        cls = self._iface()
        s = "(abc"
        assert cls._find_matching_paren(s, 0) == -1


# ---------------------------------------------------------------------------
# Integration tests — full file I/O through the public command interface.
# ---------------------------------------------------------------------------


@pytest.mark.integration
class TestEditSchematicComponentProperties:
    """Tests for the new `properties` and `removeProperties` parameters."""

    @pytest.fixture
    def sch_with_r1(self, tmp_path: Any) -> Any:
        return _make_test_schematic(tmp_path, PLACED_RESISTOR_BLOCK)

    def _iface(self) -> Any:
        from kicad_interface import KiCADInterface

        return KiCADInterface()

    def test_add_single_custom_property_string(self, sch_with_r1: Any) -> None:
        iface = self._iface()
        result = iface.handle_command(
            "edit_schematic_component",
            {
                "schematicPath": str(sch_with_r1),
                "reference": "R1",
                "properties": {"MPN": "RC0603FR-0710KL"},
            },
        )
        assert result["success"] is True
        assert "MPN" in result["updated"]["propertiesAdded"]

        get_result = iface.handle_command(
            "get_schematic_component",
            {"schematicPath": str(sch_with_r1), "reference": "R1"},
        )
        assert "MPN" in get_result["fields"]
        assert get_result["fields"]["MPN"]["value"] == "RC0603FR-0710KL"

    def test_add_multiple_custom_properties(self, sch_with_r1: Any) -> None:
        iface = self._iface()
        result = iface.handle_command(
            "edit_schematic_component",
            {
                "schematicPath": str(sch_with_r1),
                "reference": "R1",
                "properties": {
                    "MPN": "RC0603FR-0710KL",
                    "Manufacturer": "Yageo",
                    "Tolerance": "1%",
                    "Power": "0.1W",
                },
            },
        )
        assert result["success"] is True
        assert set(result["updated"]["propertiesAdded"].keys()) == {
            "MPN",
            "Manufacturer",
            "Tolerance",
            "Power",
        }

        get_result = iface.handle_command(
            "get_schematic_component",
            {"schematicPath": str(sch_with_r1), "reference": "R1"},
        )
        for name, expected_value in [
            ("MPN", "RC0603FR-0710KL"),
            ("Manufacturer", "Yageo"),
            ("Tolerance", "1%"),
            ("Power", "0.1W"),
        ]:
            assert name in get_result["fields"], f"Missing property {name}"
            assert get_result["fields"][name]["value"] == expected_value

    def test_update_existing_custom_property(self, sch_with_r1: Any) -> None:
        iface = self._iface()
        # First add
        iface.handle_command(
            "edit_schematic_component",
            {
                "schematicPath": str(sch_with_r1),
                "reference": "R1",
                "properties": {"MPN": "OLD-PN"},
            },
        )
        # Then update
        result = iface.handle_command(
            "edit_schematic_component",
            {
                "schematicPath": str(sch_with_r1),
                "reference": "R1",
                "properties": {"MPN": "RC0603FR-0710KL"},
            },
        )
        assert result["success"] is True
        assert "MPN" in result["updated"]["propertiesUpdated"]

        get_result = iface.handle_command(
            "get_schematic_component",
            {"schematicPath": str(sch_with_r1), "reference": "R1"},
        )
        assert get_result["fields"]["MPN"]["value"] == "RC0603FR-0710KL"

    def test_add_property_with_full_spec_dict(self, sch_with_r1: Any) -> None:
        iface = self._iface()
        result = iface.handle_command(
            "edit_schematic_component",
            {
                "schematicPath": str(sch_with_r1),
                "reference": "R1",
                "properties": {
                    "MPN": {
                        "value": "RC0603FR-0710KL",
                        "x": 60.0,
                        "y": 60.0,
                        "angle": 90,
                        "hide": False,
                    }
                },
            },
        )
        assert result["success"] is True

        get_result = iface.handle_command(
            "get_schematic_component",
            {"schematicPath": str(sch_with_r1), "reference": "R1"},
        )
        mpn = get_result["fields"]["MPN"]
        assert mpn["value"] == "RC0603FR-0710KL"
        assert mpn["x"] == pytest.approx(60.0)
        assert mpn["y"] == pytest.approx(60.0)
        assert mpn["angle"] == pytest.approx(90.0)

        # Verify the (hide no) flag actually made it into the file
        content = sch_with_r1.read_text(encoding="utf-8")
        m = re.search(
            r'\(property\s+"MPN"\s+"[^"]*"\s+\(at[^)]+\)\s+\(effects.*?\(hide no\)',
            content,
            re.DOTALL,
        )
        assert m is not None, "Expected (hide no) on the MPN property"

    def test_new_property_defaults_to_hidden(self, sch_with_r1: Any) -> None:
        iface = self._iface()
        iface.handle_command(
            "edit_schematic_component",
            {
                "schematicPath": str(sch_with_r1),
                "reference": "R1",
                "properties": {"DigiKey_PN": "311-10.0KHRCT-ND"},
            },
        )
        content = sch_with_r1.read_text(encoding="utf-8")
        # Match (hide yes) inside the DigiKey_PN property block
        m = re.search(
            r'\(property\s+"DigiKey_PN"\s+"[^"]*"\s+\(at[^)]+\)\s+\(effects.*?\(hide yes\)',
            content,
            re.DOTALL,
        )
        assert m is not None, "New custom properties should default to (hide yes)"

    def test_property_added_at_component_origin_by_default(self, sch_with_r1: Any) -> None:
        iface = self._iface()
        iface.handle_command(
            "edit_schematic_component",
            {
                "schematicPath": str(sch_with_r1),
                "reference": "R1",
                "properties": {"MPN": "RC0603FR-0710KL"},
            },
        )
        get_result = iface.handle_command(
            "get_schematic_component",
            {"schematicPath": str(sch_with_r1), "reference": "R1"},
        )
        mpn = get_result["fields"]["MPN"]
        # Default position should equal the parent symbol's (50, 50)
        assert mpn["x"] == pytest.approx(50.0)
        assert mpn["y"] == pytest.approx(50.0)

    def test_remove_custom_property(self, sch_with_r1: Any) -> None:
        iface = self._iface()
        iface.handle_command(
            "edit_schematic_component",
            {
                "schematicPath": str(sch_with_r1),
                "reference": "R1",
                "properties": {"MPN": "RC0603FR-0710KL"},
            },
        )
        result = iface.handle_command(
            "edit_schematic_component",
            {
                "schematicPath": str(sch_with_r1),
                "reference": "R1",
                "removeProperties": ["MPN"],
            },
        )
        assert result["success"] is True
        assert "MPN" in result["updated"]["propertiesRemoved"]

        get_result = iface.handle_command(
            "get_schematic_component",
            {"schematicPath": str(sch_with_r1), "reference": "R1"},
        )
        assert "MPN" not in get_result["fields"]

    def test_remove_protected_field_rejected(self, sch_with_r1: Any) -> None:
        iface = self._iface()
        for name in ("Reference", "Value", "Footprint", "Datasheet"):
            result = iface.handle_command(
                "edit_schematic_component",
                {
                    "schematicPath": str(sch_with_r1),
                    "reference": "R1",
                    "removeProperties": [name],
                },
            )
            assert result["success"] is False, f"Removal of {name} should be rejected"
            assert name in result["message"]

    def test_remove_non_existent_property_is_noop(self, sch_with_r1: Any) -> None:
        iface = self._iface()
        result = iface.handle_command(
            "edit_schematic_component",
            {
                "schematicPath": str(sch_with_r1),
                "reference": "R1",
                "removeProperties": ["DoesNotExist"],
            },
        )
        assert result["success"] is True
        # No-op: the field was not present, so it should not appear in propertiesRemoved
        assert "propertiesRemoved" not in result["updated"]

    def test_batch_update_adds_and_removes_atomically(self, sch_with_r1: Any) -> None:
        iface = self._iface()
        iface.handle_command(
            "edit_schematic_component",
            {
                "schematicPath": str(sch_with_r1),
                "reference": "R1",
                "properties": {"OldField": "drop_me"},
            },
        )
        result = iface.handle_command(
            "edit_schematic_component",
            {
                "schematicPath": str(sch_with_r1),
                "reference": "R1",
                "properties": {
                    "MPN": "RC0603FR-0710KL",
                    "Manufacturer": "Yageo",
                },
                "removeProperties": ["OldField"],
            },
        )
        assert result["success"] is True
        assert set(result["updated"]["propertiesAdded"].keys()) == {"MPN", "Manufacturer"}
        assert "OldField" in result["updated"]["propertiesRemoved"]

        get_result = iface.handle_command(
            "get_schematic_component",
            {"schematicPath": str(sch_with_r1), "reference": "R1"},
        )
        assert "OldField" not in get_result["fields"]
        assert "MPN" in get_result["fields"]
        assert "Manufacturer" in get_result["fields"]

    def test_property_with_special_chars_is_escaped(self, sch_with_r1: Any) -> None:
        """Values containing " and \\ must be backslash-escaped in the .kicad_sch file
        so the resulting S-expression is still well-formed and can be re-opened by KiCad.
        """
        iface = self._iface()
        tricky = 'Has "quotes" and \\backslash'
        result = iface.handle_command(
            "edit_schematic_component",
            {
                "schematicPath": str(sch_with_r1),
                "reference": "R1",
                "properties": {"Description": tricky},
            },
        )
        assert result["success"] is True

        # Inspect the file directly: the on-disk form must contain the escaped
        # representation, NOT the raw quotes (which would corrupt the S-expression).
        content = sch_with_r1.read_text(encoding="utf-8")
        assert (
            r'(property "Description" "Has \"quotes\" and \\backslash"' in content
        ), f"Expected escaped property value in file. Got:\n{content[-1000:]}"

    def test_existing_value_field_is_unchanged_when_adding_property(self, sch_with_r1: Any) -> None:
        iface = self._iface()
        iface.handle_command(
            "edit_schematic_component",
            {
                "schematicPath": str(sch_with_r1),
                "reference": "R1",
                "properties": {"MPN": "RC0603FR-0710KL"},
            },
        )
        get_result = iface.handle_command(
            "get_schematic_component",
            {"schematicPath": str(sch_with_r1), "reference": "R1"},
        )
        # Built-in fields must be untouched
        assert get_result["fields"]["Value"]["value"] == "10k"
        assert get_result["fields"]["Reference"]["value"] == "R1"
        assert get_result["fields"]["Footprint"]["value"] == "Resistor_SMD:R_0603_1608Metric"

    def test_uuid_preserved_after_property_changes(self, sch_with_r1: Any) -> None:
        iface = self._iface()
        before = sch_with_r1.read_text(encoding="utf-8")
        iface.handle_command(
            "edit_schematic_component",
            {
                "schematicPath": str(sch_with_r1),
                "reference": "R1",
                "properties": {"MPN": "RC0603FR-0710KL", "Manufacturer": "Yageo"},
                "removeProperties": ["Datasheet"] if False else None,
            },
        )
        after = sch_with_r1.read_text(encoding="utf-8")
        assert "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" in after
        # And the uuid must still be the only one (we did not duplicate the symbol)
        assert before.count("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee") == after.count(
            "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
        )

    def test_unknown_reference_returns_failure(self, sch_with_r1: Any) -> None:
        iface = self._iface()
        result = iface.handle_command(
            "edit_schematic_component",
            {
                "schematicPath": str(sch_with_r1),
                "reference": "R99",
                "properties": {"MPN": "RC0603FR-0710KL"},
            },
        )
        assert result["success"] is False

    def test_invalid_properties_type_returns_failure(self, sch_with_r1: Any) -> None:
        iface = self._iface()
        result = iface.handle_command(
            "edit_schematic_component",
            {
                "schematicPath": str(sch_with_r1),
                "reference": "R1",
                "properties": ["not", "a", "dict"],
            },
        )
        assert result["success"] is False

    def test_invalid_remove_properties_type_returns_failure(self, sch_with_r1: Any) -> None:
        iface = self._iface()
        result = iface.handle_command(
            "edit_schematic_component",
            {
                "schematicPath": str(sch_with_r1),
                "reference": "R1",
                "removeProperties": "MPN",  # should be a list
            },
        )
        assert result["success"] is False


# ---------------------------------------------------------------------------
# Tests for the dedicated set_/remove_ tools
# ---------------------------------------------------------------------------


@pytest.mark.integration
class TestSetSchematicComponentProperty:
    """Tests for the convenience `set_schematic_component_property` tool."""

    @pytest.fixture
    def sch_with_r1(self, tmp_path: Any) -> Any:
        return _make_test_schematic(tmp_path, PLACED_RESISTOR_BLOCK)

    def _iface(self) -> Any:
        from kicad_interface import KiCADInterface

        return KiCADInterface()

    def test_set_creates_property(self, sch_with_r1: Any) -> None:
        iface = self._iface()
        result = iface.handle_command(
            "set_schematic_component_property",
            {
                "schematicPath": str(sch_with_r1),
                "reference": "R1",
                "name": "MPN",
                "value": "RC0603FR-0710KL",
            },
        )
        assert result["success"] is True
        assert result["updated"]["propertiesAdded"]["MPN"] == "RC0603FR-0710KL"

    def test_set_updates_existing_property(self, sch_with_r1: Any) -> None:
        iface = self._iface()
        iface.handle_command(
            "set_schematic_component_property",
            {
                "schematicPath": str(sch_with_r1),
                "reference": "R1",
                "name": "MPN",
                "value": "OLD",
            },
        )
        result = iface.handle_command(
            "set_schematic_component_property",
            {
                "schematicPath": str(sch_with_r1),
                "reference": "R1",
                "name": "MPN",
                "value": "NEW",
            },
        )
        assert result["success"] is True
        assert result["updated"]["propertiesUpdated"]["MPN"] == "NEW"

    def test_set_with_visible_position(self, sch_with_r1: Any) -> None:
        iface = self._iface()
        result = iface.handle_command(
            "set_schematic_component_property",
            {
                "schematicPath": str(sch_with_r1),
                "reference": "R1",
                "name": "MPN",
                "value": "RC0603FR-0710KL",
                "x": 70.0,
                "y": 65.0,
                "hide": False,
                "fontSize": 0.85,
            },
        )
        assert result["success"] is True

        get_result = iface.handle_command(
            "get_schematic_component",
            {"schematicPath": str(sch_with_r1), "reference": "R1"},
        )
        mpn = get_result["fields"]["MPN"]
        assert mpn["x"] == pytest.approx(70.0)
        assert mpn["y"] == pytest.approx(65.0)

    def test_set_missing_name_fails(self, sch_with_r1: Any) -> None:
        iface = self._iface()
        result = iface.handle_command(
            "set_schematic_component_property",
            {
                "schematicPath": str(sch_with_r1),
                "reference": "R1",
                "value": "RC0603FR-0710KL",
            },
        )
        assert result["success"] is False

    def test_set_missing_value_fails(self, sch_with_r1: Any) -> None:
        iface = self._iface()
        result = iface.handle_command(
            "set_schematic_component_property",
            {
                "schematicPath": str(sch_with_r1),
                "reference": "R1",
                "name": "MPN",
            },
        )
        assert result["success"] is False

    def test_set_can_modify_built_in_value_field(self, sch_with_r1: Any) -> None:
        """Built-in fields can be re-targeted via set_..._property too."""
        iface = self._iface()
        result = iface.handle_command(
            "set_schematic_component_property",
            {
                "schematicPath": str(sch_with_r1),
                "reference": "R1",
                "name": "Value",
                "value": "22k",
            },
        )
        assert result["success"] is True
        get_result = iface.handle_command(
            "get_schematic_component",
            {"schematicPath": str(sch_with_r1), "reference": "R1"},
        )
        assert get_result["fields"]["Value"]["value"] == "22k"


@pytest.mark.integration
class TestRemoveSchematicComponentProperty:
    """Tests for the convenience `remove_schematic_component_property` tool."""

    @pytest.fixture
    def sch_with_r1(self, tmp_path: Any) -> Any:
        return _make_test_schematic(tmp_path, PLACED_RESISTOR_BLOCK)

    def _iface(self) -> Any:
        from kicad_interface import KiCADInterface

        return KiCADInterface()

    def test_remove_existing_custom_property(self, sch_with_r1: Any) -> None:
        iface = self._iface()
        iface.handle_command(
            "set_schematic_component_property",
            {
                "schematicPath": str(sch_with_r1),
                "reference": "R1",
                "name": "MPN",
                "value": "RC0603FR-0710KL",
            },
        )
        result = iface.handle_command(
            "remove_schematic_component_property",
            {
                "schematicPath": str(sch_with_r1),
                "reference": "R1",
                "name": "MPN",
            },
        )
        assert result["success"] is True
        assert "MPN" in result["updated"]["propertiesRemoved"]

        get_result = iface.handle_command(
            "get_schematic_component",
            {"schematicPath": str(sch_with_r1), "reference": "R1"},
        )
        assert "MPN" not in get_result["fields"]

    def test_remove_built_in_field_rejected(self, sch_with_r1: Any) -> None:
        iface = self._iface()
        result = iface.handle_command(
            "remove_schematic_component_property",
            {
                "schematicPath": str(sch_with_r1),
                "reference": "R1",
                "name": "Reference",
            },
        )
        assert result["success"] is False

    def test_remove_missing_property_succeeds_with_no_change(self, sch_with_r1: Any) -> None:
        iface = self._iface()
        result = iface.handle_command(
            "remove_schematic_component_property",
            {
                "schematicPath": str(sch_with_r1),
                "reference": "R1",
                "name": "NeverExisted",
            },
        )
        assert result["success"] is True
        assert "propertiesRemoved" not in result["updated"]


# ---------------------------------------------------------------------------
# Regression: KiCad emits (symbol (lib_name "...") (lib_id "...") ...) for
# rescued / customised symbols. The original lookup regex required (lib_id
# IMMEDIATELY after (symbol — silently failing on these blocks. All three
# affected handlers (get / edit / set / remove) must find them.
# ---------------------------------------------------------------------------


@pytest.mark.integration
class TestLibNameBeforeLibIdOrdering:
    """Reproduces the real-world failure where R1 in a 90k-line user
    schematic was reported as 'not found' because the placed (symbol ...)
    block carried a (lib_name "...") child before (lib_id "...")."""

    @pytest.fixture
    def sch_with_libname(self, tmp_path: Any) -> Any:
        return _make_test_schematic(tmp_path, PLACED_RESISTOR_BLOCK_LIBNAME_FIRST)

    def _iface(self) -> Any:
        from kicad_interface import KiCADInterface

        return KiCADInterface()

    def test_get_finds_symbol_with_libname_before_libid(self, sch_with_libname: Any) -> None:
        iface = self._iface()
        result = iface.handle_command(
            "get_schematic_component",
            {"schematicPath": str(sch_with_libname), "reference": "R7"},
        )
        assert result["success"] is True, result.get("message")
        assert result["fields"]["Value"]["value"] == "499k"
        assert result["fields"]["Footprint"]["value"] == "MF_Passives_R0603"
        # Symbol position must come from the symbol's own (at), not a property
        assert result["position"]["x"] == pytest.approx(132.08)
        assert result["position"]["y"] == pytest.approx(44.45)

    def test_set_property_works_on_symbol_with_libname_before_libid(
        self, sch_with_libname: Any
    ) -> None:
        iface = self._iface()
        result = iface.handle_command(
            "set_schematic_component_property",
            {
                "schematicPath": str(sch_with_libname),
                "reference": "R7",
                "name": "Mfr",
                "value": "Yageo",
            },
        )
        assert result["success"] is True, result.get("message")
        assert result["updated"]["propertiesAdded"]["Mfr"] == "Yageo"

        verify = iface.handle_command(
            "get_schematic_component",
            {"schematicPath": str(sch_with_libname), "reference": "R7"},
        )
        assert verify["fields"]["Mfr"]["value"] == "Yageo"

    def test_edit_with_properties_dict_works_on_libname_first_form(
        self, sch_with_libname: Any
    ) -> None:
        iface = self._iface()
        result = iface.handle_command(
            "edit_schematic_component",
            {
                "schematicPath": str(sch_with_libname),
                "reference": "R7",
                "properties": {
                    "MPN": "RC0603FR-07499KL",
                    "Manufacturer": "Yageo",
                    "Tolerance": "1%",
                },
            },
        )
        assert result["success"] is True, result.get("message")
        assert set(result["updated"]["propertiesAdded"].keys()) == {
            "MPN",
            "Manufacturer",
            "Tolerance",
        }

    def test_remove_property_works_on_libname_first_form(self, sch_with_libname: Any) -> None:
        iface = self._iface()
        # First add a property so we have something to remove
        iface.handle_command(
            "set_schematic_component_property",
            {
                "schematicPath": str(sch_with_libname),
                "reference": "R7",
                "name": "MPN",
                "value": "RC0603FR-07499KL",
            },
        )
        result = iface.handle_command(
            "remove_schematic_component_property",
            {
                "schematicPath": str(sch_with_libname),
                "reference": "R7",
                "name": "MPN",
            },
        )
        assert result["success"] is True, result.get("message")
        assert "MPN" in result["updated"]["propertiesRemoved"]

    def test_default_property_position_matches_symbol_origin_on_libname_first_form(
        self, sch_with_libname: Any
    ) -> None:
        """Newly-added properties default to the parent symbol's (at) position.
        This used to silently fall back to (0,0) on (lib_name)-first symbols
        because the position-extraction regex required (symbol (lib_id ...) (at ...)).
        """
        iface = self._iface()
        iface.handle_command(
            "set_schematic_component_property",
            {
                "schematicPath": str(sch_with_libname),
                "reference": "R7",
                "name": "Mfr",
                "value": "Yageo",
            },
        )
        verify = iface.handle_command(
            "get_schematic_component",
            {"schematicPath": str(sch_with_libname), "reference": "R7"},
        )
        mfr = verify["fields"]["Mfr"]
        assert mfr["x"] == pytest.approx(132.08)
        assert mfr["y"] == pytest.approx(44.45)

    def test_edit_builtin_fields_works_on_libname_first_form(self, sch_with_libname: Any) -> None:
        """The pre-existing edit path (footprint / value / reference rewriting,
        unrelated to custom properties) also relied on the buggy symbol-lookup
        regex. Verify it now works on (lib_name)-first symbols too."""
        iface = self._iface()
        result = iface.handle_command(
            "edit_schematic_component",
            {
                "schematicPath": str(sch_with_libname),
                "reference": "R7",
                "value": "1k",
                "footprint": "Resistor_SMD:R_0603_1608Metric",
            },
        )
        assert result["success"] is True, result.get("message")

        verify = iface.handle_command(
            "get_schematic_component",
            {"schematicPath": str(sch_with_libname), "reference": "R7"},
        )
        assert verify["fields"]["Value"]["value"] == "1k"
        assert verify["fields"]["Footprint"]["value"] == "Resistor_SMD:R_0603_1608Metric"

    def test_delete_works_on_libname_first_form(self, sch_with_libname: Any) -> None:
        """The pre-existing delete path used the same buggy symbol-lookup
        regex and would silently report 'not found' for (lib_name)-first
        symbols. Verify it can now locate and remove them."""
        iface = self._iface()
        result = iface.handle_command(
            "delete_schematic_component",
            {"schematicPath": str(sch_with_libname), "reference": "R7"},
        )
        assert result["success"] is True, result.get("message")

        verify = iface.handle_command(
            "get_schematic_component",
            {"schematicPath": str(sch_with_libname), "reference": "R7"},
        )
        assert verify["success"] is False
        assert "not found" in verify.get("message", "").lower()
