//! linux_test — exercises mgf-hrb-linux against REAL hardware enumeration.
//! These tests read this machine's actual /sys/bus/pci/devices. They prove the
//! seam abstraction holds against genuine sysfs facts, and that acquisition
//! fails HONESTLY (Unsupported) rather than lying when VFIO is absent (G2).

const std = @import("std");
const testing = std.testing;
const model = @import("mgf_hrb_model");
const kabi = @import("mgf_hrb_kabi");
const LinuxBackend = @import("mgf_hrb_linux_backend").LinuxBackend;

const Urn = model.Urn;
const Err = model.Err;

const Collector = struct {
    items: std.ArrayListUnmanaged(model.Resource) = .empty,
    alloc: std.mem.Allocator,
    fn push(ctx: *anyopaque, r: model.Resource) void {
        const self: *Collector = @ptrCast(@alignCast(ctx));
        self.items.append(self.alloc, r) catch {};
    }
    fn sink(self: *Collector) kabi.FactSink {
        return .{ .ctx = self, .push = push };
    }
};

test "linux REAL: enumerate finds this machine's actual PCI devices" {
    var lb = LinuxBackend.init(testing.allocator);
    const be = lb.backend();

    var col = Collector{ .alloc = testing.allocator };
    defer col.items.deinit(testing.allocator);
    const s = col.sink();
    try be.enumerate("hw:/pci/**", &s, kabi.Deadline.none);

    // This machine has real PCI devices in sysfs. If the test environment has
    // any PCI bus at all, we must find at least one — proving we read genuine
    // hardware through the same seam the fake backend implements.
    std.debug.print("\n  [linux] enumerated {d} real PCI device(s)\n", .{col.items.items.len});
    if (col.items.items.len > 0) {
        const first = col.items.items[0];
        try testing.expect(first.family == .claimable);
        try testing.expect(first.kind == .pcie_function);
        // the URN is well-formed: starts with the PCI prefix
        try testing.expect(std.mem.startsWith(u8, first.urn.slice(), "hw:/pci/"));
        // and we read it live from hardware (fidelity tag, G2/Part 2.8)
        try testing.expect(first.fidelity == .live or first.fidelity == .unknown);
        std.debug.print("  [linux] first device urn: {s} (fidelity: {s})\n", .{
            first.urn.slice(), @tagName(first.fidelity),
        });
    }
    // If there's no PCI bus (unusual), enumerate returns empty — not an error.
}

test "linux REAL: scope glob filters enumeration (USB scope → none on a PCI-only box)" {
    var lb = LinuxBackend.init(testing.allocator);
    const be = lb.backend();
    var col = Collector{ .alloc = testing.allocator };
    defer col.items.deinit(testing.allocator);
    const s = col.sink();
    // ask for USB only; this sandbox has no USB sysfs → empty, cleanly
    try be.enumerate("hw:/usb/**", &s, kabi.Deadline.none);
    try testing.expectEqual(@as(usize, 0), col.items.items.len);
}

test "linux HONEST: acquire returns Unsupported (not a lie) when VFIO is absent" {
    var lb = LinuxBackend.init(testing.allocator);
    const be = lb.backend();
    // In this sandbox /dev/vfio is absent. The contract (Part 6.3) demands the
    // backend NEVER falsely claim success. It must return Unsupported — an
    // honest "I cannot do this here" — leaving nothing acquired (G2/G3).
    const urn = Urn.from("hw:/pci/0000:00:00.0/fn0");
    if (!lb.vfio_available) {
        try testing.expectError(Err.Unsupported, be.acquire(&urn, .exclusive, kabi.Deadline.none));
        std.debug.print("  [linux] acquire honestly returned Unsupported (no /dev/vfio)\n", .{});
    } else {
        // If a real VFIO substrate existed, this path would attempt a true bind.
        std.debug.print("  [linux] VFIO present — real acquire path would run\n", .{});
    }
}

test "linux #7/D9: a DMA-capable claim on a device with no IOMMU group is REFUSED" {
    var lb = LinuxBackend.init(testing.allocator);
    const be = lb.backend();
    // A synthetic PCI BDF with no `iommu_group` symlink → the device can't be
    // IOMMU-constrained → a DMA-capable grant would be structurally unsafe (D9).
    const urn = Urn.from("hw:/pci/dead:ff:1f.7/fn0");
    if (lb.vfio_available) {
        // The broker refuses to SILENTLY grant DMA-capable access it can't make
        // safe — it declares the degraded posture by saying no (#7 / LINUX-01).
        try testing.expectError(Err.NotPermitted, be.acquire(&urn, .exclusive, kabi.Deadline.none));
        try testing.expectError(Err.NotPermitted, be.acquire(&urn, .shared_rw, kabi.Deadline.none));
        // `observe` is read-only (no DMA) → exempt from the IOMMU gate; it reaches
        // the (gated) bind path and returns Unsupported, NOT the refusal.
        try testing.expectError(Err.Unsupported, be.acquire(&urn, .observe, kabi.Deadline.none));
    } else {
        // No VFIO substrate → honest Unsupported before the IOMMU check is reached.
        try testing.expectError(Err.Unsupported, be.acquire(&urn, .exclusive, kabi.Deadline.none));
    }
}

