//! request_test — the S1 trust-gate conformance suite (Part 12 §3.2).
//!
//! The contract being tested: `request.parse()` is the ONLY way bytes become
//! typed requests. For ANY input — well-formed, malformed, type-confused,
//! oversized, random — the result MUST be either a valid `Request` or
//! `Err.BadRequest`. Never a panic. Never undefined behaviour. A client that
//! sends garbage MUST NOT be able to crash the broker.
//!
//! Each test below targets one row of the upward-attack taxonomy in the
//! survivability review (§3.1): U1 malformed request, U2 type confusion, U5
//! oversized input. The fuzzer at the bottom is the catch-all: random bytes
//! in, no panic out.

const std = @import("std");
const testing = std.testing;
const request = @import("mgf_hrb_request");
const model = @import("mgf_hrb_model");
const Err = model.Err;

// ---------------------------------------------------------------------------
// Happy path — the gate accepts well-formed requests and produces the
// expected typed `Request`. If these break, downstream tests are meaningless.
// ---------------------------------------------------------------------------

test "happy path: list parses to ListReq with the right scope" {
    const parsed = try request.parse("{\"op\":\"list\",\"scope\":\"hw:/**\"}", testing.allocator);
    try testing.expect(parsed.req == .list);
    try testing.expect(std.mem.eql(u8, parsed.req.list.scope.slice(), "hw:/**"));
    try testing.expectEqual(@as(u8, 0), parsed.correlation_id_len);
}

test "happy path: claim parses to ClaimReq with mode + holder" {
    const parsed = try request.parse(
        "{\"op\":\"claim\",\"urn\":\"hw:/pci/0000:01:00.0/fn0\",\"mode\":\"exclusive\",\"holder\":42}",
        testing.allocator,
    );
    try testing.expect(parsed.req == .claim);
    try testing.expectEqual(model.ClaimMode.exclusive, parsed.req.claim.mode);
    try testing.expectEqual(@as(model.ClientId, 42), parsed.req.claim.holder);
}

test "happy path: release parses to ReleaseReq" {
    const parsed = try request.parse(
        "{\"op\":\"release\",\"urn\":\"hw:/pci/0000:01:00.0/fn0\",\"lease_id\":7}",
        testing.allocator,
    );
    try testing.expect(parsed.req == .release);
    try testing.expectEqual(@as(model.LeaseId, 7), parsed.req.release.lease);
}

test "happy path: raw parses to RawReq" {
    const parsed = try request.parse(
        "{\"op\":\"raw\",\"urn\":\"hw:/pci/0000:01:00.0/fn0\",\"lease_id\":7,\"caller\":42}",
        testing.allocator,
    );
    try testing.expect(parsed.req == .raw);
    try testing.expectEqual(@as(model.ClientId, 42), parsed.req.raw.caller);
}

test "happy path: every valid mode round-trips" {
    inline for (.{ "observe", "shared_rw", "exclusive" }) |mode_str| {
        const line = "{\"op\":\"claim\",\"urn\":\"hw:/x/y\",\"mode\":\"" ++ mode_str ++ "\",\"holder\":1}";
        const parsed = try request.parse(line, testing.allocator);
        try testing.expect(parsed.req == .claim);
    }
}

// ---------------------------------------------------------------------------
// Part 13 §5.6.3 — correlation_id envelope field.
// ---------------------------------------------------------------------------

test "correlation_id: present + valid charset is captured into Parsed" {
    const parsed = try request.parse(
        "{\"op\":\"list\",\"scope\":\"hw:/**\",\"correlation_id\":\"vmgr-claim-001\"}",
        testing.allocator,
    );
    try testing.expectEqualStrings("vmgr-claim-001", parsed.correlationId());
}

test "correlation_id: absent leaves len == 0" {
    const parsed = try request.parse(
        "{\"op\":\"list\",\"scope\":\"hw:/**\"}",
        testing.allocator,
    );
    try testing.expectEqual(@as(u8, 0), parsed.correlation_id_len);
    try testing.expectEqualStrings("", parsed.correlationId());
}

