"""Contract: the broker's ``driver_info`` wire op (Part 13 §5.2.2).

The wire contract pinned here:

  * Op name is ``driver_info``. Required field: ``name`` (string).
    No other fields in v1 (cap_id will arrive when ops gain right-gates).
  * ``name`` charset: ``[A-Za-z0-9_-]`` only — defends the sysfs
    path build against traversal / NUL injection.
  * Success response carries: ``ok:true``, ``name``, ``module``,
    ``module_loaded``, ``bound_count``, ``sample_bound``,
    ``sysfs_attrs``, ``ioctls``, ``char_devices``, ``uevent_actions``,
    ``registry_version``, ``fidelity``.
  * In v1 the registry-derived fields are ALL null/empty:
    ``module=null``, ``ioctls=null``, ``char_devices=null``,
    ``uevent_actions=null``, ``registry_version=null``,
    ``sysfs_attrs=[]``. The curated registry ships in sub-phase 3B.
  * ``fidelity`` is `"inferred"` — the live half is live but the
    registry half is absent. Honest declaration: consumers know v1
    is partial.
  * **Honest emptiness:** a driver absent from the kernel returns
    the structured response with ``module_loaded=false,
    bound_count=0`` — NOT a NotFound error. Consumers learn
    "driver not here" structurally.
  * Missing / empty / out-of-charset name → BadRequest at the gate.

Sub-phase 3A of Part 13.
"""

from __future__ import annotations

import json
from pathlib import Path
from typing import TYPE_CHECKING

import pytest

from tests.contract.conftest import rt

if TYPE_CHECKING:
    import socket

    from mgf.hrb import Client

pytestmark = pytest.mark.contract


# ---------------------------------------------------------------------------
# Shape pins.
# ---------------------------------------------------------------------------


REGISTRY_FIELDS_FOR_OUT_OF_REGISTRY_NULL = {
    "module": None,
    "ioctls": None,
    "char_devices": None,
    "uevent_actions": None,
    "registry_version": None,
}


def test_success_shape_top_level_keys(raw_broker: socket.socket) -> None:
    """Success response carries every pinned key — for both in-
    and out-of-registry drivers."""
    r = rt(raw_broker, {"op": "driver_info", "name": "usbhid"})
    assert r.get("ok") is True
    required = {
        "name",
        "module",
        "module_loaded",
        "bound_count",
        "sample_bound",
        "sysfs_attrs",
        "ioctls",
        "char_devices",
        "uevent_actions",
        "registry_version",
        "fidelity",
    }
    missing = required - r.keys()
    assert not missing, f"missing fields: {missing}"


def test_out_of_registry_driver_emits_nulls(raw_broker: socket.socket) -> None:
    """A driver not in the curated registry MUST report
    null/empty for every registry-derived field — the consumer
    learns "no registry view for this driver" structurally.
    `usbhid` is a real kernel driver but not curated in v1's
    registry (USBDEVFS + HIDRAW are; per-driver-class curation
    is a maintainer policy decision)."""
    r = rt(raw_broker, {"op": "driver_info", "name": "usbhid"})
    for key, expected in REGISTRY_FIELDS_FOR_OUT_OF_REGISTRY_NULL.items():
        assert r[key] == expected, f"out-of-registry {key} should be null, got {r[key]!r}"
    assert r["sysfs_attrs"] == []


def test_out_of_registry_fidelity_is_unknown(raw_broker: socket.socket) -> None:
    """For an out-of-registry driver, fidelity MUST be `"unknown"`.
    Catches a regression where the §7.2 auto-splice overrides the
    formatter's honest declaration with `"live"`."""
    r = rt(raw_broker, {"op": "driver_info", "name": "usbhid"})
    assert r["fidelity"] == "unknown"
    raw_bytes = json.dumps(r)
    assert raw_bytes.count('"fidelity"') == 1


def test_in_registry_driver_emits_curated_fields(raw_broker: socket.socket) -> None:
    """A curated driver MUST surface its ioctl table + module +
    registry_version. v1 ships USBDEVFS + HIDRAW; this pin
    catches accidental removal."""
    r = rt(raw_broker, {"op": "driver_info", "name": "usbdevfs"})
    assert r["module"] == "usbcore"
    assert r["registry_version"]  # ISO date string, non-empty
    assert isinstance(r["ioctls"], list)
    assert len(r["ioctls"]) > 0
    assert isinstance(r["char_devices"], list)
    assert r["uevent_actions"] == ["bind", "unbind"]


