"""Tests for backend metadata added by KiCADInterface.handle_command."""

import sys
import types
from pathlib import Path

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


def _make_iface(command_routes, use_ipc=False):
    from kicad_interface import KiCADInterface

    iface = KiCADInterface.__new__(KiCADInterface)
    iface.use_ipc = use_ipc
    iface.ipc_backend = None
    iface.ipc_board_api = None
    iface.board = None
    iface.command_routes = command_routes
    iface._board_disk_signature = None
    iface._current_project_path = None
    iface._last_auto_save_status = None
    return iface


class _FakeIPCBoardAPI:
    def get_size(self):
        return {"width": 10, "height": 20, "unit": "mm"}

    def list_components(self):
        return []

    def get_tracks(self):
        return [
            {
                "id": "track-1",
                "start": {"x": 0, "y": 0},
                "end": {"x": 3, "y": 4},
                "width": 0.25,
                "layer": "BL_F_Cu",
                "net": "N$1",
            }
        ]

    def get_vias(self):
        return []

    def get_nets(self):
        return [{"name": "N$1", "code": 1}]

    def get_enabled_layers(self):
        return ["F.Cu", "B.Cu"]


class _FakeBoard:
    def __init__(self, filename):
        self._filename = str(filename)

    def GetFileName(self):
        return self._filename

    def GetDesignSettings(self):
        return object()

    def GetBoardEdgesBoundingBox(self):
        return object()


class _FakeIPCBackend:
    def __init__(self):
        self.connected = False

    def connect(self):
        self.connected = True
        return True

    def is_connected(self):
        return self.connected

    def get_board(self):
        return _FakeIPCBoardAPI()

    def get_version(self):
        return "9.0-test"


class _ConnectShouldNotBeCalledIPCBackend(_FakeIPCBackend):
    def connect(self):
        raise AssertionError("IPC reconnect should not be attempted")


class _FailingConnectIPCBackend(_FakeIPCBackend):
    def connect(self):
        raise RuntimeError("IPC unavailable")


class _NoBoardIPCBackend(_FakeIPCBackend):
    def get_board(self):
        raise RuntimeError("No board open")


class _FilteringIPCBoardAPI(_FakeIPCBoardAPI):
    def get_tracks(self):
        return [
            {
                "id": "track-1",
                "start": {"x": 0, "y": 0},
                "end": {"x": 3, "y": 4},
                "width": 0.25,
                "layer": "BL_F_Cu",
                "net": "N$1",
                "netCode": 1,
            },
            {
                "id": "track-2",
                "start": {"x": 10, "y": 10},
                "end": {"x": 11, "y": 11},
                "width": 0.2,
                "layer": "BL_B_Cu",
                "net": "N$2",
                "netCode": 2,
            },
        ]

    def get_vias(self):
        return [
            {
                "id": "via-1",
                "position": {"x": 0.5, "y": 0.5},
                "diameter": 0.8,
                "drill": 0.4,
                "net": "N$1",
                "netCode": 1,
            },
            {
                "id": "via-2",
                "position": {"x": 20, "y": 20},
                "diameter": 0.8,
                "drill": 0.4,
                "net": "N$2",
                "netCode": 2,
            },
        ]


class _Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y


class _BoxWithPosSize:
    def __init__(self, x, y, width, height):
        self.pos = _Point(x, y)
        self.size = _Point(width, height)


class _BoxWithMinMax:
    def __init__(self, min_x, min_y, max_x, max_y):
        self.min = _Point(min_x, min_y)
        self.max = _Point(max_x, max_y)


class _BoundingBoxBoard:
    def __init__(self, boxes):
        self._boxes = boxes

    def get_shapes(self):
        return list(range(len(self._boxes)))

    def get_item_bounding_box(self, shape):
        return self._boxes[shape]


def _stub_kipy_units(monkeypatch):
    units_module = types.ModuleType("kipy.util.units")
    units_module.to_mm = lambda nm: nm / 1_000_000
    monkeypatch.setitem(sys.modules, "kipy", types.ModuleType("kipy"))
    monkeypatch.setitem(sys.modules, "kipy.util", types.ModuleType("kipy.util"))
    monkeypatch.setitem(sys.modules, "kipy.util.units", units_module)


def test_generic_command_is_tagged_as_swig():
    iface = _make_iface(
        {
            "get_project_info": lambda params: {
                "success": True,
                "project": {"name": "demo"},
            }
        },
        use_ipc=True,
    )

    result = iface.handle_command("get_project_info", {})

    assert result["_backend"] == "swig"
    assert result["_realtime"] is False