test "correlation_id: non-string type is BadRequest" {
    try testing.expectError(Err.BadRequest, request.parse(
        "{\"op\":\"list\",\"scope\":\"hw:/**\",\"correlation_id\":42}",
        testing.allocator,
    ));
}

test "correlation_id: oversized value is BadRequest" {
    // 130 chars — over the 128-byte cap.
    var big_id: [130]u8 = undefined;
    @memset(&big_id, 'a');
    var line_buf: [256]u8 = undefined;
    const line = try std.fmt.bufPrint(
        &line_buf,
        "{{\"op\":\"list\",\"scope\":\"hw:/**\",\"correlation_id\":\"{s}\"}}",
        .{big_id},
    );
    try testing.expectError(Err.BadRequest, request.parse(line, testing.allocator));
}

test "correlation_id: out-of-charset characters are BadRequest" {
    // Space + `<` + `\\` are not in [A-Za-z0-9-_.]; reject at the gate so
    // the splice path doesn't need to JSON-escape on emit.
    try testing.expectError(Err.BadRequest, request.parse(
        "{\"op\":\"list\",\"scope\":\"hw:/**\",\"correlation_id\":\"has space\"}",
        testing.allocator,
    ));
    try testing.expectError(Err.BadRequest, request.parse(
        "{\"op\":\"list\",\"scope\":\"hw:/**\",\"correlation_id\":\"angle<bracket\"}",
        testing.allocator,
    ));
}

test "correlation_id: every allowed character round-trips" {
    const id = "abcdefghijklmnopqrstuvwxyz.ABCDEFGHIJKLMNOPQRSTUVWXYZ-0123456789_";
    var line_buf: [256]u8 = undefined;
    const line = try std.fmt.bufPrint(
        &line_buf,
        "{{\"op\":\"list\",\"scope\":\"hw:/**\",\"correlation_id\":\"{s}\"}}",
        .{id},
    );
    const parsed = try request.parse(line, testing.allocator);
    try testing.expectEqualStrings(id, parsed.correlationId());
}

// ---------------------------------------------------------------------------
// U1 — malformed input. JSON that doesn't parse, JSON that isn't an object,
// empty input. All must hit the gate as BadRequest, not a crash.
// ---------------------------------------------------------------------------

test "U1: empty input is BadRequest, not a crash" {
    try testing.expectError(Err.BadRequest, request.parse("", testing.allocator));
}

test "U1: non-JSON bytes are BadRequest, not a crash" {
    try testing.expectError(Err.BadRequest, request.parse("this is not JSON", testing.allocator));
}

test "U1: truncated JSON is BadRequest, not a crash" {
    try testing.expectError(Err.BadRequest, request.parse("{\"op\":\"list\",", testing.allocator));
}

test "U1: a JSON array at the top level is BadRequest (not an object)" {
    try testing.expectError(Err.BadRequest, request.parse("[1,2,3]", testing.allocator));
}

test "U1: a bare JSON number is BadRequest (not an object)" {
    try testing.expectError(Err.BadRequest, request.parse("42", testing.allocator));
}

test "U1: a JSON null is BadRequest (not an object)" {
    try testing.expectError(Err.BadRequest, request.parse("null", testing.allocator));
}

// ---------------------------------------------------------------------------
// U2 — type confusion. This is the class of attack the original `.string` /
// `.integer` JSON-tag panics enabled. The gate must reject every mismatch.
// ---------------------------------------------------------------------------

test "U2: op is an integer instead of a string — BadRequest" {
    try testing.expectError(
        Err.BadRequest,
        request.parse("{\"op\":123,\"scope\":\"hw:/**\"}", testing.allocator),
    );
}

test "U2: op is an object instead of a string — BadRequest" {
    try testing.expectError(
        Err.BadRequest,
        request.parse("{\"op\":{\"nested\":true},\"scope\":\"hw:/**\"}", testing.allocator),
    );
}

test "U2: scope is a number instead of a string — BadRequest" {
    try testing.expectError(
        Err.BadRequest,
        request.parse("{\"op\":\"list\",\"scope\":999}", testing.allocator),
    );
}