def test_in_registry_fidelity_is_live(raw_broker: socket.socket) -> None:
    """In-registry drivers get `fidelity:"live"` (both halves are
    answered). Catches the splice not double-emitting fidelity."""
    r = rt(raw_broker, {"op": "driver_info", "name": "usbdevfs"})
    assert r["fidelity"] == "live"
    raw_bytes = json.dumps(r)
    assert raw_bytes.count('"fidelity"') == 1


def test_in_registry_ioctl_entry_shape(raw_broker: socket.socket) -> None:
    """Each `ioctls[*]` entry MUST carry name + arg + direction.
    Fixed-number entries additionally carry `number`; variable-
    size entries (Part 13 §5.2.3 schema extension) carry
    `base_number` + `variable_size:true` instead. Both numbers
    are canonical `"0xHHHHHHHH"` hex (32-bit ioctl encoding).
    `direction` is from the closed set r / w / rw / none."""
    r = rt(raw_broker, {"op": "driver_info", "name": "usbdevfs"})
    valid_dirs = {"r", "w", "rw", "none"}
    for io in r["ioctls"]:
        assert {"name", "arg", "direction"} <= io.keys()
        assert io["direction"] in valid_dirs
        if io.get("variable_size"):
            assert "base_number" in io
            assert io["base_number"].startswith("0x")
            assert len(io["base_number"]) == 10
        else:
            assert "number" in io
            assert io["number"].startswith("0x")
            assert len(io["number"]) == 10


def test_in_registry_hidraw_carries_variable_size_entries(
    raw_broker: socket.socket,
) -> None:
    """The HIDIOC* parameterized macros (Part 13 §5.2.3 schema
    extension) MUST be visible as `variable_size:true` entries.
    Catches a regression where the extension's wire surface
    silently drops back to fixed-only."""
    r = rt(raw_broker, {"op": "driver_info", "name": "hidraw"})
    var_entries = [io for io in r["ioctls"] if io.get("variable_size")]
    assert len(var_entries) > 0, "hidraw registry should carry variable-size HIDIOC* entries"
    for io in var_entries:
        assert "base_number" in io
        assert "number" not in io  # the two shapes are mutually exclusive


# ---------------------------------------------------------------------------
# Live data pins (run only when the relevant driver is present).
# ---------------------------------------------------------------------------


def test_live_module_loaded_reports_true_for_present_driver(
    raw_broker: socket.socket,
) -> None:
    """If /sys/module/usbcore exists on this host (it always does on
    a kernel with USB support), the broker reports module_loaded=true."""
    if not Path("/sys/module/usbcore").exists():
        pytest.skip("kernel without /sys/module/usbcore")
    r = rt(raw_broker, {"op": "driver_info", "name": "usbcore"})
    assert r["module_loaded"] is True


def test_live_bound_count_matches_sysfs(raw_broker: socket.socket) -> None:
    """For any driver that exposes /sys/bus/usb/drivers/<name>/, the
    bound_count MUST equal the count of symlink entries (i.e. all
    dirents minus the well-known control files)."""
    if not Path("/sys/bus/usb/drivers/usbhid").exists():
        pytest.skip("usbhid driver not present")
    expected = sum(
        1
        for p in Path("/sys/bus/usb/drivers/usbhid").iterdir()
        if p.name
        not in {
            "bind",
            "unbind",
            "uevent",
            "module",
            "new_id",
            "remove_id",
            "rebind",
        }
    )
    r = rt(raw_broker, {"op": "driver_info", "name": "usbhid"})
    assert r["bound_count"] == expected


