from __future__ import annotations

import json
import plistlib
from pathlib import Path

import pytest
from typer.testing import CliRunner

from k1link.device_plugins.xgrids_k1.cli import app
from k1link.device_plugins.xgrids_k1.usb.snapshot import (
    DISKUTIL_COMMAND,
    SERIAL_IOREG_COMMAND,
    USB_IOREG_COMMAND,
    CommandOutput,
    snapshot,
)

runner = CliRunner()


def _plist_output(argv: tuple[str, ...], payload: object) -> CommandOutput:
    return CommandOutput(
        argv=argv,
        returncode=0,
        stdout=plistlib.dumps(payload),
        stderr=b"",
        error=None,
    )


def _interface(
    name: str,
    number: int,
    interface_class: int,
    subclass: int,
    protocol: int,
) -> dict[str, object]:
    return {
        "IOObjectClass": "IOUSBHostInterface",
        "IORegistryEntryName": name,
        "kUSBString": name,
        "bInterfaceNumber": number,
        "bInterfaceClass": interface_class,
        "bInterfaceSubClass": subclass,
        "bInterfaceProtocol": protocol,
        "bAlternateSetting": 0,
        "bConfigurationValue": 1,
        "bNumEndpoints": 2,
    }


def test_snapshot_parses_xgrids_interfaces_storage_and_usbmodem() -> None:
    usb_plist = [
        {
            "IOObjectClass": "IOUSBHostDevice",
            "IORegistryEntryName": "XGRIDS Device",
            "USB Product Name": "XGRIDS Device",
            "USB Vendor Name": "rockchip",
            "USB Serial Number": "synthetic-serial",
            "idVendor": 0x2207,
            "idProduct": 0x0019,
            "bDeviceClass": 0xEF,
            "bDeviceSubClass": 0x02,
            "bDeviceProtocol": 0x01,
            "bcdUSB": 0x0210,
            "bcdDevice": 0x0310,
            "USBSpeed": 3,
            "UsbLinkSpeed": 480_000_000,
            "USB Address": 1,
            "locationID": 0x01100000,
            "IORegistryEntryID": 12345,
            "IORegistryEntryChildren": [
                _interface("RNDIS Communications Control", 0, 0xE0, 0x01, 0x03),
                _interface("RNDIS Ethernet Data", 1, 0x0A, 0x00, 0x00),
                _interface("Mass Storage", 2, 0x08, 0x06, 0x50),
                _interface("CDC NCM Control", 3, 0x02, 0x0D, 0x00),
                _interface("CDC ACM Serial", 4, 0x02, 0x02, 0x01),
                {
                    "IOObjectClass": "IOMedia",
                    "IORegistryEntryName": "Synthetic Media",
                    "BSD Name": "disk4",
                },
            ],
        },
        {
            "IOObjectClass": "IOUSBHostDevice",
            "IORegistryEntryName": "Unrelated Camera",
            "USB Product Name": "Unrelated Camera",
            "idVendor": 999,
        },
    ]
    serial_plist = [
        {
            "IOCalloutDevice": "/dev/cu.usbmodemK1TEST",
            "IODialinDevice": "/dev/tty.usbmodemK1TEST",
            "IOTTYBaseName": "usbmodemK1TEST",
            "IOSerialBSDClientType": "IOSerialStream",
        },
        {
            "IOCalloutDevice": "/dev/cu.debug-console",
            "IODialinDevice": "/dev/tty.debug-console",
        },
    ]
    storage_plist = {
        "AllDisks": ["disk4", "disk4s1"],
        "WholeDisks": ["disk4"],
        "VolumesFromDisks": ["K1_DATA"],
        "AllDisksAndPartitions": [
            {
                "DeviceIdentifier": "disk4",
                "Content": "GUID_partition_scheme",
                "Size": 64_000_000,
                "OSInternal": False,
                "Partitions": [
                    {
                        "DeviceIdentifier": "disk4s1",
                        "Content": "Microsoft Basic Data",
                        "Size": 63_000_000,
                        "VolumeName": "K1_DATA",
                        "MountPoint": "/Volumes/K1_DATA",
                        "OSInternal": False,
                    }
                ],
            }
        ],
    }
    outputs = {
        USB_IOREG_COMMAND: _plist_output(USB_IOREG_COMMAND, usb_plist),
        SERIAL_IOREG_COMMAND: _plist_output(SERIAL_IOREG_COMMAND, serial_plist),
        DISKUTIL_COMMAND: _plist_output(DISKUTIL_COMMAND, storage_plist),
    }

    result = snapshot(lambda argv: outputs[tuple(argv)])

    assert result["xgrids_device_count"] == 1
    device = result["xgrids_devices"][0]
    assert device["vendor_id_hex"] == "0x2207"
    assert device["product_id_hex"] == "0x0019"
    assert device["bsd_names"] == ["disk4"]
    assert device["interface_capabilities"] == [
        "cdc_data",
        "mass_storage",
        "ncm",
        "rndis",
        "serial",
    ]
    assert [interface["interface_number"] for interface in device["interfaces"]] == [
        0,
        1,
        2,
        3,
        4,
    ]
    assert all(entry["xgrids_related"] for entry in result["external_storage"]["entries"])
    assert result["usbmodem_device_names"] == [
        "/dev/cu.usbmodemK1TEST",
        "/dev/tty.usbmodemK1TEST",
    ]
    assert len(result["usbmodem_devices"]) == 1
    assert all(source["ok"] for source in result["sources"])
    assert all("sudo" not in source["argv"] for source in result["sources"])
    assert result["safety"] == {
        "metadata_only": True,
        "sudo_used": False,
        "device_file_contents_read": False,
        "device_writes_performed": False,
    }