test "U2: holder is a string instead of an integer — BadRequest" {
    try testing.expectError(Err.BadRequest, request.parse(
        "{\"op\":\"claim\",\"urn\":\"hw:/x/y\",\"mode\":\"exclusive\",\"holder\":\"not-a-number\"}",
        testing.allocator,
    ));
}

test "U2: mode is an object instead of a string — BadRequest" {
    try testing.expectError(Err.BadRequest, request.parse(
        "{\"op\":\"claim\",\"urn\":\"hw:/x/y\",\"mode\":{\"x\":1},\"holder\":1}",
        testing.allocator,
    ));
}

test "U2: lease is a float instead of an integer — BadRequest" {
    // std.json parses 7.5 as a float; integer extractor rejects.
    try testing.expectError(Err.BadRequest, request.parse(
        "{\"op\":\"release\",\"urn\":\"hw:/x/y\",\"lease_id\":7.5}",
        testing.allocator,
    ));
}

// ---------------------------------------------------------------------------
// Missing-field cases. Validate-before-dispatch.
// ---------------------------------------------------------------------------

test "missing op key is BadRequest" {
    try testing.expectError(Err.BadRequest, request.parse("{\"scope\":\"hw:/**\"}", testing.allocator));
}

test "claim missing urn is BadRequest" {
    try testing.expectError(Err.BadRequest, request.parse(
        "{\"op\":\"claim\",\"mode\":\"exclusive\",\"holder\":1}",
        testing.allocator,
    ));
}

test "claim missing holder is BadRequest" {
    try testing.expectError(Err.BadRequest, request.parse(
        "{\"op\":\"claim\",\"urn\":\"hw:/x/y\",\"mode\":\"exclusive\"}",
        testing.allocator,
    ));
}

test "unknown op verb is BadRequest" {
    try testing.expectError(Err.BadRequest, request.parse(
        "{\"op\":\"DROP_TABLE\",\"scope\":\"hw:/**\"}",
        testing.allocator,
    ));
}

test "unknown mode value is BadRequest" {
    try testing.expectError(Err.BadRequest, request.parse(
        "{\"op\":\"claim\",\"urn\":\"hw:/x/y\",\"mode\":\"god\",\"holder\":1}",
        testing.allocator,
    ));
}

// ---------------------------------------------------------------------------
// U5 — oversized input. S5 bounded-everything, made concrete at the gate.
// ---------------------------------------------------------------------------

test "U5: an oversized line is rejected before parsing" {
    // build a line larger than MAX_LINE_BYTES
    const oversized = try testing.allocator.alloc(u8, request.MAX_LINE_BYTES + 1);
    defer testing.allocator.free(oversized);
    @memset(oversized, 'x');
    try testing.expectError(Err.BadRequest, request.parse(oversized, testing.allocator));
}

test "U5: an oversized urn string is rejected" {
    // 200 bytes of 'a' in the urn slot; MAX_URN_BYTES is 128.
    const big_urn = "a" ** 200;
    const line = "{\"op\":\"release\",\"urn\":\"" ++ big_urn ++ "\",\"lease_id\":1}";
    try testing.expectError(Err.BadRequest, request.parse(line, testing.allocator));
}

test "out-of-range integer is rejected (negative holder)" {
    try testing.expectError(Err.BadRequest, request.parse(
        "{\"op\":\"claim\",\"urn\":\"hw:/x/y\",\"mode\":\"exclusive\",\"holder\":-1}",
        testing.allocator,
    ));
}

test "out-of-range integer is rejected (zero lease — ids are 1-based)" {
    try testing.expectError(Err.BadRequest, request.parse(
        "{\"op\":\"release\",\"urn\":\"hw:/x/y\",\"lease_id\":0}",
        testing.allocator,
    ));
}

// === D-029 — deadline_ms is plumbed through the gate ========================

test "deadline_ms is parsed when present" {
    const parsed = try request.parse(
        "{\"op\":\"claim\",\"urn\":\"hw:/x/y\",\"mode\":\"exclusive\",\"holder\":1,\"deadline_ms\":5000}",
        testing.allocator,
    );
    try testing.expectEqual(@as(u64, 5000), parsed.req.claim.deadline_ms);
}

