//! Zig-side unit tests for the subscription module (Part 13 §5.4.2).
//!
//! Covers:
//!   * Predicate parser — every grammar form + each rejection branch.
//!   * Predicate evaluator — every operator + term against synthetic
//!     events, with both intrinsic-attr matching and the lookup
//!     closure path.
//!   * RingBuffer — ordering, overflow → gap-event emission.
//!   * Registry — register, dedup, unregister, reapPeer, fan-out
//!     filtering.

const std = @import("std");
const testing = std.testing;
const json = std.json;

const model = @import("mgf_hrb_model");
const sub = @import("mgf_hrb_subscription");
const Event = model.Event;
const Urn = model.Urn;
const Err = model.Err;

// ---------------------------------------------------------------------------
// Predicate parsing.
// ---------------------------------------------------------------------------

fn parse(s: []const u8, arena: std.mem.Allocator) Err!sub.Predicate {
    var p = json.parseFromSlice(json.Value, testing.allocator, s, .{}) catch
        return Err.BadRequest;
    defer p.deinit();
    var budget: u32 = sub.MAX_PREDICATE_NODES;
    return sub.parsePredicate(p.value, arena, &budget);
}

test "predicate: kind_in single value" {
    var arena = std.heap.ArenaAllocator.init(testing.allocator);
    defer arena.deinit();
    const pred = try parse("{\"kind_in\":[\"added\"]}", arena.allocator());
    try testing.expect(pred == .kind_in);
    try testing.expectEqual(@as(usize, 1), pred.kind_in.len);
    try testing.expectEqual(sub.EventKind.added, pred.kind_in[0]);
}

test "predicate: kind_in unknown value rejects" {
    var arena = std.heap.ArenaAllocator.init(testing.allocator);
    defer arena.deinit();
    try testing.expectError(Err.BadRequest, parse(
        "{\"kind_in\":[\"not_a_real_event\"]}",
        arena.allocator(),
    ));
}

test "predicate: urn_glob string" {
    var arena = std.heap.ArenaAllocator.init(testing.allocator);
    defer arena.deinit();
    const pred = try parse(
        "{\"urn_glob\":\"hw:/usb/**\"}",
        arena.allocator(),
    );
    try testing.expectEqualStrings("hw:/usb/**", pred.urn_glob);
}

test "predicate: attr_eq parses key/value" {
    var arena = std.heap.ArenaAllocator.init(testing.allocator);
    defer arena.deinit();
    const pred = try parse(
        "{\"attr_eq\":{\"key\":\"vid\",\"value\":\"0x1234\"}}",
        arena.allocator(),
    );
    try testing.expectEqualStrings("vid", pred.attr_eq.key);
    try testing.expectEqualStrings("0x1234", pred.attr_eq.value);
}

test "predicate: attr_in parses values list" {
    var arena = std.heap.ArenaAllocator.init(testing.allocator);
    defer arena.deinit();
    const pred = try parse(
        "{\"attr_in\":{\"key\":\"vid\",\"values\":[\"0x1\",\"0x2\"]}}",
        arena.allocator(),
    );
    try testing.expectEqualStrings("vid", pred.attr_in.key);
    try testing.expectEqual(@as(usize, 2), pred.attr_in.values.len);
    try testing.expectEqualStrings("0x1", pred.attr_in.values[0]);
    try testing.expectEqualStrings("0x2", pred.attr_in.values[1]);
}

test "predicate: all_of nests children" {
    var arena = std.heap.ArenaAllocator.init(testing.allocator);
    defer arena.deinit();
    const pred = try parse(
        \\{"all_of":[{"kind_in":["added"]},{"urn_glob":"hw:/x/**"}]}
    ,
        arena.allocator(),
    );
    try testing.expectEqual(@as(usize, 2), pred.all_of.len);
}

test "predicate: any_of + not" {
    var arena = std.heap.ArenaAllocator.init(testing.allocator);
    defer arena.deinit();
    const pred = try parse(
        \\{"not":{"any_of":[{"kind_in":["gap"]}]}}
    ,
        arena.allocator(),
    );
    try testing.expect(pred == .not);
    try testing.expect(pred.not.* == .any_of);
}