def test_snapshot_reports_command_and_plist_errors_without_raising() -> None:
    outputs = {
        USB_IOREG_COMMAND: CommandOutput(
            argv=USB_IOREG_COMMAND,
            returncode=0,
            stdout=b"not a plist",
            stderr=b"",
            error=None,
        ),
        SERIAL_IOREG_COMMAND: CommandOutput(
            argv=SERIAL_IOREG_COMMAND,
            returncode=1,
            stdout=b"",
            stderr=b"serial registry unavailable",
            error=None,
        ),
        DISKUTIL_COMMAND: _plist_output(
            DISKUTIL_COMMAND,
            {
                "AllDisks": [],
                "WholeDisks": [],
                "VolumesFromDisks": [],
                "AllDisksAndPartitions": [],
            },
        ),
    }

    result = snapshot(lambda argv: outputs[tuple(argv)])

    assert result["xgrids_devices"] == []
    statuses = {source["name"]: source for source in result["sources"]}
    assert statuses["usb_ioreg"]["ok"] is False
    assert statuses["usb_ioreg"]["error"].startswith("invalid plist:")
    assert statuses["serial_ioreg"]["ok"] is False
    assert statuses["serial_ioreg"]["error"] == "serial registry unavailable"
    assert statuses["external_disks"]["ok"] is True


def test_snapshot_treats_empty_ioreg_output_as_no_devices() -> None:
    outputs = {
        USB_IOREG_COMMAND: CommandOutput(USB_IOREG_COMMAND, 0, b"", b"", None),
        SERIAL_IOREG_COMMAND: CommandOutput(SERIAL_IOREG_COMMAND, 0, b"", b"", None),
        DISKUTIL_COMMAND: _plist_output(
            DISKUTIL_COMMAND,
            {
                "AllDisks": [],
                "WholeDisks": [],
                "VolumesFromDisks": [],
                "AllDisksAndPartitions": [],
            },
        ),
    }

    result = snapshot(lambda argv: outputs[tuple(argv)])

    assert result["xgrids_device_count"] == 0
    assert all(source["ok"] for source in result["sources"])


def test_usb_snapshot_cli_writes_json(
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Path,
) -> None:
    output = tmp_path / "nested" / "usb-snapshot.json"
    payload = {
        "schema_version": 1,
        "xgrids_device_count": 0,
        "safety": {
            "metadata_only": True,
            "sudo_used": False,
            "device_file_contents_read": False,
            "device_writes_performed": False,
        },
    }
    monkeypatch.setattr("k1link.device_plugins.xgrids_k1.cli.usb_snapshot", lambda: payload)

    result = runner.invoke(app, ["usb", "snapshot", "--out", str(output)])

    assert result.exit_code == 0
    assert json.loads(output.read_text(encoding="utf-8")) == payload
    assert "no sudo" in result.stdout
