//! liveness_test — proves the LivenessSource end-to-end (Part 5.5 + 3.6) using
//! REAL sockets. A closed socket (a dead client) is detected by the kernel and
//! drives lease reclamation in the core — the G3 chain, no mocks:
//!   real connection death → liveness signal → core reclaims leases → safe-reset.

const std = @import("std");
const testing = std.testing;
const posix = std.posix;
const model = @import("mgf_hrb_model");
const kabi = @import("mgf_hrb_kabi");
const Core = @import("mgf_hrb_core").Core;
const FakeBackend = @import("mgf_hrb_fake_backend").FakeBackend;
const liveness = @import("mgf_hrb_liveness");

const Urn = model.Urn;
const ClientId = model.ClientId;

fn seeded(fb: *FakeBackend) !kabi.KernelBackend {
    try fb.add("hw:/pci/0000:01:00.0/fn0", .claimable, .pcie_function);
    try fb.add("hw:/usb/bus1/dev4/if0", .claimable, .usb_interface);
    return fb.backend();
}

// Adapter: wire the LivenessMonitor's death signal into Core.holderDied.
const CoreSink = struct {
    core: *Core,
    fn onDeath(ctx: *anyopaque, who: ClientId) void {
        const self: *CoreSink = @ptrCast(@alignCast(ctx));
        _ = self.core.holderDied(who);
    }
    fn source(self: *CoreSink) liveness.LivenessSource {
        return .{ .ctx = self, .on_death = onDeath };
    }
};

test "liveness: a CLOSED socket (dead client) drives lease reclamation (G3 end-to-end)" {
    var fb = FakeBackend.init(testing.allocator);
    defer fb.deinit();
    var core = Core.init(testing.allocator, try seeded(&fb));
    defer core.deinit();

    var sink = CoreSink{ .core = &core };
    var mon = liveness.LivenessMonitor.init(testing.allocator, sink.source());
    defer mon.deinit();

    // a real connected pair: [0] = broker side (we poll it), [1] = client side
    const pair = try liveness.seqpacketPair();
    const broker_fd = pair.broker;
    const client_fd = pair.client;

    // client 7 connects and claims two devices
    try mon.register(broker_fd, 7);
    _ = try core.claim(&Urn.from("hw:/pci/0000:01:00.0/fn0"), .exclusive, 7, kabi.Deadline.none);
    _ = try core.claim(&Urn.from("hw:/usb/bus1/dev4/if0"), .exclusive, 7, kabi.Deadline.none);
    try testing.expectEqual(@as(usize, 2), core.liveLeaseCount());

    // ── the client process "dies": its socket end closes ──
    liveness.closeFd(client_fd);

    // the broker polls connections; the kernel reports the peer is gone
    const reclaimed = mon.pollConnections();
    try testing.expectEqual(@as(usize, 1), reclaimed); // one client reclaimed
    try testing.expectEqual(@as(usize, 0), mon.count()); // dropped from monitor

    // and the CORE has reclaimed BOTH its leases and safe-reset both devices
    try testing.expectEqual(@as(usize, 0), core.liveLeaseCount());
    try testing.expect(fb.isSafe("hw:/pci/0000:01:00.0/fn0"));
    try testing.expect(fb.isSafe("hw:/usb/bus1/dev4/if0"));

    liveness.closeFd(broker_fd);
}

test "liveness: a LIVE socket is not reclaimed (no false positive)" {
    var fb = FakeBackend.init(testing.allocator);
    defer fb.deinit();
    var core = Core.init(testing.allocator, try seeded(&fb));
    defer core.deinit();
    var sink = CoreSink{ .core = &core };
    var mon = liveness.LivenessMonitor.init(testing.allocator, sink.source());
    defer mon.deinit();

    const pair = try liveness.seqpacketPair();
    try mon.register(pair.broker, 1);
    _ = try core.claim(&Urn.from("hw:/pci/0000:01:00.0/fn0"), .exclusive, 1, kabi.Deadline.none);

    // client is alive (both ends open) → poll must NOT reclaim
    const reclaimed = mon.pollConnections();
    try testing.expectEqual(@as(usize, 0), reclaimed);
    try testing.expectEqual(@as(usize, 1), core.liveLeaseCount()); // lease intact

    liveness.closeFd(pair.broker);
    liveness.closeFd(pair.client);
}