test "predicate: multi-key object rejects (must be tagged-dict)" {
    var arena = std.heap.ArenaAllocator.init(testing.allocator);
    defer arena.deinit();
    try testing.expectError(Err.BadRequest, parse(
        "{\"kind_in\":[\"added\"],\"urn_glob\":\"hw:/x\"}",
        arena.allocator(),
    ));
}

test "predicate: unknown operator rejects" {
    var arena = std.heap.ArenaAllocator.init(testing.allocator);
    defer arena.deinit();
    try testing.expectError(Err.BadRequest, parse(
        "{\"weird_op\":[]}",
        arena.allocator(),
    ));
}

test "predicate: node-budget exhaustion rejects" {
    var arena = std.heap.ArenaAllocator.init(testing.allocator);
    defer arena.deinit();
    // Build a deeply nested chain of `not` wrappers to exhaust the budget.
    var buf: [4096]u8 = undefined;
    var pos: usize = 0;
    for (0..sub.MAX_PREDICATE_NODES + 5) |_| {
        const seg = "{\"not\":";
        @memcpy(buf[pos..][0..seg.len], seg);
        pos += seg.len;
    }
    const leaf = "{\"kind_in\":[\"added\"]}";
    @memcpy(buf[pos..][0..leaf.len], leaf);
    pos += leaf.len;
    for (0..sub.MAX_PREDICATE_NODES + 5) |_| {
        buf[pos] = '}';
        pos += 1;
    }
    try testing.expectError(Err.BadRequest, parse(buf[0..pos], arena.allocator()));
}

// ---------------------------------------------------------------------------
// Predicate evaluation.
// ---------------------------------------------------------------------------

const no_lookup: sub.AttrLookup = .{ .ctx = null, .get = null };

fn ev_added(u: []const u8) Event {
    return .{ .added = .{ .urn = Urn.from(u) } };
}
fn ev_driver_bound(u: []const u8, drv: []const u8) Event {
    return .{ .driver_bound = .{
        .urn = Urn.from(u),
        .driver = model.DriverName.from(drv),
    } };
}
fn ev_gap() Event {
    return .{ .gap = .{ .source = .netlink_uevent, .since_ms = 0 } };
}

test "eval: matches_all matches every event" {
    const pred: sub.Predicate = .{ .matches_all = {} };
    try testing.expect(sub.evaluate(&pred, &ev_added("hw:/usb/x"), no_lookup));
    try testing.expect(sub.evaluate(&pred, &ev_gap(), no_lookup));
}

test "eval: kind_in" {
    var arena = std.heap.ArenaAllocator.init(testing.allocator);
    defer arena.deinit();
    const pred = try parse("{\"kind_in\":[\"added\",\"driver_bound\"]}", arena.allocator());
    try testing.expect(sub.evaluate(&pred, &ev_added("hw:/x"), no_lookup));
    try testing.expect(sub.evaluate(&pred, &ev_driver_bound("hw:/x", "usbhid"), no_lookup));
    try testing.expect(!sub.evaluate(&pred, &ev_gap(), no_lookup));
}

test "eval: urn_glob" {
    var arena = std.heap.ArenaAllocator.init(testing.allocator);
    defer arena.deinit();
    const pred = try parse("{\"urn_glob\":\"hw:/usb/**\"}", arena.allocator());
    try testing.expect(sub.evaluate(&pred, &ev_added("hw:/usb/3-9"), no_lookup));
    try testing.expect(!sub.evaluate(&pred, &ev_added("hw:/pci/0000:00:1c.0"), no_lookup));
    // Gap has no URN; urn_glob should reject (no false positives).
    try testing.expect(!sub.evaluate(&pred, &ev_gap(), no_lookup));
}

test "eval: attr_eq against intrinsic 'kind'" {
    var arena = std.heap.ArenaAllocator.init(testing.allocator);
    defer arena.deinit();
    const pred = try parse(
        "{\"attr_eq\":{\"key\":\"kind\",\"value\":\"added\"}}",
        arena.allocator(),
    );
    try testing.expect(sub.evaluate(&pred, &ev_added("hw:/x"), no_lookup));
    try testing.expect(!sub.evaluate(&pred, &ev_gap(), no_lookup));
}

