//! EDNS(0) OPT pseudo-record (RFC 6891) and the Client Subnet option
//! (RFC 7871). Pure: no allocation, no `std.Io` beyond writing encoded bytes
//! to a caller's writer.
//!
//! OPT reuses two record fields for other purposes: CLASS carries the
//! requestor's UDP payload size and TTL carries the extended RCODE, the EDNS
//! version and the DO bit. `record.Record` therefore keeps both as plain
//! integers, and this module reinterprets them.

const std = @import("std");
const types = @import("types.zig");
const record = @import("record.zig");
const question = @import("question.zig");
const packet_mod = @import("packet.zig");
const Writer = std.Io.Writer;

/// The EDNS Client Subnet option code (RFC 7871 §6).
pub const ecs_option_code: u16 = 8;

pub const OptRecord = struct {
    udp_payload_size: u16,
    extended_rcode: u8,
    version: u8,
    do_bit: bool,
    /// The option list, as a span into the packet the OPT record came from.
    options: record.RdataSpan,
};

pub const ParseError = error{
    /// The record is not a well-formed OPT: wrong type, or an owner name other
    /// than the root, which RFC 6891 §6.1.2 requires.
    NotOpt,
    /// An option header or body runs past the end of the option list.
    BadOption,
};

/// Reinterprets an already-parsed record as an OPT record and checks that its
/// option list is structurally sound, so a returned `OptRecord` always
/// iterates without error.
pub fn parseOpt(packet: []const u8, rec: record.Record) ParseError!OptRecord {
    if (rec.rtype != .opt) return error.NotOpt;
    if (!rec.name.isRoot()) return error.NotOpt;

    const opt: OptRecord = .{
        .udp_payload_size = rec.class,
        .extended_rcode = @truncate(rec.ttl >> 24),
        .version = @truncate(rec.ttl >> 16),
        .do_bit = rec.ttl & 0x8000 != 0,
        .options = rec.rdata,
    };

    var it = options(packet, opt);
    while (try it.next()) |_| {}
    return opt;
}

pub const Option = struct {
    code: u16,
    data: []const u8,
};

pub const OptionIterator = struct {
    bytes: []const u8,
    pos: usize,

    pub fn next(self: *OptionIterator) error{BadOption}!?Option {
        if (self.pos == self.bytes.len) return null;
        if (self.pos + 4 > self.bytes.len) return error.BadOption;
        const code = std.mem.readInt(u16, self.bytes[self.pos..][0..2], .big);
        const len: usize = std.mem.readInt(u16, self.bytes[self.pos + 2 ..][0..2], .big);
        const data_start = self.pos + 4;
        if (data_start + len > self.bytes.len) return error.BadOption;
        self.pos = data_start + len;
        return .{ .code = code, .data = self.bytes[data_start..][0..len] };
    }
};

pub fn options(packet: []const u8, opt: OptRecord) OptionIterator {
    return .{ .bytes = opt.options.slice(packet), .pos = 0 };
}

/// Returns the first option carrying `code`, or null. An `OptRecord` from
/// `parseOpt` cannot fail here, but the error stays visible so that a
/// hand-built `OptRecord` cannot smuggle a malformed list past this.
pub fn findOption(packet: []const u8, opt: OptRecord, code: u16) error{BadOption}!?Option {
    var it = options(packet, opt);
    while (try it.next()) |o| {
        if (o.code == code) return o;
    }
    return null;
}

/// Writes the OPT record: root owner name, type OPT, the payload size in
/// CLASS, the flags in TTL, then the option list verbatim.
pub fn encodeOpt(opt: OptRecord, options_bytes: []const u8, w: *Writer) (Writer.Error || error{OptionsTooLong})!void {
    if (options_bytes.len > std.math.maxInt(u16)) return error.OptionsTooLong;
    try w.writeByte(0);
    try w.writeInt(u16, @intFromEnum(types.Type.opt), .big);
    try w.writeInt(u16, opt.udp_payload_size, .big);
    try w.writeInt(u32, ttlFrom(opt), .big);
    try w.writeInt(u16, @intCast(options_bytes.len), .big);
    try w.writeAll(options_bytes);
}