def test_sample_bound_is_a_real_sysfs_name(
    raw_broker: socket.socket,
) -> None:
    """When bound_count > 0, sample_bound MUST be the basename of an
    existing entry in /sys/bus/.../drivers/<name>/. v1's USB-or-PCI
    walk means the name is bus-shaped (e.g. ``3-9:1.4`` for USB or
    ``0000:01:00.0`` for PCI)."""
    if not Path("/sys/bus/usb/drivers/usbhid").exists():
        pytest.skip("usbhid not present")
    r = rt(raw_broker, {"op": "driver_info", "name": "usbhid"})
    if r["bound_count"] == 0:
        pytest.skip("no devices bound to usbhid right now")
    sample = r["sample_bound"]
    assert sample is not None
    assert isinstance(sample, str)
    candidates = [
        Path("/sys/bus/usb/drivers/usbhid") / sample,
        Path("/sys/bus/pci/drivers/usbhid") / sample,
    ]
    assert any(p.exists() for p in candidates), (
        f"sample_bound={sample!r} not found under any known driver dir"
    )


# ---------------------------------------------------------------------------
# Honest emptiness for an absent driver.
# ---------------------------------------------------------------------------


def test_absent_driver_returns_structured_response(
    raw_broker: socket.socket,
) -> None:
    """A driver that doesn't exist on this host yields a SUCCESS
    response with module_loaded=false + bound_count=0 — NOT an
    error. Consumers learn 'not here' structurally."""
    r = rt(raw_broker, {"op": "driver_info", "name": "never_a_driver_xyz"})
    assert r.get("ok") is True
    assert r["module_loaded"] is False
    assert r["bound_count"] == 0
    assert r["sample_bound"] is None
    assert r["name"] == "never_a_driver_xyz"


# ---------------------------------------------------------------------------
# Error pins.
# ---------------------------------------------------------------------------


def test_missing_name_is_bad_request(raw_broker: socket.socket) -> None:
    r = rt(raw_broker, {"op": "driver_info"})
    assert r.get("ok") is False
    assert r.get("err") == "BadRequest"


def test_empty_name_is_bad_request(raw_broker: socket.socket) -> None:
    r = rt(raw_broker, {"op": "driver_info", "name": ""})
    assert r.get("ok") is False
    assert r.get("err") == "BadRequest"


@pytest.mark.parametrize(
    "bad_name",
    [
        "../etc/passwd",  # path traversal
        "foo/bar",  # path separator
        "foo\x00bar",  # NUL injection
        ".hidden",  # leading dot
        "name with space",  # space
        "name;rm -rf /",  # shell metacharacters
        "Ünicode",  # non-ASCII
    ],
)
def test_out_of_charset_name_rejects(
    raw_broker: socket.socket,
    bad_name: str,
) -> None:
    """Only ``[A-Za-z0-9_-]`` is allowed. Every other byte rejects
    at the parse gate."""
    r = rt(raw_broker, {"op": "driver_info", "name": bad_name})
    assert r.get("ok") is False, f"name={bad_name!r}: {r}"
    assert r.get("err") == "BadRequest"


def test_oversized_name_rejects(raw_broker: socket.socket) -> None:
    r = rt(raw_broker, {"op": "driver_info", "name": "a" * 200})
    assert r.get("ok") is False
    assert r.get("err") == "BadRequest"


# ---------------------------------------------------------------------------
# SDK projection pin.
# ---------------------------------------------------------------------------


def test_sdk_projection_matches_wire(broker: Client) -> None:
    """``Client.driver_info(name)`` returns a typed
    :class:`DriverInfo` whose fields mirror the wire response."""
    # Out-of-registry driver: nulls + fidelity="unknown"
    info = broker.driver_info("usbhid")
    assert info.name == "usbhid"
    assert isinstance(info.module_loaded, bool)
    assert isinstance(info.bound_count, int)
    assert info.fidelity == "unknown"
    assert info.module is None
    assert info.ioctls is None
    assert info.char_devices is None
    assert info.uevent_actions is None
    assert info.registry_version is None
    assert info.sysfs_attrs == ()

    # In-registry driver: registry fields populated +
    # fidelity="live". v1's USBDEVFS has 31 fixed-number
    # ioctls; the tuple is non-empty.
    ud = broker.driver_info("usbdevfs")
    assert ud.module == "usbcore"
    assert ud.fidelity == "live"
    assert ud.ioctls is not None and len(ud.ioctls) > 0
    assert ud.char_devices is not None
    assert ud.uevent_actions == ("bind", "unbind")
    assert ud.registry_version is not None