test "deadline_ms defaults to 0 when absent (Deadline.none on the Zig side)" {
    const parsed = try request.parse(
        "{\"op\":\"claim\",\"urn\":\"hw:/x/y\",\"mode\":\"exclusive\",\"holder\":1}",
        testing.allocator,
    );
    try testing.expectEqual(@as(u64, 0), parsed.req.claim.deadline_ms);
}

test "deadline_ms wrong type is BadRequest, not a silent default" {
    try testing.expectError(Err.BadRequest, request.parse(
        "{\"op\":\"claim\",\"urn\":\"hw:/x/y\",\"mode\":\"exclusive\",\"holder\":1,\"deadline_ms\":\"forever\"}",
        testing.allocator,
    ));
}

test "deadline_ms above MAX_DEADLINE_MS (1 hour) is rejected" {
    // Past the cap a typoed huge number would otherwise create a deadline a
    // wedged op could ride for centuries. Cap at the gate.
    try testing.expectError(Err.BadRequest, request.parse(
        "{\"op\":\"claim\",\"urn\":\"hw:/x/y\",\"mode\":\"exclusive\",\"holder\":1,\"deadline_ms\":99999999999}",
        testing.allocator,
    ));
}

test "happy path: control parses verb + value into ControlReq" {
    const parsed = try request.parse(
        "{\"op\":\"control\",\"urn\":\"hw:/usb/bus3/port-9/dev/if0\",\"verb\":\"set_authorized\",\"value\":\"1\"}",
        testing.allocator,
    );
    try testing.expect(parsed.req == .control);
    try testing.expectEqual(model.ControlVerb.set_authorized, parsed.req.control.verb);
    try testing.expect(std.mem.eql(u8, "1", parsed.req.control.value()));
}

test "control: unknown verb is BadRequest (closed enum at the gate)" {
    try testing.expectError(Err.BadRequest, request.parse(
        "{\"op\":\"control\",\"urn\":\"hw:/x/y\",\"verb\":\"reboot_machine\",\"value\":\"now\"}",
        testing.allocator,
    ));
}

test "control: missing value is BadRequest" {
    try testing.expectError(Err.BadRequest, request.parse(
        "{\"op\":\"control\",\"urn\":\"hw:/x/y\",\"verb\":\"set_authorized\"}",
        testing.allocator,
    ));
}

test "happy path: list_leases parses scope" {
    const parsed = try request.parse(
        "{\"op\":\"list_leases\",\"scope\":\"hw:/usb/**\"}",
        testing.allocator,
    );
    try testing.expect(parsed.req == .list_leases);
    try testing.expect(std.mem.eql(u8, parsed.req.list_leases.scope.slice(), "hw:/usb/**"));
}

// ---------------------------------------------------------------------------
// Part 13 §5.5.1 — inspect_full op parsing + depth grammar.
// ---------------------------------------------------------------------------

test "inspect_full: default depth is `all`" {
    const parsed = try request.parse(
        "{\"op\":\"inspect_full\",\"urn\":\"hw:/usb/bus3/port-9/dev/if0\"}",
        testing.allocator,
    );
    try testing.expect(parsed.req == .inspect_full);
    try testing.expectEqual(model.InspectDepth.all, parsed.req.inspect_full.depth);
}

test "inspect_full: depth=shallow parses" {
    const parsed = try request.parse(
        "{\"op\":\"inspect_full\",\"urn\":\"hw:/usb/.../if0\",\"depth\":\"shallow\"}",
        testing.allocator,
    );
    try testing.expectEqual(model.InspectDepth.shallow, parsed.req.inspect_full.depth);
}

test "inspect_full: depth=all parses" {
    const parsed = try request.parse(
        "{\"op\":\"inspect_full\",\"urn\":\"hw:/usb/.../if0\",\"depth\":\"all\"}",
        testing.allocator,
    );
    try testing.expectEqual(model.InspectDepth.all, parsed.req.inspect_full.depth);
}