fn ttlFrom(opt: OptRecord) u32 {
    return (@as(u32, opt.extended_rcode) << 24) |
        (@as(u32, opt.version) << 16) |
        (@as(u32, @intFromBool(opt.do_bit)) << 15);
}

pub const StripResult = union(enum) {
    /// The query needs no rewrite and nothing was written to `out`.
    unchanged,
    /// The rewritten query, a prefix of `out`.
    rewritten: []u8,
};

/// Byte offset of RDLENGTH inside an OPT record as `encodeOpt` writes it: a
/// one-byte root owner name, TYPE, CLASS and TTL come first.
const opt_rdlength_offset = 1 + 2 + 2 + 4;

/// Rebuilds `query` into `out` without its ECS option, so that a client's
/// subnet never reaches the upstream resolver (PLAN §6.1). Every byte outside
/// the OPT record is copied verbatim — ID, flags, all four section counts, the
/// question and any record beside the OPT — and the OPT keeps its payload size,
/// its flags and every option other than code 8.
///
/// `pkt` and `opt` are the caller's already-parsed views of `query`, and `out`
/// must not overlap `query`.
///
/// One byte pattern does not survive the rewrite: a compression pointer in a
/// record that follows the OPT and targets a byte inside the OPT's option list.
/// Removing an option shifts those bytes, so the pointer then decodes to
/// something else. RFC 1035 §4.1.4 only ever points a name at an earlier *name*,
/// so such a pointer is malformed to begin with, and the upstream resolver
/// answers the rewritten query with FORMERR instead of the original's answer.
pub fn stripEcs(
    query: []const u8,
    pkt: packet_mod.Packet,
    opt: OptRecord,
    out: []u8,
) error{ BadOption, Overflow }!StripResult {
    std.debug.assert(query.ptr == pkt.bytes.ptr);
    std.debug.assert(query.len == pkt.bytes.len);

    // The rebuild reads `query` while it writes `out`, so an overlap would let
    // it consume bytes it has already overwritten. The two buffers must be
    // disjoint, and the caller finds that out here rather than in the packet it
    // sends upstream.
    std.debug.assert(@intFromPtr(out.ptr) + out.len <= @intFromPtr(query.ptr) or
        @intFromPtr(query.ptr) + query.len <= @intFromPtr(out.ptr));

    const opt_start = (try optRecordStart(pkt, opt)) orelse return .unchanged;
    if ((try findOption(query, opt, ecs_option_code)) == null) return .unchanged;

    var w = Writer.fixed(out);
    w.writeAll(query[0..opt_start]) catch return error.Overflow;
    encodeOpt(opt, &.{}, &w) catch |err| switch (err) {
        error.OptionsTooLong => unreachable, // the list written here is empty
        error.WriteFailed => return error.Overflow,
    };

    // RDLENGTH is only known once the list has been filtered, and the surviving
    // options are not contiguous in `query`, so `encodeOpt` writes a placeholder
    // and the options stream in behind it.
    var kept_len: usize = 0;
    var it = options(query, opt);
    while (try it.next()) |o| {
        if (o.code == ecs_option_code) continue;
        var option_header: [4]u8 = undefined;
        std.mem.writeInt(u16, option_header[0..2], o.code, .big);
        std.mem.writeInt(u16, option_header[2..4], @intCast(o.data.len), .big);
        w.writeAll(&option_header) catch return error.Overflow;
        w.writeAll(o.data) catch return error.Overflow;
        kept_len += option_header.len + o.data.len;
    }

    w.writeAll(query[opt.options.offset + opt.options.len ..]) catch return error.Overflow;

    const message = w.buffered();
    std.mem.writeInt(u16, message[opt_start + opt_rdlength_offset ..][0..2], @intCast(kept_len), .big);
    return .{ .rewritten = message };
}