def test_explicit_ipc_command_is_tagged_as_ipc_when_ipc_is_active():
    iface = _make_iface(
        {
            "ipc_add_track": lambda params: {
                "success": True,
                "message": "Track added",
                "realtime": True,
            }
        },
        use_ipc=True,
    )

    result = iface.handle_command("ipc_add_track", {})

    assert result["_backend"] == "ipc"
    assert result["_realtime"] is True


def test_backend_info_uses_reported_backend_for_metadata():
    iface = _make_iface(
        {
            "get_backend_info": lambda params: {
                "success": True,
                "backend": "ipc",
                "realtime_sync": True,
            }
        },
        use_ipc=True,
    )

    result = iface.handle_command("get_backend_info", {})

    assert result["_backend"] == "ipc"
    assert result["_realtime"] is True


def test_backend_state_without_board_reports_loaded_flags(monkeypatch):
    """Backend state must explicitly say when no board/project is loaded."""
    import kicad_interface

    monkeypatch.setattr(kicad_interface.KiCADProcessManager, "is_running", lambda: False)

    iface = _make_iface({}, use_ipc=False)
    iface.command_routes["get_backend_state"] = iface._handle_get_backend_state

    result = iface.handle_command("get_backend_state", {})

    assert result["success"] is True
    assert result["backend"] == "swig"
    assert result["realtime"] is False
    assert result["loadedProject"] is False
    assert result["loadedBoard"] is False
    assert result["projectPath"] is None
    assert result["boardPath"] is None
    assert result["dirty"] is False
    assert result["_backend"] == "swig"
    assert result["_realtime"] is False


def test_backend_state_reports_loaded_project_board_and_clean_signature(tmp_path, monkeypatch):
    """A loaded board with a matching disk signature should be visible and clean."""
    import kicad_interface

    monkeypatch.setattr(kicad_interface.KiCADProcessManager, "is_running", lambda: False)

    board_path = tmp_path / "demo.kicad_pcb"
    project_path = tmp_path / "demo.kicad_pro"
    board_path.write_text("(kicad_pcb demo)\n", encoding="utf-8")
    project_path.write_text("{}\n", encoding="utf-8")

    iface = _make_iface({}, use_ipc=False)
    iface.board = _FakeBoard(board_path)
    iface._current_project_path = tmp_path
    iface._record_board_signature()
    iface.command_routes["get_backend_state"] = iface._handle_get_backend_state

    result = iface.handle_command("get_backend_state", {})

    assert result["loadedProject"] is True
    assert result["loadedBoard"] is True
    assert result["projectPath"] == str(project_path.resolve())
    assert result["boardPath"] == str(board_path.resolve())
    assert result["dirty"] is False
    assert result["diskChangedExternally"] is False


def test_backend_state_reports_disk_divergence_as_dirty(tmp_path, monkeypatch):
    """If the board file changed after load, callers need a loud dirty signal."""
    import kicad_interface

    monkeypatch.setattr(kicad_interface.KiCADProcessManager, "is_running", lambda: False)

    board_path = tmp_path / "demo.kicad_pcb"
    board_path.write_text("(kicad_pcb original)\n", encoding="utf-8")

    iface = _make_iface({}, use_ipc=False)
    iface.board = _FakeBoard(board_path)
    iface._record_board_signature()
    board_path.write_text("(kicad_pcb changed)\n", encoding="utf-8")
    iface.command_routes["get_backend_state"] = iface._handle_get_backend_state

    result = iface.handle_command("get_backend_state", {})

    assert result["loadedBoard"] is True
    assert result["dirty"] is True
    assert result["diskChangedExternally"] is True
    assert "changed on disk" in result["dirtyReason"]


def test_backend_state_reports_unsaved_memory_after_refused_autosave(tmp_path, monkeypatch):
    """Auto-save refusal means the MCP has memory changes that are not persisted."""
    import kicad_interface

    monkeypatch.setattr(kicad_interface.KiCADProcessManager, "is_running", lambda: False)

    board_path = tmp_path / "demo.kicad_pcb"
    board_path.write_text("(kicad_pcb demo)\n", encoding="utf-8")

    iface = _make_iface({}, use_ipc=False)
    iface.board = _FakeBoard(board_path)
    iface._record_board_signature()
    iface._last_auto_save_status = {
        "saved": False,
        "memChangesUnsaved": True,
        "diskChangedExternally": True,
    }
    iface.command_routes["get_backend_state"] = iface._handle_get_backend_state

    result = iface.handle_command("get_backend_state", {})

    assert result["dirty"] is True
    assert result["diskChangedExternally"] is True
    assert "memory changes" in result["dirtyReason"]