test "inspect_full: unknown depth value is BadRequest" {
    try testing.expectError(Err.BadRequest, request.parse(
        "{\"op\":\"inspect_full\",\"urn\":\"hw:/usb/.../if0\",\"depth\":\"deep\"}",
        testing.allocator,
    ));
}

test "inspect_full: non-string depth value is BadRequest" {
    try testing.expectError(Err.BadRequest, request.parse(
        "{\"op\":\"inspect_full\",\"urn\":\"hw:/usb/.../if0\",\"depth\":42}",
        testing.allocator,
    ));
}

test "inspect_full: missing urn is BadRequest" {
    try testing.expectError(Err.BadRequest, request.parse(
        "{\"op\":\"inspect_full\"}",
        testing.allocator,
    ));
}

// ---------------------------------------------------------------------------
// Part 13 §5.5.2 — dump_chain op parsing.
// ---------------------------------------------------------------------------

test "dump_chain: parses with urn" {
    const parsed = try request.parse(
        "{\"op\":\"dump_chain\",\"urn\":\"hw:/usb/bus3/port-9/dev/if0\"}",
        testing.allocator,
    );
    try testing.expect(parsed.req == .dump_chain);
    try testing.expect(std.mem.eql(
        u8,
        parsed.req.dump_chain.urn.slice(),
        "hw:/usb/bus3/port-9/dev/if0",
    ));
}

test "dump_chain: missing urn is BadRequest" {
    try testing.expectError(Err.BadRequest, request.parse(
        "{\"op\":\"dump_chain\"}",
        testing.allocator,
    ));
}

test "dump_chain: non-string urn is BadRequest" {
    try testing.expectError(Err.BadRequest, request.parse(
        "{\"op\":\"dump_chain\",\"urn\":42}",
        testing.allocator,
    ));
}

// ---------------------------------------------------------------------------
// Part 13 §5.2.2 — driver_info op parsing.
// ---------------------------------------------------------------------------

test "driver_info: parses with name" {
    const parsed = try request.parse(
        "{\"op\":\"driver_info\",\"name\":\"usbhid\"}",
        testing.allocator,
    );
    try testing.expect(parsed.req == .driver_info);
    try testing.expectEqualStrings("usbhid", parsed.req.driver_info.name());
}

test "driver_info: missing name is BadRequest" {
    try testing.expectError(Err.BadRequest, request.parse(
        "{\"op\":\"driver_info\"}",
        testing.allocator,
    ));
}

test "driver_info: empty name is BadRequest" {
    try testing.expectError(Err.BadRequest, request.parse(
        "{\"op\":\"driver_info\",\"name\":\"\"}",
        testing.allocator,
    ));
}

test "driver_info: name with path-separator is BadRequest" {
    try testing.expectError(Err.BadRequest, request.parse(
        "{\"op\":\"driver_info\",\"name\":\"../etc/passwd\"}",
        testing.allocator,
    ));
}

test "driver_info: name with NUL byte is BadRequest" {
    try testing.expectError(Err.BadRequest, request.parse(
        "{\"op\":\"driver_info\",\"name\":\"usb\\u0000hid\"}",
        testing.allocator,
    ));
}

test "driver_info: name with dot-prefix rejects" {
    // `.` is not in [A-Za-z0-9_-]; a driver name like `.hidden` is
    // out-of-charset.
    try testing.expectError(Err.BadRequest, request.parse(
        "{\"op\":\"driver_info\",\"name\":\".hidden\"}",
        testing.allocator,
    ));
}

test "driver_info: name with hyphen + digit is OK" {
    const parsed = try request.parse(
        "{\"op\":\"driver_info\",\"name\":\"snd-usb-audio\"}",
        testing.allocator,
    );
    try testing.expectEqualStrings("snd-usb-audio", parsed.req.driver_info.name());
}

test "list_leases: missing scope is BadRequest" {
    try testing.expectError(Err.BadRequest, request.parse(
        "{\"op\":\"list_leases\"}",
        testing.allocator,
    ));
}