/// Where the record holding `opt` begins, or null when `pkt` has no such
/// record. A record's start offset is not part of a parsed view — only a walk
/// of every section before it reaches that byte — so this repeats the walk
/// `packet.parse` already did.
///
/// The match is on the whole RDATA span, which keeps a hand-built `OptRecord`
/// from pointing this function at a different record than the one it describes.
/// A section that will not walk lands on `error.BadOption` for the same reason:
/// a `Packet` from `packet.parse` always walks, so only a `Packet` assembled by
/// hand around unvalidated bytes gets here.
fn optRecordStart(pkt: packet_mod.Packet, opt: OptRecord) error{BadOption}!?usize {
    var pos: usize = types.header_len;
    var q: u16 = 0;
    while (q < pkt.header.qdcount) : (q += 1) {
        const parsed = question.parse(pkt.bytes, pos) catch return error.BadOption;
        pos = parsed.end;
    }

    const record_count = @as(u32, pkt.header.ancount) +
        @as(u32, pkt.header.nscount) + @as(u32, pkt.header.arcount);
    var r: u32 = 0;
    while (r < record_count) : (r += 1) {
        const parsed = record.parse(pkt.bytes, pos) catch return error.BadOption;
        if (parsed.record.rtype == .opt and
            parsed.record.rdata.offset == opt.options.offset and
            parsed.record.rdata.len == opt.options.len) return pos;
        pos = parsed.end;
    }
    return null;
}

/// The full 12-bit RCODE (RFC 6891 §6.1.3): the OPT record supplies the upper
/// eight bits, the header the lower four. Without an OPT record the value is
/// just the header's four bits.
pub fn extendedRcode(header_rcode: types.Rcode, opt: ?OptRecord) u12 {
    const low: u12 = @intFromEnum(header_rcode);
    const o = opt orelse return low;
    return (@as(u12, o.extended_rcode) << 4) | low;
}

/// The write-side pair of `extendedRcode`: splits a 12-bit RCODE into the four
/// bits the header carries and the eight the OPT record carries. A reply that
/// needs a code above 15 (BADVERS, RFC 6891 §6.1.3) has no other way to say it.
pub fn splitRcode(rcode: u12) SplitRcode {
    return .{
        .header = @enumFromInt(@as(u4, @truncate(rcode))),
        .extended = @truncate(rcode >> 4),
    };
}

pub const SplitRcode = struct {
    header: types.Rcode,
    extended: u8,
};

/// RCODE 16, BADVERS (RFC 6891 §6.1.3): the query asked for an EDNS version
/// this server does not implement.
pub const badvers: u12 = 16;

const testing = std.testing;

/// An OPT record with a 4096-byte payload size, DO set, and no options.
const opt_do = "\x00\x00\x29\x10\x00\x00\x00\x80\x00\x00\x00";

test "parseOpt reads payload size, version and the DO bit" {
    const r = try record.parse(opt_do, 0);
    const opt = try parseOpt(opt_do, r.record);
    try testing.expectEqual(@as(u16, 4096), opt.udp_payload_size);
    try testing.expectEqual(@as(u8, 0), opt.extended_rcode);
    try testing.expectEqual(@as(u8, 0), opt.version);
    try testing.expectEqual(true, opt.do_bit);
    try testing.expectEqual(@as(usize, 0), opt.options.len);
}

test "parseOpt reads a cleared DO bit and an extended rcode" {
    // TTL 0x01_00_0000: extended rcode 1, version 0, DO clear.
    const packet = "\x00\x00\x29\x02\x00\x01\x00\x00\x00\x00\x00";
    const r = try record.parse(packet, 0);
    const opt = try parseOpt(packet, r.record);
    try testing.expectEqual(@as(u16, 512), opt.udp_payload_size);
    try testing.expectEqual(@as(u8, 1), opt.extended_rcode);
    try testing.expectEqual(false, opt.do_bit);
}

