"""Conformance: the curated ioctl registry MUST agree with the
live Linux UAPI header on this host (Part 13 §5.2.3, sub-phase 3B).

A driver's `_IO*` macros can change between kernel versions —
either the macro's number itself shifts, or the underlying
struct's sizeof drifts (which changes the `size` field of the
encoded number even when the macro literal is unchanged).

This conformance gate is the federation's currency check: for
every driver in the registry, compute the live numbers from
``/usr/include/linux/<header>.h`` via a C-compiler probe and
assert each registered number matches. A mismatch fails with
a diff that names the macros that drifted; the maintainer then
re-runs ``scripts/regenerate_ioctl_registry.py``, audits the
diff, and bumps ``last_verified``.

**Refusal to live-probe.** The gate does NOT call ``ioctl(fd, X,
NULL)`` against a real device — that's UB-adjacent for drivers
that interpret the arg unconditionally. The currency check
operates purely on the kernel HEADER, never the runtime.

**Skip conditions** (honest emptiness, not test failure):
  * No C compiler on this host.
  * Linux UAPI headers missing (no `/usr/include/linux/`).
  * A specific driver's header missing (e.g. building on a
    minimal container without all UAPI fragments).
"""

from __future__ import annotations

import json
from pathlib import Path
from typing import Any

import pytest

from tests.conformance._ioctl_compute import (
    compute_ioctl_numbers,
    has_compiler,
)

pytestmark = pytest.mark.conformance


REPO_ROOT = Path(__file__).resolve().parents[2]
REGISTRY_DIR = REPO_ROOT / "hrb-code" / "src" / "data" / "driver_interfaces"


def _registry_files() -> list[Path]:
    """All `*.json` files in the curated registry dir, sorted for
    a stable parametrize order."""
    if not REGISTRY_DIR.exists():
        return []
    return sorted(REGISTRY_DIR.glob("*.json"))


_REG_FILES = _registry_files()


@pytest.mark.skipif(not has_compiler(), reason="cc/gcc not on PATH")
@pytest.mark.skipif(
    not Path("/usr/include/linux").is_dir(),
    reason="Linux UAPI headers not installed",
)
@pytest.mark.parametrize(
    "registry_file",
    _REG_FILES,
    ids=[f.stem for f in _REG_FILES] or ["no_registry"],
)
def test_registry_matches_live_header(registry_file: Path) -> None:
    """Every ioctl number in `<driver>.json` MUST equal the value
    the live header expands to. Failure prints which macros
    drifted; the fix is ``scripts/regenerate_ioctl_registry.py``."""
    record: dict[str, Any] = json.loads(registry_file.read_text())
    header_with_brackets = str(record["header"])
    # `"<linux/foo.h>"` → `"linux/foo.h"` for the include directive.
    header = header_with_brackets.strip("<>")
    header_path = Path("/usr/include") / header
    if not header_path.exists():
        pytest.skip(f"header {header_path} not installed")

    # Build the (display, expr) macro list. Variable-size entries
    # are invoked as `NAME(0)` so we get the size-0 base value to
    # compare against the registry's `base_number` field. Fixed
    # entries pass through as bare names.
    macros: list[tuple[str, str]] = []
    for io in record["ioctls"]:
        name = str(io["name"])
        if io.get("variable_size"):
            macros.append((name, f"{name}(0)"))
        else:
            macros.append((name, name))
    if not macros:
        pytest.skip(f"{registry_file.stem} has no ioctls")
    live = compute_ioctl_numbers(header, macros)
    assert live is not None  # has_compiler skipped earlier

    mismatches: list[str] = []
    for io in record["ioctls"]:
        name = str(io["name"])
        is_variable = bool(io.get("variable_size", False))
        # Fixed: number field. Variable: base_number field.
        registered_str = str(io["base_number"] if is_variable else io["number"])
        registered = int(registered_str, 16)
        live_val = live.get(name)
        if live_val is None:
            mismatches.append(f"{name}: live header omits this macro (rename or removal?)")
        elif live_val != registered:
            label = "base_number" if is_variable else "number"
            mismatches.append(f"{name}: registered {label}={registered_str} live=0x{live_val:08x}")

    if mismatches:
        msg = (
            f"{registry_file.name} drifted from live header "
            f"<{header}>; run scripts/regenerate_ioctl_registry.py + "
            f"audit + bump last_verified:\n  " + "\n  ".join(mismatches)
        )
        pytest.fail(msg)


def test_registry_header_field_is_well_formed() -> None:
    """Each registry's `header` MUST be in `<…>` form (matches the
    spec example). Catches a maintainer typo before it reaches the
    C-compiler step where the error message is less obvious."""
    if not _REG_FILES:
        pytest.skip("no registry files yet")
    for f in _REG_FILES:
        record = json.loads(f.read_text())
        h = record["header"]
        assert h.startswith("<") and h.endswith(">"), (
            f"{f.name}: header={h!r} should be wrapped in <…>"
        )


def test_registry_last_verified_is_iso_date() -> None:
    """`last_verified` MUST parse as ISO date so consumers (and
    future doctor checks) can compare it. Catches a maintainer
    pasting a human-formatted date by mistake."""
    if not _REG_FILES:
        pytest.skip("no registry files yet")
    import datetime

    for f in _REG_FILES:
        record = json.loads(f.read_text())
        lv = record["last_verified"]
        datetime.date.fromisoformat(lv)  # raises on bad shape


def test_registry_ioctl_direction_is_closed_grammar() -> None:
    """`direction` MUST be one of: r, w, rw, none. A typo or new
    direction value would break the SDK's typed projection
    silently."""
    if not _REG_FILES:
        pytest.skip("no registry files yet")
    allowed = {"r", "w", "rw", "none"}
    for f in _REG_FILES:
        record = json.loads(f.read_text())
        for io in record["ioctls"]:
            d = io["direction"]
            assert d in allowed, f"{f.name} ioctl {io['name']!r}: direction={d!r} not in {allowed}"