test "deadline_ms on release + raw also plumbed" {
    const rel = try request.parse(
        "{\"op\":\"release\",\"urn\":\"hw:/x/y\",\"lease_id\":7,\"deadline_ms\":250}",
        testing.allocator,
    );
    try testing.expectEqual(@as(u64, 250), rel.req.release.deadline_ms);

    const raw = try request.parse(
        "{\"op\":\"raw\",\"urn\":\"hw:/x/y\",\"lease_id\":7,\"caller\":42,\"deadline_ms\":250}",
        testing.allocator,
    );
    try testing.expectEqual(@as(u64, 250), raw.req.raw.deadline_ms);
}

// ---------------------------------------------------------------------------
// THE S1 conformance fuzzer. The whole survivability claim hangs on this:
// random bytes in, NEVER a panic out. Just BadRequest or a valid Request.
//
// Deterministic seed for reproducibility (re-runs find the same failures);
// the iteration count is modest because each iteration runs the full
// `parseFromSlice` path. Increase locally if hunting a specific class of
// input.
// ---------------------------------------------------------------------------

fn fuzzOnce(seed: u64, iterations: usize) !void {
    var prng = std.Random.DefaultPrng.init(seed);
    const rand = prng.random();

    var buf: [256]u8 = undefined;
    var i: usize = 0;
    while (i < iterations) : (i += 1) {
        // pick a random length up to MAX_LINE_BYTES; clamp to our buffer
        const len = rand.intRangeAtMost(usize, 0, buf.len);
        rand.bytes(buf[0..len]);

        // The contract: parse returns BadRequest OR a valid Request. The act
        // of switching on the result without crashing is the proof. If
        // anything panics inside parse(), the testing framework records it.
        const result = request.parse(buf[0..len], testing.allocator);
        if (result) |parsed| {
            // Whatever shape it took, the discriminant must be a known op —
            // any "valid Request" the gate produces is dispatchable.
            switch (parsed.req) {
                .list, .claim, .release, .raw, .inspect, .capabilities, .drain_events, .control, .list_leases, .dump_all, .inspect_full, .dump_chain, .subscribe, .unsubscribe, .driver_info, .audit_tail, .state_snapshot, .revoke, .attenuate, .recover, .open_shm_channel, .drain_shm_channel, .start_shm_drain, .reconcile, .irq_scan => {},
            }
        } else |err| {
            // Only BadRequest is allowed at the gate.
            try testing.expectEqual(Err.BadRequest, err);
        }
    }
}

test "S1 fuzz: 4 000 random byte sequences never panic, never escape BadRequest" {
    try fuzzOnce(0xDEADBEEF, 4000);
}

test "S1 fuzz: a different seed never panics either" {
    try fuzzOnce(0x1234_5678_ABCD_EF00, 1000);
}

test "S1 fuzz: structured-garbage seed — bytes biased toward JSON characters" {
    // Bias inputs toward characters that often appear in JSON, to exercise
    // more of the parser's state machine.
    var prng = std.Random.DefaultPrng.init(0xCAFEBABE);
    const rand = prng.random();
    const alphabet = "{}[],:\"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_/.- \n\t";

    var buf: [256]u8 = undefined;
    var i: usize = 0;
    while (i < 2000) : (i += 1) {
        const len = rand.intRangeAtMost(usize, 0, buf.len);
        for (buf[0..len]) |*b| b.* = alphabet[rand.intRangeLessThan(usize, 0, alphabet.len)];

        const result = request.parse(buf[0..len], testing.allocator);
        if (result) |parsed| {
            switch (parsed.req) {
                .list, .claim, .release, .raw, .inspect, .capabilities, .drain_events, .control, .list_leases, .dump_all, .inspect_full, .dump_chain, .subscribe, .unsubscribe, .driver_info, .audit_tail, .state_snapshot, .revoke, .attenuate, .recover, .open_shm_channel, .drain_shm_channel, .start_shm_drain, .reconcile, .irq_scan => {},
            }
        } else |err| {
            try testing.expectEqual(Err.BadRequest, err);
        }
    }
}