test "parseOpt keeps an unknown EDNS version" {
    const packet = "\x00\x00\x29\x10\x00\x00\x01\x00\x00\x00\x00";
    const r = try record.parse(packet, 0);
    const opt = try parseOpt(packet, r.record);
    try testing.expectEqual(@as(u8, 1), opt.version);
}

test "parseOpt rejects the wrong type and a non-root owner name" {
    const not_opt = "\x00\x00\x01\x00\x01\x00\x00\x00\x0a\x00\x04\x01\x02\x03\x04";
    const r = try record.parse(not_opt, 0);
    try testing.expectError(error.NotOpt, parseOpt(not_opt, r.record));

    const named = "\x03com\x00\x00\x29\x10\x00\x00\x00\x00\x00\x00\x00";
    const rn = try record.parse(named, 0);
    try testing.expectEqual(types.Type.opt, rn.record.rtype);
    try testing.expectError(error.NotOpt, parseOpt(named, rn.record));
}

test "option iterator yields every option" {
    // Two options: ECS with 7 bytes, code 12 (padding) with 2 bytes.
    const packet = "\x00\x00\x29\x10\x00\x00\x00\x80\x00\x00\x11" ++
        "\x00\x08\x00\x07\x00\x01\x18\x00\xc0\x00\x02" ++
        "\x00\x0c\x00\x02\x00\x00";
    const r = try record.parse(packet, 0);
    const opt = try parseOpt(packet, r.record);
    try testing.expectEqual(@as(usize, 17), opt.options.len);

    var it = options(packet, opt);
    const first = (try it.next()).?;
    try testing.expectEqual(ecs_option_code, first.code);
    try testing.expectEqualSlices(u8, "\x00\x01\x18\x00\xc0\x00\x02", first.data);

    const second = (try it.next()).?;
    try testing.expectEqual(@as(u16, 12), second.code);
    try testing.expectEqualSlices(u8, "\x00\x00", second.data);

    try testing.expectEqual(@as(?Option, null), try it.next());

    const found = (try findOption(packet, opt, ecs_option_code)).?;
    try testing.expectEqualSlices(u8, first.data, found.data);
    try testing.expectEqual(@as(?Option, null), try findOption(packet, opt, 99));
}

test "option iterator accepts a zero-length option body" {
    const packet = "\x00\x00\x29\x10\x00\x00\x00\x00\x00\x00\x04\x00\x0c\x00\x00";
    const r = try record.parse(packet, 0);
    const opt = try parseOpt(packet, r.record);
    var it = options(packet, opt);
    const only = (try it.next()).?;
    try testing.expectEqual(@as(u16, 12), only.code);
    try testing.expectEqual(@as(usize, 0), only.data.len);
    try testing.expectEqual(@as(?Option, null), try it.next());
}

test "parseOpt rejects a malformed option list" {
    // Option length claims 8 bytes but only 4 follow.
    const overrun = "\x00\x00\x29\x10\x00\x00\x00\x00\x00\x00\x08" ++
        "\x00\x08\x00\x08\x00\x01\x18\x00";
    const ro = try record.parse(overrun, 0);
    try testing.expectError(error.BadOption, parseOpt(overrun, ro.record));

    // A trailing partial option header (three bytes, not four).
    const partial = "\x00\x00\x29\x10\x00\x00\x00\x00\x00\x00\x03\x00\x08\x00";
    const rp = try record.parse(partial, 0);
    try testing.expectError(error.BadOption, parseOpt(partial, rp.record));
}