def test_backend_state_reports_ipc_connection_without_loaded_board(monkeypatch):
    """Backend state separates IPC connectivity from whether a board is loaded."""
    import kicad_interface

    monkeypatch.setattr(kicad_interface.KiCADProcessManager, "is_running", lambda: False)

    backend = _FakeIPCBackend()
    backend.connected = True
    iface = _make_iface({}, use_ipc=True)
    iface.ipc_backend = backend
    iface.command_routes["get_backend_state"] = iface._handle_get_backend_state

    result = iface.handle_command("get_backend_state", {})

    assert result["backend"] == "ipc"
    assert result["realtime"] is True
    assert result["ipcConnected"] is True
    assert result["loadedBoard"] is False
    assert result["_backend"] == "ipc"
    assert result["_realtime"] is True


def test_ipc_capable_command_reconnects_when_kicad_is_running(monkeypatch):
    import kicad_interface

    monkeypatch.setattr(kicad_interface, "KICAD_BACKEND", "auto")
    monkeypatch.setattr(kicad_interface.KiCADProcessManager, "is_running", lambda: True)
    monkeypatch.setitem(
        sys.modules,
        "kicad_api.ipc_backend",
        types.SimpleNamespace(IPCBackend=_FakeIPCBackend),
    )

    iface = _make_iface({}, use_ipc=False)

    result = iface.handle_command("get_board_info", {})

    assert result["success"] is True
    assert result["_backend"] == "ipc"
    assert result["_realtime"] is True


def test_ipc_capable_command_does_not_reconnect_in_strict_swig_mode(monkeypatch):
    import kicad_interface

    monkeypatch.setattr(kicad_interface, "KICAD_BACKEND", "swig")
    monkeypatch.setattr(kicad_interface.KiCADProcessManager, "is_running", lambda: True)
    monkeypatch.setitem(
        sys.modules,
        "kicad_api.ipc_backend",
        types.SimpleNamespace(IPCBackend=_ConnectShouldNotBeCalledIPCBackend),
    )

    iface = _make_iface(
        {
            "get_board_info": lambda params: {
                "success": True,
                "board": {"filename": "demo.kicad_pcb"},
            }
        },
        use_ipc=False,
    )

    result = iface.handle_command("get_board_info", {})

    assert result["success"] is True
    assert result["_backend"] == "swig"
    assert result["_realtime"] is False
    assert iface.ipc_board_api is None


def test_ipc_reconnect_failure_falls_back_to_swig(monkeypatch):
    import kicad_interface

    monkeypatch.setattr(kicad_interface, "KICAD_BACKEND", "auto")
    monkeypatch.setattr(kicad_interface.KiCADProcessManager, "is_running", lambda: True)
    monkeypatch.setitem(
        sys.modules,
        "kicad_api.ipc_backend",
        types.SimpleNamespace(IPCBackend=_FailingConnectIPCBackend),
    )

    iface = _make_iface(
        {
            "get_board_info": lambda params: {
                "success": True,
                "board": {"filename": "demo.kicad_pcb"},
            }
        },
        use_ipc=False,
    )

    result = iface.handle_command("get_board_info", {})

    assert result["success"] is True
    assert result["_backend"] == "swig"
    assert result["_realtime"] is False
    assert iface.use_ipc is False
    assert iface.ipc_board_api is None


def test_connected_ipc_without_board_api_reports_status_but_board_tools_fallback(monkeypatch):
    import kicad_interface

    monkeypatch.setattr(kicad_interface, "KICAD_BACKEND", "auto")
    monkeypatch.setattr(kicad_interface.KiCADProcessManager, "is_running", lambda: True)
    monkeypatch.setitem(
        sys.modules,
        "kicad_api.ipc_backend",
        types.SimpleNamespace(IPCBackend=_NoBoardIPCBackend),
    )

    iface = _make_iface(
        {
            "get_board_info": lambda params: {
                "success": True,
                "board": {"filename": "demo.kicad_pcb"},
            }
        },
        use_ipc=False,
    )
    iface.command_routes["get_backend_info"] = iface._handle_get_backend_info

    board_result = iface.handle_command("get_board_info", {})
    backend_result = iface.handle_command("get_backend_info", {})

    assert board_result["success"] is True
    assert board_result["_backend"] == "swig"
    assert board_result["_realtime"] is False
    assert iface.use_ipc is True
    assert iface.ipc_board_api is None

    assert backend_result["success"] is True
    assert backend_result["backend"] == "ipc"
    assert backend_result["realtime_sync"] is True
    assert backend_result["ipc_connected"] is True
    assert backend_result["_backend"] == "ipc"