test "eval: attr_eq against intrinsic 'driver'" {
    var arena = std.heap.ArenaAllocator.init(testing.allocator);
    defer arena.deinit();
    const pred = try parse(
        "{\"attr_eq\":{\"key\":\"driver\",\"value\":\"usbhid\"}}",
        arena.allocator(),
    );
    try testing.expect(sub.evaluate(&pred, &ev_driver_bound("hw:/x", "usbhid"), no_lookup));
    try testing.expect(!sub.evaluate(&pred, &ev_driver_bound("hw:/x", "uvcvideo"), no_lookup));
    // added has no driver field; attr_eq{driver} falls through to lookup
    // (which is null) — should NOT match.
    try testing.expect(!sub.evaluate(&pred, &ev_added("hw:/x"), no_lookup));
}

test "eval: attr_eq with device-attr lookup closure" {
    const Ctx = struct {
        const Self = @This();
        fn lookup(ctx: ?*anyopaque, urn: []const u8, key: []const u8) ?[]const u8 {
            _ = ctx;
            if (std.mem.eql(u8, urn, "hw:/usb/a") and std.mem.eql(u8, key, "vid")) {
                return "0x1234";
            }
            return null;
        }
    };
    var arena = std.heap.ArenaAllocator.init(testing.allocator);
    defer arena.deinit();
    const pred = try parse(
        "{\"attr_eq\":{\"key\":\"vid\",\"value\":\"0x1234\"}}",
        arena.allocator(),
    );
    const lookup: sub.AttrLookup = .{ .ctx = null, .get = Ctx.lookup };
    try testing.expect(sub.evaluate(&pred, &ev_added("hw:/usb/a"), lookup));
    try testing.expect(!sub.evaluate(&pred, &ev_added("hw:/usb/b"), lookup));
}

test "eval: attr_in against intrinsic 'kind'" {
    var arena = std.heap.ArenaAllocator.init(testing.allocator);
    defer arena.deinit();
    const pred = try parse(
        "{\"attr_in\":{\"key\":\"kind\",\"values\":[\"added\",\"removed\"]}}",
        arena.allocator(),
    );
    try testing.expect(sub.evaluate(&pred, &ev_added("hw:/x"), no_lookup));
    try testing.expect(!sub.evaluate(&pred, &ev_driver_bound("hw:/x", "u"), no_lookup));
}

test "eval: all_of combines (both true)" {
    var arena = std.heap.ArenaAllocator.init(testing.allocator);
    defer arena.deinit();
    const pred = try parse(
        \\{"all_of":[{"kind_in":["driver_bound"]},{"urn_glob":"hw:/usb/**"}]}
    , arena.allocator());

    // Sanity: each child predicate evaluates independently as expected
    // (catches a regression in either kind_in or urn_glob without
    // ascribing the failure to the all_of combiner).
    try testing.expect(pred == .all_of);
    try testing.expectEqual(@as(usize, 2), pred.all_of.len);
    const dbound = ev_driver_bound("hw:/usb/3-9", "usbhid");
    try testing.expect(sub.evaluate(&pred.all_of[0], &dbound, no_lookup));
    try testing.expect(sub.evaluate(&pred.all_of[1], &dbound, no_lookup));
    try testing.expect(sub.evaluate(&pred, &dbound, no_lookup));

    const dbound_pci = ev_driver_bound("hw:/pci/x", "x");
    try testing.expect(!sub.evaluate(&pred, &dbound_pci, no_lookup));
    const added_usb = ev_added("hw:/usb/x");
    try testing.expect(!sub.evaluate(&pred, &added_usb, no_lookup));
}

test "eval: any_of (either true)" {
    var arena = std.heap.ArenaAllocator.init(testing.allocator);
    defer arena.deinit();
    const pred = try parse(
        \\{"any_of":[{"kind_in":["added"]},{"kind_in":["driver_bound"]}]}
    , arena.allocator());
    try testing.expect(sub.evaluate(&pred, &ev_added("hw:/x"), no_lookup));
    try testing.expect(sub.evaluate(&pred, &ev_driver_bound("hw:/x", "u"), no_lookup));
    try testing.expect(!sub.evaluate(&pred, &ev_gap(), no_lookup));
}