test "encodeOpt round-trips through record parse and parseOpt" {
    const original: OptRecord = .{
        .udp_payload_size = 1232,
        .extended_rcode = 0x10,
        .version = 0,
        .do_bit = true,
        .options = .{ .offset = 0, .len = 0 },
    };
    const options_bytes = "\x00\x08\x00\x07\x00\x01\x18\x00\xc0\x00\x02";

    var buf: [64]u8 = undefined;
    var w = Writer.fixed(&buf);
    try encodeOpt(original, options_bytes, &w);
    const bytes = w.buffered();

    const r = try record.parse(bytes, 0);
    try testing.expectEqual(@as(usize, bytes.len), r.end);
    const opt = try parseOpt(bytes, r.record);
    try testing.expectEqual(original.udp_payload_size, opt.udp_payload_size);
    try testing.expectEqual(original.extended_rcode, opt.extended_rcode);
    try testing.expectEqual(original.version, opt.version);
    try testing.expectEqual(original.do_bit, opt.do_bit);
    try testing.expectEqualSlices(u8, options_bytes, opt.options.slice(bytes));

    try testing.expectEqualSlices(
        u8,
        "\x00\x01\x18\x00\xc0\x00\x02",
        (try findOption(bytes, opt, ecs_option_code)).?.data,
    );
}

test "encodeOpt round-trips both DO states" {
    for ([_]bool{ false, true }) |do_bit| {
        const opt: OptRecord = .{
            .udp_payload_size = 4096,
            .extended_rcode = 0,
            .version = 0,
            .do_bit = do_bit,
            .options = .{ .offset = 0, .len = 0 },
        };
        var buf: [32]u8 = undefined;
        var w = Writer.fixed(&buf);
        try encodeOpt(opt, "", &w);
        const bytes = w.buffered();
        try testing.expectEqualSlices(u8, opt_do[0..3], bytes[0..3]);
        const r = try record.parse(bytes, 0);
        try testing.expectEqual(do_bit, (try parseOpt(bytes, r.record)).do_bit);
    }
}

test "encodeOpt leaves the Z bits clear" {
    const opt: OptRecord = .{
        .udp_payload_size = 512,
        .extended_rcode = 0,
        .version = 0,
        .do_bit = true,
        .options = .{ .offset = 0, .len = 0 },
    };
    try testing.expectEqual(@as(u32, 0x0000_8000), ttlFrom(opt));
}

test "encodeOpt reports a short buffer" {
    const opt: OptRecord = .{
        .udp_payload_size = 512,
        .extended_rcode = 0,
        .version = 0,
        .do_bit = false,
        .options = .{ .offset = 0, .len = 0 },
    };
    var buf: [8]u8 = undefined;
    var w = Writer.fixed(&buf);
    try testing.expectError(error.WriteFailed, encodeOpt(opt, "", &w));
}

/// A query for example.com A whose OPT record carries an ECS option for
/// 192.0.2.0/24 followed by a two-byte padding option, with DO set and a
/// 4096-byte payload size.
const ecs_query =
    "\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x01" ++ // header
    "\x07example\x03com\x00\x00\x01\x00\x01" ++ // question
    "\x00\x00\x29\x10\x00\x00\x00\x80\x00\x00\x11" ++ // OPT, 17 rdata bytes
    "\x00\x08\x00\x07\x00\x01\x18\x00\xc0\x00\x02" ++ // ECS
    "\x00\x0c\x00\x02\x00\x00"; // padding

/// `ecs_query` as `stripEcs` must rebuild it.
const ecs_query_stripped =
    "\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x01" ++
    "\x07example\x03com\x00\x00\x01\x00\x01" ++
    "\x00\x00\x29\x10\x00\x00\x00\x80\x00\x00\x06" ++
    "\x00\x0c\x00\x02\x00\x00";

const ParsedQuery = struct { pkt: packet_mod.Packet, opt: OptRecord };

fn parseQuery(bytes: []const u8) !ParsedQuery {
    const p = try packet_mod.parse(bytes);
    return .{ .pkt = p, .opt = try parseOpt(bytes, packet_mod.findOptRecord(p).?) };
}

fn expectUnchanged(result: StripResult) !void {
    switch (result) {
        .unchanged => {},
        .rewritten => return error.TestExpectedUnchanged,
    }
}