def test_ui_status_tools_report_live_ipc_backend_status(monkeypatch):
    import kicad_interface

    monkeypatch.setattr(kicad_interface, "KICAD_BACKEND", "auto")
    monkeypatch.setattr(kicad_interface.KiCADProcessManager, "is_running", lambda self=None: True)
    monkeypatch.setattr(
        kicad_interface.KiCADProcessManager,
        "get_process_info",
        lambda self: [{"pid": "1234", "name": "pcbnew.exe", "command": "pcbnew.exe"}],
    )
    monkeypatch.setitem(
        sys.modules,
        "kicad_api.ipc_backend",
        types.SimpleNamespace(IPCBackend=_FakeIPCBackend),
    )

    iface = _make_iface({}, use_ipc=False)
    iface.ipc_backend = _FakeIPCBackend()
    iface.command_routes = {
        "check_kicad_ui": iface._handle_check_kicad_ui,
        "launch_kicad_ui": iface._handle_launch_kicad_ui,
        "get_backend_info": iface._handle_get_backend_info,
    }
    monkeypatch.setattr(
        kicad_interface,
        "check_and_launch_kicad",
        lambda path_obj, auto_launch: {
            "running": True,
            "launched": True,
            "processes": [{"pid": "1234", "name": "pcbnew.exe", "command": "pcbnew.exe"}],
            "message": "KiCAD launched successfully",
            "project": str(path_obj) if path_obj else None,
        },
    )

    for command in ("check_kicad_ui", "launch_kicad_ui", "get_backend_info"):
        result = iface.handle_command(command, {})

        assert result["success"] is True
        assert result["backend"] == "ipc"
        assert result["realtime_sync"] is True
        assert result["ipc_connected"] is True
        assert result["_backend"] == "ipc"
        assert result["_realtime"] is True


def test_query_traces_can_use_ipc_backend(monkeypatch):
    import kicad_interface

    monkeypatch.setattr(kicad_interface, "KICAD_BACKEND", "swig")

    iface = _make_iface({}, use_ipc=True)
    iface.ipc_board_api = _FakeIPCBoardAPI()

    result = iface.handle_command(
        "query_traces",
        {"layer": "F.Cu", "boundingBox": {"x1": -1, "y1": -1, "x2": 1, "y2": 1}},
    )

    assert result["success"] is True
    assert result["traceCount"] == 1
    assert result["traces"][0]["layer"] == "F.Cu"
    assert result["traces"][0]["length"] == 5
    assert result["_backend"] == "ipc"


def test_query_traces_ipc_filters_and_vias(monkeypatch):
    import kicad_interface

    monkeypatch.setattr(kicad_interface, "KICAD_BACKEND", "swig")

    iface = _make_iface({}, use_ipc=True)
    iface.ipc_board_api = _FilteringIPCBoardAPI()

    net_miss = iface.handle_command("query_traces", {"net": "NO_MATCH"})
    layer_match = iface.handle_command("query_traces", {"layer": "B.Cu"})
    reversed_bbox_with_vias = iface.handle_command(
        "query_traces",
        {
            "net": "N$1",
            "includeVias": True,
            "boundingBox": {"x1": 1, "y1": 1, "x2": -1, "y2": -1},
        },
    )

    assert net_miss["success"] is True
    assert net_miss["traceCount"] == 0
    assert net_miss["_backend"] == "ipc"

    assert layer_match["success"] is True
    assert layer_match["traceCount"] == 1
    assert layer_match["traces"][0]["uuid"] == "track-2"
    assert layer_match["traces"][0]["layer"] == "B.Cu"

    assert reversed_bbox_with_vias["success"] is True
    assert reversed_bbox_with_vias["traceCount"] == 1
    assert reversed_bbox_with_vias["traces"][0]["uuid"] == "track-1"
    assert reversed_bbox_with_vias["viaCount"] == 1
    assert reversed_bbox_with_vias["vias"][0]["uuid"] == "via-1"


def test_ipc_board_size_supports_kicad_10_box2_pos_size(monkeypatch):
    from kicad_api.ipc_backend import IPCBoardAPI

    _stub_kipy_units(monkeypatch)
    board_api = IPCBoardAPI(None, lambda *_args: None)
    board_api._board = _BoundingBoxBoard(
        [
            _BoxWithPosSize(1_000_000, 2_000_000, 3_000_000, 4_000_000),
            _BoxWithPosSize(0, 1_000_000, 2_000_000, 1_000_000),
        ]
    )

    result = board_api.get_size()

    assert result == {"width": 4.0, "height": 5.0, "unit": "mm"}


def test_ipc_board_size_keeps_min_max_box2_compatibility(monkeypatch):
    from kicad_api.ipc_backend import IPCBoardAPI

    _stub_kipy_units(monkeypatch)
    board_api = IPCBoardAPI(None, lambda *_args: None)
    board_api._board = _BoundingBoxBoard(
        [
            _BoxWithMinMax(1_000_000, 2_000_000, 3_000_000, 4_000_000),
            _BoxWithMinMax(0, 1_000_000, 2_000_000, 3_000_000),
        ]
    )

    result = board_api.get_size()

    assert result == {"width": 3.0, "height": 3.0, "unit": "mm"}
