//! pci-cap-test — QEMU fixture for the driver-side PCI library (library/device/pci).
//! The pci-caps test case boots with an extra `-device e1000e` NIC that no danos driver
//! claims; this fixture claims it and exercises the whole claimed-function surface
//! against real (emulated) hardware: header accessors, command bits, the capability
//! walk, MSI programming (the first driver-side `msi_bind` use), the MSI-X table,
//! extended capabilities, power state, and — where offered — function-level reset.
//! Every check prints `pci-cap-test: <check> ok` or `pci-cap-test: FAIL <check>`; the
//! harness asserts on the final `all checks passed` marker (test/qemu_test.py).

const std = @import("std");
const device = @import("driver");
const ipc = @import("ipc");
const time = @import("time");
const logging = @import("logging");
const mmio = @import("mmio");
const pci = @import("pci");
const pci_class = @import("pci-class");

/// QEMU's e1000e: Intel 82574L.
const intel_vendor: u16 = 0x8086;
const e1000e_device: u16 = 0x10D3;

const ethernet_class: u64 = pci_class.ClassCode.pack(.{
    .base = @intFromEnum(pci_class.BaseClass.network),
    .subclass = @intFromEnum(pci_class.network.SubClass.ethernet),
    .prog_if = 0,
});

/// `pci.Function` keeps a pointer to the descriptor, so it must outlive the stack frame
/// that found it.
var descriptor: device.DeviceDescriptor = undefined;

fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
    var line: [128]u8 = undefined;
    _ = logging.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
}

/// Print the check's verdict; the caller returns on false to stop at the first failure.
fn check(comptime name: []const u8, ok: bool) bool {
    if (ok) {
        _ = logging.write("pci-cap-test: " ++ name ++ " ok\n");
    } else {
        _ = logging.write("pci-cap-test: FAIL " ++ name ++ "\n");
    }
    return ok;
}