test "stripEcs rebuilds the query without the ECS option" {
    const parsed = try parseQuery(ecs_query);
    var out: [512]u8 = undefined;
    const result = try stripEcs(ecs_query, parsed.pkt, parsed.opt, &out);
    const rewritten = result.rewritten;
    try testing.expectEqualSlices(u8, ecs_query_stripped, rewritten);

    const p = try packet_mod.parse(rewritten);
    try testing.expectEqual(@as(u16, 0x1234), p.header.id);
    try testing.expectEqual(@as(u16, 1), p.header.qdcount);
    try testing.expectEqual(@as(u16, 1), p.header.arcount);

    const opt = try parseOpt(rewritten, packet_mod.findOptRecord(p).?);
    try testing.expectEqual(@as(u16, 4096), opt.udp_payload_size);
    try testing.expectEqual(true, opt.do_bit);
    try testing.expectEqual(@as(?Option, null), try findOption(rewritten, opt, ecs_option_code));
    try testing.expectEqualSlices(u8, "\x00\x00", (try findOption(rewritten, opt, 12)).?.data);
}

test "stripEcs reports a query with nothing to strip as unchanged" {
    const parsed = try parseQuery(ecs_query_stripped);
    var out: [512]u8 = undefined;
    try expectUnchanged(try stripEcs(ecs_query_stripped, parsed.pkt, parsed.opt, &out));

    // No OPT record at all: the caller still holds an `OptRecord`, so the
    // absence has to be found by the walk rather than assumed.
    const no_opt = "\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00" ++
        "\x07example\x03com\x00\x00\x01\x00\x01";
    const p = try packet_mod.parse(no_opt);
    try testing.expectEqual(@as(?record.Record, null), packet_mod.findOptRecord(p));
    const absent: OptRecord = .{
        .udp_payload_size = 4096,
        .extended_rcode = 0,
        .version = 0,
        .do_bit = false,
        .options = .{ .offset = 0, .len = 0 },
    };
    try expectUnchanged(try stripEcs(no_opt, p, absent, &out));
}

test "stripEcs keeps a record that follows the OPT" {
    const head = "\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x02" ++
        "\x07example\x03com\x00\x00\x01\x00\x01";
    const trailing = "\x00\x00\x01\x00\x01\x00\x00\x00\x0a\x00\x04\x01\x02\x03\x04";
    const query = head ++
        "\x00\x00\x29\x10\x00\x00\x00\x80\x00\x00\x0b" ++
        "\x00\x08\x00\x07\x00\x01\x18\x00\xc0\x00\x02" ++
        trailing;

    const parsed = try parseQuery(query);
    var out: [512]u8 = undefined;
    const rewritten = (try stripEcs(query, parsed.pkt, parsed.opt, &out)).rewritten;
    try testing.expectEqualSlices(
        u8,
        head ++ "\x00\x00\x29\x10\x00\x00\x00\x80\x00\x00\x00" ++ trailing,
        rewritten,
    );
    try testing.expectEqual(@as(u16, 2), (try packet_mod.parse(rewritten)).header.arcount);
}

test "stripEcs removes a repeated ECS option" {
    const ecs = "\x00\x08\x00\x07\x00\x01\x18\x00\xc0\x00\x02";
    const query = "\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x01" ++
        "\x07example\x03com\x00\x00\x01\x00\x01" ++
        "\x00\x00\x29\x10\x00\x00\x00\x80\x00\x00\x1c" ++
        ecs ++ "\x00\x0c\x00\x02\x00\x00" ++ ecs;

    const parsed = try parseQuery(query);
    var out: [512]u8 = undefined;
    const rewritten = (try stripEcs(query, parsed.pkt, parsed.opt, &out)).rewritten;
    try testing.expectEqualSlices(u8, ecs_query_stripped, rewritten);
}

