//! event_pump_test — M-B B2. The in-broker event pump's lifecycle and its
//! one behavioural promise: an event that becomes readable on a backend's
//! watch fd reaches every subscription's ring WITHOUT any client polling.
//!
//! The pump takes `{fd, ctx, drain-fn}` triples rather than a facade, so
//! these pins need no hardware: a pipe stands in for the netlink socket
//! and a closure for `pumpUevent`. That is the whole point of the seam —
//! the thing under test is the pump's scheduling, not USB.

const std = @import("std");
const testing = std.testing;
const model = @import("mgf_hrb_model");
const kabi = @import("mgf_hrb_kabi");
const sub = @import("mgf_hrb_subscription");
const pump_mod = @import("mgf_hrb_event_pump");

const Event = model.Event;
const Err = model.Err;
const linux = std.os.linux;

// ---------------------------------------------------------------------------
// A stand-in backend: a pipe for readiness + a bounded queue of events its
// "drain" hands over. Exactly the contract `LinuxUsbBackend.vDrainEvents`
// implements against netlink.
// ---------------------------------------------------------------------------

const FakeSource = struct {
    read_fd: i32 = -1,
    write_fd: i32 = -1,
    lock: kabi.SpinMutex = .{},
    pending: [32]Event = undefined,
    n: usize = 0,
    drains: std.atomic.Value(u64) = std.atomic.Value(u64).init(0),
    fail_next: bool = false,

    fn open(self: *FakeSource) !void {
        var fds: [2]i32 = undefined;
        const rc = linux.pipe2(&fds, .{ .NONBLOCK = true, .CLOEXEC = true });
        if (linux.errno(rc) != .SUCCESS) return error.PipeFailed;
        self.read_fd = fds[0];
        self.write_fd = fds[1];
    }

    fn close(self: *FakeSource) void {
        if (self.read_fd >= 0) _ = linux.close(self.read_fd);
        if (self.write_fd >= 0) _ = linux.close(self.write_fd);
        self.read_fd = -1;
        self.write_fd = -1;
    }

    /// Queue an event AND make the fd readable — the two halves of "the
    /// kernel put a uevent on the netlink socket".
    fn inject(self: *FakeSource, ev: Event) void {
        {
            self.lock.lock();
            defer self.lock.unlock();
            if (self.n < self.pending.len) {
                self.pending[self.n] = ev;
                self.n += 1;
            }
        }
        const byte: u8 = 1;
        _ = linux.write(self.write_fd, @ptrCast(&byte), 1);
    }

    /// The drain: hand over everything queued, and consume the readiness
    /// bytes so a level-triggered poll doesn't re-fire forever.
    fn drain(
        ctx: *anyopaque,
        scope: []const u8,
        sink: *const kabi.EventSink,
        deadline: kabi.Deadline,
    ) Err!void {
        _ = scope;
        _ = deadline;
        const self: *FakeSource = @ptrCast(@alignCast(ctx));
        _ = self.drains.fetchAdd(1, .monotonic);
        var swallow: [64]u8 = undefined;
        while (linux.errno(linux.read(self.read_fd, &swallow, swallow.len)) == .SUCCESS) {}
        if (self.fail_next) {
            self.fail_next = false;
            return Err.HardwareFault;
        }
        var batch: [32]Event = undefined;
        var count: usize = 0;
        {
            self.lock.lock();
            defer self.lock.unlock();
            count = self.n;
            @memcpy(batch[0..count], self.pending[0..count]);
            self.n = 0;
        }
        for (batch[0..count]) |ev| sink.push(sink.ctx, ev);
    }
};

fn evAdded(urn: []const u8) Event {
    return .{ .added = .{ .urn = model.Urn.from(urn) } };
}

/// std's `sleep` moved onto the async `std.Io` interface in 0.16; these
/// plain test threads carry no `Io`. Same raw-syscall idiom `shm.zig` uses.
fn sleepMs(ms: u64) void {
    const ts = linux.timespec{
        .sec = @intCast(ms / std.time.ms_per_s),
        .nsec = @intCast((ms % std.time.ms_per_s) * std.time.ns_per_ms),
    };
    _ = linux.nanosleep(&ts, null);
}

fn subscribeAll(peer: u64, id: []const u8, scope: []const u8) !void {
    const arena = std.heap.ArenaAllocator.init(testing.allocator);
    _ = try sub.register(peer, id, scope, .{ .matches_all = {} }, arena);
}

/// Spin (bounded) until `f` is true, so the pins don't hard-code a sleep.
fn waitUntil(ctx: anytype, comptime f: fn (@TypeOf(ctx)) bool, budget_ms: u64) bool {
    const start = kabi.nowMs();
    while (kabi.nowMs() - start < budget_ms) {
        if (f(ctx)) return true;
        sleepMs(2);
    }
    return f(ctx);
}

// ---------------------------------------------------------------------------
// Lifecycle.
// ---------------------------------------------------------------------------