test "linux HONEST: reset returns Unsupported when it cannot achieve safe state" {
    var lb = LinuxBackend.init(testing.allocator);
    const be = lb.backend();
    const urn = Urn.from("hw:/pci/0000:00:00.0/fn0");
    if (!lb.vfio_available) {
        // never claim a reset succeeded when it didn't (G2)
        try testing.expectError(Err.Unsupported, be.reset(&urn, .function_level, kabi.Deadline.none));
    }
}

test "linux: the SAME conformance contract shape applies (substitutability/LSP)" {
    // This test demonstrates that LinuxBackend plugs into the exact same
    // KernelBackend interface the fake implements — the core would not know the
    // difference (LSP). We only assert the discovery half here, since the
    // sandbox can't exercise real acquisition; the full conformance suite runs
    // against the fake (which CAN exercise acquire), and would run against this
    // backend unchanged on a host with /dev/vfio.
    var lb = LinuxBackend.init(testing.allocator);
    const be: kabi.KernelBackend = lb.backend(); // <- same type as fake's
    var col = Collector{ .alloc = testing.allocator };
    defer col.items.deinit(testing.allocator);
    const s = col.sink();
    try be.queryTopology(&s, kabi.Deadline.none); // works through the identical abstract seam
    std.debug.print("  [linux] queryTopology via the same seam → {d} device(s)\n", .{col.items.items.len});
}

// ===========================================================================
// #7 / R5 slice 3 — PCI VFIO lifecycle: fd-delivery + revoke-below-the-fd.
//
// On this host no real bind succeeds (no root), so we exercise the
// post-bind lifecycle by injecting a synthetic bound device and driving the
// VFIO + driver seams with fakes (the same DIP the rest of the backend uses).
// This pins the logic that's new in slice 3 — `fd_for_token`, `revoke`'s
// find-by-URN + total teardown, and idempotency — host-independently.
// ===========================================================================

const SeamFake = struct {
    closed: [8]i32 = [_]i32{-1} ** 8,
    closed_n: usize = 0,
    writes: usize = 0,

    fn openPath(_: *anyopaque, _: [:0]const u8) i64 {
        return -1; // never used here (no bind path)
    }
    fn ioctl(_: *anyopaque, _: i32, _: u32, _: usize) i64 {
        return 0;
    }
    fn closeFd(ctx: *anyopaque, fd: i32) void {
        const self: *SeamFake = @ptrCast(@alignCast(ctx));
        if (self.closed_n < self.closed.len) {
            self.closed[self.closed_n] = fd;
            self.closed_n += 1;
        }
    }
    fn currentDriver(_: *anyopaque, _: [:0]const u8, _: []u8) ?[]const u8 {
        return null; // device already off its host driver
    }
    fn writeAttr(ctx: *anyopaque, _: [:0]const u8, _: []const u8) bool {
        const self: *SeamFake = @ptrCast(@alignCast(ctx));
        self.writes += 1;
        return true;
    }
    fn wasClosed(self: *SeamFake, fd: i32) bool {
        for (self.closed[0..self.closed_n]) |c| if (c == fd) return true;
        return false;
    }
};

test "linux #7 slice 3: fd_for_token delivers the bound device fd; revoke tears it down + is idempotent" {
    var lb = LinuxBackend.init(testing.allocator);
    var fake = SeamFake{};
    lb.vfio_ops = .{ .ctx = &fake, .openPath = SeamFake.openPath, .ioctl = SeamFake.ioctl, .closeFd = SeamFake.closeFd };
    lb.driver_ops = .{ .ctx = &fake, .currentDriver = SeamFake.currentDriver, .writeAttr = SeamFake.writeAttr };
    const be = lb.backend();
    defer lb.deinit();

    // Inject a synthetic bind for hw:/pci/0000:01:00.0/fn0 (container=10, group=11,
    // device=12), bypassing the root-gated real bind path.
    try lb.bound.append(lb.alloc, .{
        .id = 42,
        .dev = .{ .container_fd = 10, .group_fd = 11, .device_fd = 12, .group = 7 },
        .detach = .{},
        .bdf_len = 12,
    });
    const bdf: []const u8 = "0000:01:00.0";
    @memcpy(lb.bound.items[0].bdf_buf[0..bdf.len], bdf);

    // fd_for_token hands out the device fd; an unknown token is -1.
    try testing.expectEqual(@as(i32, 12), be.fdForToken(.{ .id = 42 }));
    try testing.expectEqual(@as(i32, -1), be.fdForToken(.{ .id = 99 }));

    // revoke tears the VFIO stack down (device→group→container fd all closed),
    // reattaches the host driver, and drops the entry — the client's dup'd fd
    // now references a dead kernel object (revoke below the fd, §3.4).
    const urn = Urn.from("hw:/pci/0000:01:00.0/fn0");
    try be.revoke(&urn, kabi.Deadline.none);
    try testing.expect(fake.wasClosed(12) and fake.wasClosed(11) and fake.wasClosed(10));
    try testing.expect(fake.writes >= 1); // host-driver reattach attempted
    try testing.expectEqual(@as(usize, 0), lb.bound.items.len); // entry removed
    try testing.expectEqual(@as(i32, -1), be.fdForToken(.{ .id = 42 })); // gone

    // revoke again on the same (now-unbound) URN is a clean idempotent no-op.
    try be.revoke(&urn, kabi.Deadline.none);
    try testing.expectEqual(@as(usize, 3), fake.closed_n); // no further closes
}