test "stripEcs reports an output buffer one byte short" {
    const parsed = try parseQuery(ecs_query);

    var exact: [ecs_query_stripped.len]u8 = undefined;
    _ = (try stripEcs(ecs_query, parsed.pkt, parsed.opt, &exact)).rewritten;

    var short: [ecs_query_stripped.len - 1]u8 = undefined;
    try testing.expectError(error.Overflow, stripEcs(ecs_query, parsed.pkt, parsed.opt, &short));

    // Every shorter buffer fails the same way, including one that cannot even
    // hold the copied header.
    var i: usize = 0;
    while (i < short.len) : (i += 1) {
        try testing.expectError(
            error.Overflow,
            stripEcs(ecs_query, parsed.pkt, parsed.opt, short[0..i]),
        );
    }
}

test "stripEcs rejects a malformed option list" {
    // The record is well-formed; only its option list overruns, so `parseOpt`
    // is the one thing that rejects this packet.
    const query = "\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x01" ++
        "\x07example\x03com\x00\x00\x01\x00\x01" ++
        "\x00\x00\x29\x10\x00\x00\x00\x00\x00\x00\x08" ++
        "\x00\x08\x00\x08\x00\x01\x18\x00";

    const p = try packet_mod.parse(query);
    const rec = packet_mod.findOptRecord(p).?;
    try testing.expectError(error.BadOption, parseOpt(query, rec));

    const hand_built: OptRecord = .{
        .udp_payload_size = rec.class,
        .extended_rcode = 0,
        .version = 0,
        .do_bit = false,
        .options = rec.rdata,
    };

    var out: [512]u8 = undefined;
    try testing.expectError(error.BadOption, stripEcs(query, p, hand_built, &out));
}

test "encodeOpt writes RDLENGTH where stripEcs patches it" {
    const opt: OptRecord = .{
        .udp_payload_size = 4096,
        .extended_rcode = 0,
        .version = 0,
        .do_bit = true,
        .options = .{ .offset = 0, .len = 0 },
    };
    var buf: [32]u8 = undefined;
    var w = Writer.fixed(&buf);
    try encodeOpt(opt, "\x00\x0c\x00\x02\x00\x00", &w);
    const bytes = w.buffered();
    try testing.expectEqual(
        @as(u16, 6),
        std.mem.readInt(u16, bytes[opt_rdlength_offset..][0..2], .big),
    );
}

test "extendedRcode composes the twelve bits" {
    try testing.expectEqual(@as(u12, 3), extendedRcode(.nx_domain, null));

    const opt: OptRecord = .{
        .udp_payload_size = 4096,
        .extended_rcode = 1,
        .version = 0,
        .do_bit = false,
        .options = .{ .offset = 0, .len = 0 },
    };
    // Extended rcode 1 over header rcode 0 is BADVERS (16).
    try testing.expectEqual(@as(u12, 16), extendedRcode(.no_error, opt));

    var not_auth = opt;
    not_auth.extended_rcode = 0;
    try testing.expectEqual(@as(u12, 9), extendedRcode(.not_auth, not_auth));

    var high = opt;
    high.extended_rcode = 0xff;
    try testing.expectEqual(@as(u12, 0xfff), extendedRcode(@enumFromInt(15), high));
}

test "splitRcode splits BADVERS the way RFC 6891 does" {
    const split = splitRcode(badvers);
    try testing.expectEqual(types.Rcode.no_error, split.header);
    try testing.expectEqual(@as(u8, 1), split.extended);
}

test "splitRcode round-trips every twelve-bit rcode through extendedRcode" {
    var rcode: u12 = 0;
    while (true) : (rcode += 1) {
        const split = splitRcode(rcode);
        const opt: OptRecord = .{
            .udp_payload_size = 4096,
            .extended_rcode = split.extended,
            .version = 0,
            .do_bit = false,
            .options = .{ .offset = 0, .len = 0 },
        };
        try testing.expectEqual(rcode, extendedRcode(split.header, opt));
        if (rcode == std.math.maxInt(u12)) break;
    }
}