test "liveness: pending protocol data does NOT look like death (peek, not consume)" {
    var fb = FakeBackend.init(testing.allocator);
    defer fb.deinit();
    var core = Core.init(testing.allocator, try seeded(&fb));
    defer core.deinit();
    var sink = CoreSink{ .core = &core };
    var mon = liveness.LivenessMonitor.init(testing.allocator, sink.source());
    defer mon.deinit();

    const pair = try liveness.seqpacketPair();
    try mon.register(pair.broker, 1);
    _ = try core.claim(&Urn.from("hw:/pci/0000:01:00.0/fn0"), .exclusive, 1, kabi.Deadline.none);

    // client sends a real message — alive, with data pending
    _ = try liveness.sendMsg(pair.client, "hello");
    const reclaimed = mon.pollConnections();
    try testing.expectEqual(@as(usize, 0), reclaimed); // data pending ≠ death
    try testing.expectEqual(@as(usize, 1), core.liveLeaseCount());

    // and the peeked data is still there to be consumed by the real protocol path
    var buf: [16]u8 = undefined;
    const n = try liveness.recvMsg(pair.broker, &buf);
    try testing.expectEqual(@as(usize, 5), n); // "hello" not consumed by the liveness peek

    liveness.closeFd(pair.broker);
    liveness.closeFd(pair.client);
}

test "liveness: heartbeat backstop reclaims a HUNG client (signal 2)" {
    var fb = FakeBackend.init(testing.allocator);
    defer fb.deinit();
    var core = Core.init(testing.allocator, try seeded(&fb));
    defer core.deinit();
    var sink = CoreSink{ .core = &core };
    var mon = liveness.LivenessMonitor.init(testing.allocator, sink.source());
    defer mon.deinit();
    mon.max_missed = 2;

    const pair = try liveness.seqpacketPair();
    try mon.register(pair.broker, 5);
    _ = try core.claim(&Urn.from("hw:/pci/0000:01:00.0/fn0"), .exclusive, 5, kabi.Deadline.none);

    // connection stays OPEN (so signal 1 never fires) but the client is wedged
    // and sends no heartbeats. Tick past max_missed.
    try testing.expectEqual(@as(usize, 0), mon.tickHeartbeats()); // miss 1
    try testing.expectEqual(@as(usize, 0), mon.tickHeartbeats()); // miss 2
    const reclaimed = mon.tickHeartbeats(); // miss 3 > max_missed(2) → hung
    try testing.expectEqual(@as(usize, 1), reclaimed);
    try testing.expectEqual(@as(usize, 0), core.liveLeaseCount()); // hung client's lease reclaimed
    try testing.expect(fb.isSafe("hw:/pci/0000:01:00.0/fn0"));

    liveness.closeFd(pair.broker);
    liveness.closeFd(pair.client);
}

test "liveness: a heartbeat resets the miss counter (healthy client survives)" {
    var fb = FakeBackend.init(testing.allocator);
    defer fb.deinit();
    var core = Core.init(testing.allocator, try seeded(&fb));
    defer core.deinit();
    var sink = CoreSink{ .core = &core };
    var mon = liveness.LivenessMonitor.init(testing.allocator, sink.source());
    defer mon.deinit();
    mon.max_missed = 2;

    const pair = try liveness.seqpacketPair();
    try mon.register(pair.broker, 5);
    _ = try core.claim(&Urn.from("hw:/pci/0000:01:00.0/fn0"), .exclusive, 5, kabi.Deadline.none);

    _ = mon.tickHeartbeats(); // miss 1
    _ = mon.tickHeartbeats(); // miss 2
    mon.heartbeat(5); // client checks in → counter resets
    _ = mon.tickHeartbeats(); // miss 1 again (not 3)
    try testing.expectEqual(@as(usize, 1), core.liveLeaseCount()); // survived
    try testing.expectEqual(@as(usize, 1), mon.count());

    liveness.closeFd(pair.broker);
    liveness.closeFd(pair.client);
}