test "eval: not inverts" {
    var arena = std.heap.ArenaAllocator.init(testing.allocator);
    defer arena.deinit();
    const pred = try parse(
        \\{"not":{"kind_in":["gap"]}}
    , arena.allocator());
    try testing.expect(sub.evaluate(&pred, &ev_added("hw:/x"), no_lookup));
    try testing.expect(!sub.evaluate(&pred, &ev_gap(), no_lookup));
}

// ---------------------------------------------------------------------------
// Registry — fan-out + lifecycle.
// ---------------------------------------------------------------------------

test "registry: register + drain matched events" {
    sub.init(testing.allocator);
    defer sub.deinit();

    var arena = std.heap.ArenaAllocator.init(testing.allocator);
    const pred = try parse("{\"kind_in\":[\"added\"]}", arena.allocator());

    _ = try sub.register(1234, "s1", "hw:/usb/**", pred, arena);

    sub.pushFanout(ev_added("hw:/usb/3-9"), no_lookup);
    sub.pushFanout(ev_driver_bound("hw:/usb/3-9", "usbhid"), no_lookup);
    sub.pushFanout(ev_added("hw:/pci/x"), no_lookup); // wrong scope
    sub.pushFanout(ev_added("hw:/usb/4-1"), no_lookup);

    var buf: [16]Event = undefined;
    const n = try sub.drainSubscription(1234, "s1", &buf);
    try testing.expectEqual(@as(usize, 2), n);
    try testing.expectEqualStrings("hw:/usb/3-9", buf[0].added.urn.slice());
    try testing.expectEqualStrings("hw:/usb/4-1", buf[1].added.urn.slice());
}

test "registry: unregister tears down" {
    sub.init(testing.allocator);
    defer sub.deinit();

    var arena = std.heap.ArenaAllocator.init(testing.allocator);
    const pred = try parse("{\"kind_in\":[\"added\"]}", arena.allocator());
    _ = try sub.register(1, "s1", "hw:/**", pred, arena);
    try testing.expectEqual(@as(usize, 1), sub.count());

    try sub.unregister(1, "s1");
    try testing.expectEqual(@as(usize, 0), sub.count());

    try testing.expectError(Err.NotFound, sub.unregister(1, "s1"));
}

test "registry: reapPeer drops all peer's subscriptions" {
    sub.init(testing.allocator);
    defer sub.deinit();

    var a1 = std.heap.ArenaAllocator.init(testing.allocator);
    var a2 = std.heap.ArenaAllocator.init(testing.allocator);
    var a3 = std.heap.ArenaAllocator.init(testing.allocator);
    const p1 = try parse("{\"kind_in\":[\"added\"]}", a1.allocator());
    const p2 = try parse("{\"kind_in\":[\"removed\"]}", a2.allocator());
    const p3 = try parse("{\"kind_in\":[\"added\"]}", a3.allocator());
    _ = try sub.register(1, "s1", "hw:/**", p1, a1);
    _ = try sub.register(1, "s2", "hw:/**", p2, a2);
    _ = try sub.register(2, "s1", "hw:/**", p3, a3); // different peer, same id ok
    try testing.expectEqual(@as(usize, 3), sub.count());

    sub.reapPeer(1);
    try testing.expectEqual(@as(usize, 1), sub.count());
    // peer 2's s1 still there.
    var buf: [4]Event = undefined;
    sub.pushFanout(ev_added("hw:/x"), no_lookup);
    try testing.expectEqual(@as(usize, 1), try sub.drainSubscription(2, "s1", &buf));
}

test "registry: duplicate (peer, id) rejects" {
    sub.init(testing.allocator);
    defer sub.deinit();

    var a1 = std.heap.ArenaAllocator.init(testing.allocator);
    var a2 = std.heap.ArenaAllocator.init(testing.allocator);
    const p1 = try parse("{\"kind_in\":[\"added\"]}", a1.allocator());
    const p2 = try parse("{\"kind_in\":[\"removed\"]}", a2.allocator());
    _ = try sub.register(1, "s1", "hw:/**", p1, a1);
    try testing.expectError(Err.BadRequest, sub.register(1, "s1", "hw:/**", p2, a2));
}