pub fn main() void {
    // The bus scan runs in another process; poll until the NIC shows up.
    const nic_id: u64 = found: {
        var tries: u32 = 0;
        while (tries < 150) : (tries += 1) {
            var descriptors: [64]device.DeviceDescriptor = undefined;
            const total = device.enumerate(&descriptors);
            for (descriptors[0..@min(total, descriptors.len)]) |*entry| {
                if (entry.class == @intFromEnum(device.DeviceClass.pci_device) and entry.pci_class == ethernet_class) {
                    descriptor = entry.*;
                    break :found entry.id;
                }
            }
            time.sleepMillis(100);
        }
        _ = logging.write("pci-cap-test: FAIL no ethernet function found\n");
        return;
    };
    writeLine("pci-cap-test: claiming ethernet function (device {d})\n", .{nic_id});
    if (!check("claim", device.claim(nic_id))) return;
    var function = pci.Function.map(nic_id, &descriptor) orelse {
        _ = logging.write("pci-cap-test: FAIL config-space map\n");
        return;
    };

    // Identity: the header accessors against known e1000e values.
    if (!check("vendor/device id", function.vendorId() == intel_vendor and function.deviceId() == e1000e_device)) return;
    if (!check("class code", function.classCode().pack() == ethernet_class)) return;
    if (!check("subsystem ids readable", function.subsystemVendorId() != 0xFFFF and function.subsystemId() != 0xFFFF)) return;

    // Command bits: enable, read back, quiesce, read back, re-enable.
    function.enableMemoryAndBusMaster();
    if (!check("memory+bus-master enable", function.command() & pci_class.command_memory_and_bus_master == pci_class.command_memory_and_bus_master)) return;
    function.disableBusMaster();
    if (!check("bus-master disable", function.command() & pci_class.command_bus_master == 0)) return;
    function.enableMemoryAndBusMaster();

    // The capability walk: e1000e advertises PM, MSI, PCIe, and MSI-X.
    var seen_power = false;
    var seen_msi = false;
    var seen_pci_express = false;
    var seen_msix = false;
    var walk = function.capabilities();
    while (walk.next()) |capability| {
        switch (capability.id) {
            @intFromEnum(pci_class.CapabilityId.power_management) => seen_power = true,
            @intFromEnum(pci_class.CapabilityId.msi) => seen_msi = true,
            @intFromEnum(pci_class.CapabilityId.pci_express) => seen_pci_express = true,
            @intFromEnum(pci_class.CapabilityId.msix) => seen_msix = true,
            else => {},
        }
    }
    if (!check("capability walk", seen_power and seen_msi and seen_pci_express and seen_msix)) return;
    if (!check("findCapability", function.findCapability(.msi) != null and function.findCapability(.pci_express) != null)) return;

    // MSI: bind a vector (the syscall's first driver-side use), program the capability,
    // and read the registers straight back.
    const endpoint = ipc.createIpcEndpoint() orelse {
        _ = logging.write("pci-cap-test: FAIL endpoint creation\n");
        return;
    };
    const message = device.msiBind(nic_id, endpoint) orelse {
        _ = logging.write("pci-cap-test: FAIL msi_bind\n");
        return;
    };
    if (!check("msi_bind address", message.address == 0xFEE0_0000)) return;
    if (!check("programMsi", function.programMsi(message))) return;
    const msi_cap = function.findCapability(.msi).?;
    const msi_control = mmio.readRegister(u16, msi_cap.offset + pci_class.msi.control);
    const msi_data_offset: usize = if (msi_control & pci_class.msi.control_64bit_capable != 0) pci_class.msi.data_64 else pci_class.msi.data_32;
    if (!check("msi registers read back", msi_control & pci_class.msi.control_enable != 0 and
        msi_control & pci_class.msi.control_multiple_message_enable_mask == 0 and
        mmio.readRegister(u32, msi_cap.offset + pci_class.msi.address) == @as(u32, @truncate(message.address)) and
        mmio.readRegister(u16, msi_cap.offset + msi_data_offset) == @as(u16, @truncate(message.data)))) return;
    if (!check("intx disabled with msi", function.command() & pci_class.command_interrupt_disable != 0)) return;
    function.disableMsi();
    if (!check("disableMsi", mmio.readRegister(u16, msi_cap.offset + pci_class.msi.control) & pci_class.msi.control_enable == 0)) return;

    // MSI-X: map the table, program entry 0, exercise the masks. Never enable — this
    // proves the programming surface, not delivery.
    const msix_table = function.msix() orelse {
        _ = logging.write("pci-cap-test: FAIL msix table map\n");
        return;
    };
    writeLine("pci-cap-test: msix table has {d} entries\n", .{msix_table.entry_count});
    if (!check("msix entry count", msix_table.entry_count >= 1)) return;
    if (!check("msix programEntry", msix_table.programEntry(0, message))) return;
    if (!check("msix entry reads back", mmio.readRegister(u32, msix_table.table + pci_class.msix.entry_address) == @as(u32, @truncate(message.address)) and
        mmio.readRegister(u32, msix_table.table + pci_class.msix.entry_data) == message.data and
        mmio.readRegister(u32, msix_table.table + pci_class.msix.entry_vector_control) & pci_class.msix.entry_vector_control_masked != 0)) return;
    if (!check("msix unmask entry", msix_table.unmaskEntry(0) and
        mmio.readRegister(u32, msix_table.table + pci_class.msix.entry_vector_control) & pci_class.msix.entry_vector_control_masked == 0)) return;
    if (!check("msix re-mask entry", msix_table.maskEntry(0) and
        mmio.readRegister(u32, msix_table.table + pci_class.msix.entry_vector_control) & pci_class.msix.entry_vector_control_masked != 0)) return;
    msix_table.setFunctionMask();
    if (!check("msix function mask", mmio.readRegister(u16, msix_table.capability + pci_class.msix.control) & pci_class.msix.control_function_mask != 0)) return;
    msix_table.clearFunctionMask();
    if (!check("msix function unmask", mmio.readRegister(u16, msix_table.capability + pci_class.msix.control) & pci_class.msix.control_function_mask == 0)) return;
    if (!check("msix out-of-range rejected", !msix_table.programEntry(msix_table.entry_count, message))) return;

    // Extended capabilities: the walk must terminate cleanly; the count is informative
    // (don't hard-bind to QEMU's exact extended-capability set).
    var extended_count: u32 = 0;
    var extended = function.extendedCapabilities();
    while (extended.next()) |_| extended_count += 1;
    writeLine("pci-cap-test: {d} extended capabilities\n", .{extended_count});
    if (!check("extended walk terminates", extended_count < 480)) return;

    // Power: QEMU leaves the function in D0; ensurePowerStateD0 must agree and not
    // disturb the PMCSR.
    const power_cap = function.findCapability(.power_management).?;
    const pmcsr_before = mmio.readRegister(u16, power_cap.offset + pci_class.power_management.control_status);
    if (!check("power state is D0", pmcsr_before & pci_class.power_management.control_status_power_state_mask == pci_class.power_management.power_state_d0)) return;
    function.ensurePowerStateD0();
    if (!check("ensurePowerStateD0 is a no-op at D0", mmio.readRegister(u16, power_cap.offset + pci_class.power_management.control_status) == pmcsr_before)) return;

    // Function-level reset, where the device offers it: afterwards the function must be
    // readable with its identity intact, and bring-up must work again.
    const express_cap = function.findCapability(.pci_express).?;
    const device_capabilities = mmio.readRegister(u32, express_cap.offset + pci_class.pci_express.device_capabilities);
    if (device_capabilities & pci_class.pci_express.device_capabilities_flr != 0) {
        if (!check("functionLevelReset", function.functionLevelReset())) return;
        if (!check("identity after flr", function.vendorId() == intel_vendor and function.deviceId() == e1000e_device)) return;
        function.enableMemoryAndBusMaster();
        if (!check("re-enable after flr", function.command() & pci_class.command_memory_and_bus_master == pci_class.command_memory_and_bus_master)) return;
    } else {
        _ = logging.write("pci-cap-test: flr not offered, skipped\n");
    }

    _ = logging.write("pci-cap-test: all checks passed\n");
}