test "pump lifecycle: start spawns, stop joins, no fd or thread leak" {
    sub.init(testing.allocator);
    defer sub.deinit();

    var src = FakeSource{};
    try src.open();
    defer src.close();

    var pump = pump_mod.Pump{};
    try pump.addSource(src.read_fd, &src, FakeSource.drain, "hw:/usb/**");
    try pump.start();
    // A pass happens without any event: the tick is what flushes the USB
    // synthesiser's debounced removes, so it must actually fire.
    try testing.expect(waitUntil(&pump, struct {
        fn f(p: *pump_mod.Pump) bool {
            return p.passes.load(.monotonic) >= 1;
        }
    }.f, 2000));
    pump.stop();
    // Idempotent: the broker's teardown paths are not all reachable from
    // one place, so a second stop must be a no-op rather than a crash.
    pump.stop();
    try testing.expectEqual(@as(i32, -1), pump.stop_fd);
}

test "pump lifecycle: start twice is refused, wiring after start is refused" {
    sub.init(testing.allocator);
    defer sub.deinit();
    var src = FakeSource{};
    try src.open();
    defer src.close();

    var pump = pump_mod.Pump{};
    try pump.addSource(src.read_fd, &src, FakeSource.drain, "hw:/usb/**");
    try pump.start();
    defer pump.stop();
    try testing.expectError(Err.Conflict, pump.start());
    // Source wiring is a startup act; the table is deliberately
    // unsynchronised, so adding one to a running pump must be refused
    // rather than raced.
    try testing.expectError(
        Err.Conflict,
        pump.addSource(src.read_fd, &src, FakeSource.drain, "hw:/pci/**"),
    );
}

test "pump: an event on the watch fd reaches a subscriber with NOBODY polling" {
    sub.init(testing.allocator);
    defer sub.deinit();
    try subscribeAll(1, "s1", "hw:/usb/**");

    var src = FakeSource{};
    try src.open();
    defer src.close();

    var pump = pump_mod.Pump{};
    try pump.addSource(src.read_fd, &src, FakeSource.drain, "hw:/usb/**");
    try pump.start();
    defer pump.stop();

    src.inject(evAdded("hw:/usb/3-9"));

    // The subscriber never calls a backend. It waits on its own ring.
    var buf: [4]Event = undefined;
    const n = try sub.drainSubscriptionWait(1, "s1", &buf, 2000);
    try testing.expectEqual(@as(usize, 1), n);
    try testing.expectEqualStrings("hw:/usb/3-9", buf[0].added.urn.slice());
}

test "pump: it drains on the TICK too, with no fd readiness at all" {
    // The USB watch synthesiser holds `removed` events inside a debounce
    // window and flushes them at the head of each `pumpUevent` call. Those
    // have no fd readiness of their own — they are events waiting on a
    // clock. Before B2 the SDK's 0.5 s client poll supplied the passes that
    // surfaced them; with clients out of the drain path, this tick is the
    // whole guarantee. A pump that only drained on readiness would hold a
    // remove forever on a bus that then went quiet, which is a silent loss.
    sub.init(testing.allocator);
    defer sub.deinit();

    var src = FakeSource{};
    try src.open();
    defer src.close();

    var pump = pump_mod.Pump{};
    try pump.addSource(src.read_fd, &src, FakeSource.drain, "hw:/usb/**");
    try pump.start();
    defer pump.stop();

    // Nothing is ever injected, so the fd is never readable.
    try testing.expect(waitUntil(&src, struct {
        fn f(s: *FakeSource) bool {
            return s.drains.load(.monotonic) >= 2;
        }
    }.f, 3000));
}

test "pump: a failing drain retires its source instead of spinning the CPU" {
    sub.init(testing.allocator);
    defer sub.deinit();

    var src = FakeSource{};
    try src.open();
    defer src.close();
    src.fail_next = true;

    var pump = pump_mod.Pump{};
    try pump.addSource(src.read_fd, &src, FakeSource.drain, "hw:/usb/**");
    try pump.start();
    defer pump.stop();

    // Make the fd permanently readable. Pre-retirement this is the
    // busy-loop shape: poll returns instantly, drain fails, repeat.
    src.inject(evAdded("hw:/usb/3-9"));

    try testing.expect(waitUntil(&src, struct {
        fn f(s: *FakeSource) bool {
            return s.drains.load(.monotonic) >= 1;
        }
    }.f, 2000));
    // Give the pump room to spin if it were going to.
    sleepMs(120);
    const after = src.drains.load(.monotonic);
    try testing.expectEqual(@as(u64, 1), after);
    try testing.expect(pump.sources[0].disabled);
}

test "pump: caps its source table rather than overrunning it" {
    var pump = pump_mod.Pump{};
    var src = FakeSource{};
    var i: usize = 0;
    while (i < pump_mod.MAX_SOURCES) : (i += 1) {
        try pump.addSource(3, &src, FakeSource.drain, "hw:/**");
    }
    try testing.expectError(Err.Overloaded, pump.addSource(3, &src, FakeSource.drain, "hw:/**"));
}