test "registry: queue overflow emits a gap event" {
    sub.init(testing.allocator);
    defer sub.deinit();

    const arena = std.heap.ArenaAllocator.init(testing.allocator);
    const pred: sub.Predicate = .{ .matches_all = {} };
    _ = try sub.register(1, "s1", "hw:/**", pred, arena);

    // Fill the queue past MAX_QUEUE_LEN.
    for (0..sub.MAX_QUEUE_LEN + 4) |i| {
        var buf: [32]u8 = undefined;
        const urn = std.fmt.bufPrint(&buf, "hw:/x/{d}", .{i}) catch unreachable;
        sub.pushFanout(ev_added(urn), no_lookup);
    }

    // Drain everything.
    var out: [sub.MAX_QUEUE_LEN]Event = undefined;
    const n = try sub.drainSubscription(1, "s1", &out);
    try testing.expectEqual(@as(usize, sub.MAX_QUEUE_LEN), n);
    // The OLDEST entries got replaced by a gap event during overflow.
    var saw_gap = false;
    for (out[0..n]) |ev| {
        if (ev == .gap and ev.gap.source == .subscription_overflow) saw_gap = true;
    }
    try testing.expect(saw_gap);
}

test "registry: gap events pass to every subscription regardless of scope" {
    sub.init(testing.allocator);
    defer sub.deinit();

    const arena = std.heap.ArenaAllocator.init(testing.allocator);
    const pred: sub.Predicate = .{ .matches_all = {} };
    _ = try sub.register(1, "s1", "hw:/very/specific/**", pred, arena);

    sub.pushFanout(ev_gap(), no_lookup);
    var buf: [4]Event = undefined;
    const n = try sub.drainSubscription(1, "s1", &buf);
    try testing.expectEqual(@as(usize, 1), n);
    try testing.expect(buf[0] == .gap);
}

// ---------------------------------------------------------------------------
// R2-A3 — concurrent pushFanout (evaluates predicates OUTSIDE the registry
// lock) racing reapPeer (which may free a subscription mid-eval). The
// refcount + deferred-free contract must hold: no use-after-free, no
// double-free, no leak. The GPA-backed testing allocator's safety checks
// ARE the assertion — a UAF/double-free/leak fails the test at teardown.
//
// `testing.allocator` is used directly (not a thread-safe wrapper): every
// real child-allocator call — `register`'s `create`, the deferred free's
// `destroy`, and the empty arena's `deinit` — happens UNDER `registry.lock`,
// so they're serialized. The `matches_all` predicate allocates nothing into
// its arena, so the only unlocked work on the churn thread (arena init) and
// the fan-out thread (Phase-2 eval) touches no allocator at all.
// ---------------------------------------------------------------------------

const StressFanout = struct {
    fn run() void {
        var i: usize = 0;
        while (i < 4000) : (i += 1) {
            // Phase-2 eval dereferences the (possibly-being-reaped)
            // predicate AST; the ref pin must keep it alive.
            sub.pushFanout(ev_added("hw:/usb/3-9"), no_lookup);
        }
    }
};

const StressChurn = struct {
    fn run(alloc: std.mem.Allocator) void {
        var i: usize = 0;
        while (i < 4000) : (i += 1) {
            // matches_all needs no arena allocation, so the churn loop
            // doesn't depend on the JSON `parse` helper (which would pin
            // the raw testing.allocator and race the fan-out free).
            const arena = std.heap.ArenaAllocator.init(alloc);
            const pred: sub.Predicate = .{ .matches_all = {} };
            _ = sub.register(42, "s", "hw:/usb/**", pred, arena) catch continue;
            sub.reapPeer(42); // may unlink while a fan-out eval holds a ref
        }
    }
};

test "R2-A3: concurrent pushFanout + reapPeer is UAF/leak-free (deferred free)" {
    sub.init(testing.allocator);
    defer sub.deinit();

    const t_fan = try std.Thread.spawn(.{}, StressFanout.run, .{});
    const t_churn = try std.Thread.spawn(.{}, StressChurn.run, .{testing.allocator});
    t_fan.join();
    t_churn.join();

    // Reap any survivor so deinit + the allocator's end-of-test leak check
    // see a clean registry. The real verdict is the allocator's safety
    // accounting across the ~4000 racing register/reap/eval cycles.
    sub.reapPeer(42);
    try testing.expectEqual(@as(usize, 0), sub.count());
}
