//! Question section entries (RFC 1035 §4.1.2). Pure: no allocation, no
//! `std.Io` beyond writing encoded bytes to a caller's writer.

const std = @import("std");
const types = @import("types.zig");
const name = @import("name.zig");
const Writer = std.Io.Writer;

pub const Question = struct {
    name: name.Name,
    qtype: types.Type,
    qclass: types.Class,
};

pub const ParseError = name.ParseError;

pub const Parsed = struct {
    question: Question,
    /// Offset just past the question as it appears at `offset`; follows the
    /// same convention as `name.Parsed.end` for a compressed name.
    end: usize,
};

pub fn parse(packet: []const u8, offset: usize) ParseError!Parsed {
    const parsed_name = try name.parse(packet, offset);
    const fixed = parsed_name.end;
    if (fixed + 4 > packet.len) return error.Truncated;
    return .{
        .question = .{
            .name = parsed_name.name,
            .qtype = @enumFromInt(std.mem.readInt(u16, packet[fixed..][0..2], .big)),
            .qclass = @enumFromInt(std.mem.readInt(u16, packet[fixed + 2 ..][0..2], .big)),
        },
        .end = fixed + 4,
    };
}

pub fn encode(q: Question, w: *Writer) Writer.Error!void {
    try name.encode(q.name, w);
    try w.writeInt(u16, @intFromEnum(q.qtype), .big);
    try w.writeInt(u16, @intFromEnum(q.qclass), .big);
}

const testing = std.testing;

test "parse an uncompressed question" {
    const packet = "\x07example\x03com\x00\x00\x01\x00\x01";
    const r = try parse(packet, 0);
    try testing.expectEqualSlices(
        u8,
        (try name.fromText("example.com")).wire(),
        r.question.name.wire(),
    );
    try testing.expectEqual(types.Type.a, r.question.qtype);
    try testing.expectEqual(types.Class.in, r.question.qclass);
    try testing.expectEqual(@as(usize, 17), r.end);
}

test "parse a question whose name is compressed" {
    // "com" at offset 0, then a question at offset 5 naming "example.com"
    // through a pointer.
    const packet = "\x03com\x00" ++ "\x07example\xc0\x00\x00\x1c\x00\x01";
    const r = try parse(packet, 5);
    try testing.expectEqualSlices(
        u8,
        (try name.fromText("example.com")).wire(),
        r.question.name.wire(),
    );
    try testing.expectEqual(types.Type.aaaa, r.question.qtype);
    try testing.expectEqual(types.Class.in, r.question.qclass);
    try testing.expectEqual(@as(usize, 19), r.end);
}

test "parse keeps unknown type and class values" {
    const packet = "\x00\x12\x34\x56\x78";
    const r = try parse(packet, 0);
    try testing.expect(r.question.name.isRoot());
    try testing.expectEqual(@as(u16, 0x1234), @intFromEnum(r.question.qtype));
    try testing.expectEqual(@as(u16, 0x5678), @intFromEnum(r.question.qclass));
    try testing.expectEqual(@as(usize, 5), r.end);
}

test "parse rejects a truncated fixed field" {
    const full = "\x07example\x03com\x00\x00\x01\x00\x01";
    var i: usize = 13;
    while (i < full.len) : (i += 1) {
        try testing.expectError(error.Truncated, parse(full[0..i], 0));
    }
}

test "parse propagates a name error" {
    try testing.expectError(error.BadPointer, parse("\xc0\x00\x00\x01\x00\x01", 0));
    try testing.expectError(error.Truncated, parse("\x07exa", 0));
    try testing.expectError(error.LabelTooLong, parse("\x40abc\x00\x00\x01\x00\x01", 0));
}

test "encode round-trips" {
    const original: Question = .{
        .name = try name.fromText("www.example.com"),
        .qtype = .aaaa,
        .qclass = .in,
    };

    var buf: [512]u8 = undefined;
    var w = Writer.fixed(&buf);
    try encode(original, &w);
    const bytes = w.buffered();
    try testing.expectEqual(@as(usize, 21), bytes.len);

    const r = try parse(bytes, 0);
    try testing.expectEqualSlices(u8, original.name.wire(), r.question.name.wire());
    try testing.expectEqual(original.qtype, r.question.qtype);
    try testing.expectEqual(original.qclass, r.question.qclass);
    try testing.expectEqual(bytes.len, r.end);
}

test "encode writes big-endian fixed fields" {
    const q: Question = .{
        .name = try name.fromText("."),
        .qtype = @enumFromInt(0x1234),
        .qclass = @enumFromInt(0x5678),
    };
    var buf: [16]u8 = undefined;
    var w = Writer.fixed(&buf);
    try encode(q, &w);
    try testing.expectEqualSlices(u8, "\x00\x12\x34\x56\x78", w.buffered());
}

test "encode reports a short buffer" {
    const q: Question = .{
        .name = try name.fromText("example.com"),
        .qtype = .a,
        .qclass = .in,
    };
    var buf: [16]u8 = undefined;
    var w = Writer.fixed(&buf);
    try testing.expectError(error.WriteFailed, encode(q, &w));
}
