//! Used in conjuncation with `std.testing.fuzz` to generate values

const builtin = @import("builtin");
const std = @import("../std.zig");
const assert = std.debug.assert;
const fuzz_abi = std.Build.abi.fuzz;
const Smith = @This();

/// Null if the fuzzer is being used, in which case this struct will not be mutated.
///
/// Intended to be initialized directly.
in: ?[]const u8,

pub const Weight = fuzz_abi.Weight;

fn intUid(hash: u32) fuzz_abi.Uid {
    @disableInstrumentation();
    return @bitCast(hash << 1);
}

fn bytesUid(hash: u32) fuzz_abi.Uid {
    @disableInstrumentation();
    return @bitCast(hash | 1);
}

fn Backing(T: type) type {
    return @Int(.unsigned, @bitSizeOf(T));
}

fn toExcessK(T: type, x: T) Backing(T) {
    return @bitCast(x -% std.math.minInt(T));
}

fn fromExcessK(T: type, x: Backing(T)) T {
    return @as(T, @bitCast(x)) +% std.math.minInt(T);
}

fn enumFieldLessThan(_: void, a: std.builtin.Type.EnumField, b: std.builtin.Type.EnumField) bool {
    return a.value < b.value;
}

/// Returns an array of weights containing each possible value of `T`.
//
// `inline` to propogate the `comptime`ness of the result
pub inline fn baselineWeights(T: type) []const Weight {
    return comptime switch (@typeInfo(T)) {
        .bool, .int, .float => i: {
            // Reject types that don't have a fixed bitsize (esp. usize)
            // since they are not gauraunteed to fit in a u64 across targets.
            if (std.mem.indexOfScalar(type, &.{
                isize,      usize,
                c_char,     c_longdouble,
                c_short,    c_ushort,
                c_int,      c_uint,
                c_long,     c_ulong,
                c_longlong, c_ulonglong,
            }, T) != null) {
                @compileError("type does not have a fixed bitsize: " ++ @typeName(T));
            }
            break :i &.{.rangeAtMost(Backing(T), 0, (1 << @bitSizeOf(T)) - 1, 1)};
        },
        .@"struct" => |s| if (s.backing_integer) |B|
            baselineWeights(B)
        else
            @compileError("non-packed structs cannot be weighted"),
        .@"union" => |u| if (u.layout == .@"packed")
            baselineWeights(Backing(T))
        else
            @compileError("non-packed unions cannot be weighted"),
        .@"enum" => |e| if (!e.is_exhaustive)
            baselineWeights(e.tag_type)
        else if (e.fields.len == 0)
            // Cannot be included in below branch due to `log2_int_ceil`
            @compileError("exhaustive zero-field enums cannot be weighted")
        else e: {
            @setEvalBranchQuota(@intCast(4 * e.fields.len *
                std.math.log2_int_ceil(usize, e.fields.len)));

            var sorted_fields = e.fields[0..e.fields.len].*;
            std.mem.sortUnstable(std.builtin.Type.EnumField, &sorted_fields, {}, enumFieldLessThan);

            var weights: []const Weight = &.{};
            var seq_first: u64 = sorted_fields[0].value;
            for (sorted_fields[0 .. sorted_fields.len - 1], sorted_fields[1..]) |prev, field| {
                if (field.value != prev.value + 1) {
                    weights = weights ++ .{Weight.rangeAtMost(u64, seq_first, prev.value, 1)};
                    seq_first = field.value;
                }
            }
            weights = weights ++ .{Weight.rangeAtMost(
                u64,
                seq_first,
                sorted_fields[sorted_fields.len - 1].value,
                1,
            )};

            break :e weights;
        },
        else => @compileError("unexpected type: " ++ @typeName(T)),
    };
}

test baselineWeights {
    try std.testing.expectEqualSlices(
        Weight,
        &.{.rangeAtMost(bool, false, true, 1)},
        baselineWeights(bool),
    );
    try std.testing.expectEqualSlices(
        Weight,
        &.{.rangeAtMost(u4, 0, 15, 1)},
        baselineWeights(u4),
    );
    try std.testing.expectEqualSlices(
        Weight,
        &.{.rangeAtMost(u4, 0, 15, 1)},
        baselineWeights(i4),
    );
    try std.testing.expectEqualSlices(
        Weight,
        &.{.rangeAtMost(u16, 0, 0xffff, 1)},
        baselineWeights(f16),
    );
    try std.testing.expectEqualSlices(
        Weight,
        &.{.rangeAtMost(u4, 0, 15, 1)},
        baselineWeights(packed struct(u4) { _: u4 }),
    );
    try std.testing.expectEqualSlices(
        Weight,
        &.{.rangeAtMost(u4, 0, 15, 1)},
        baselineWeights(packed union { _: u4 }),
    );
    try std.testing.expectEqualSlices(
        Weight,
        &.{.rangeAtMost(u4, 0, 15, 1)},
        baselineWeights(enum(u4) { _ }),
    );
    try std.testing.expectEqualSlices(Weight, &.{
        .rangeAtMost(u4, 0, 1, 1),
        .value(u4, 3, 1),
        .value(u4, 5, 1),
        .rangeAtMost(u4, 8, 10, 1),
    }, baselineWeights(enum(u4) {
        a = 1,
        b = 5,
        c = 8,
        d = 3,
        e = 0,
        f = 9,
        g = 10,
    }));
}

fn valueFromInt(T: anytype, int: Backing(T)) T {
    @disableInstrumentation();
    return switch (@typeInfo(T)) {
        .@"enum" => @enumFromInt(int),
        else => @bitCast(int),
    };
}

fn checkWeights(weights: []const Weight, max_incl: u64) void {
    @disableInstrumentation();
    const w0 = weights[0]; // Sum of weights is zero
    assert(w0.weight != 0);
    assert(w0.max <= max_incl);

    var incl_sum: u64 = (w0.max - w0.min) * w0.weight + (w0.weight - 1); // Sum of weights greater than 2^64
    for (weights[1..]) |w| {
        assert(w.weight != 0);
        assert(w.max <= max_incl);
        // This addition will not overflow except with an illegal combination of weights since
        // the exclusive sum must be at least one so a span of all values is impossible.
        incl_sum += (w.max - w.min + 1) * w.weight; // Sum of weights greater than 2^64
    }
}

// `inline` to propogate callee's unique return address
inline fn firstHash() u32 {
    return @truncate(std.hash.int(@returnAddress()));
}

// `noinline` to capture a unique return address
pub noinline fn value(s: *Smith, T: type) T {
    @disableInstrumentation();
    return s.valueWithHash(T, firstHash());
}

// `noinline` to capture a unique return address
pub noinline fn valueWeighted(s: *Smith, T: type, weights: []const Weight) T {
    @disableInstrumentation();
    return s.valueWeightedWithHash(T, weights, firstHash());
}

// `noinline` to capture a unique return address
pub noinline fn valueRangeAtMost(s: *Smith, T: type, at_least: T, at_most: T) T {
    @disableInstrumentation();
    return s.valueRangeAtMostWithHash(T, at_least, at_most, firstHash());
}

// `noinline` to capture a unique return address
pub noinline fn valueRangeLessThan(s: *Smith, T: type, at_least: T, less_than: T) T {
    @disableInstrumentation();
    return s.valueRangeLessThanWithHash(T, at_least, less_than, firstHash());
}

/// It is asserted `len` is nonzero.
/// It is asserted `len` fits within 64 bits.
//
// `noinline` to capture a unique return address
pub noinline fn index(s: *Smith, len: usize) usize {
    @disableInstrumentation();
    return s.indexWithHash(len, firstHash());
}

/// It is asserted that the weight of `false` is non-zero.
/// It is asserted that the weight of `true` is non-zero.
//
// `noinline` to capture a unique return address
pub noinline fn boolWeighted(s: *Smith, false_weight: u64, true_weight: u64) bool {
    @disableInstrumentation();
    return s.boolWeightedWithHash(false_weight, true_weight, firstHash());
}

/// This is similar to `value(bool)` however it is gauraunteed to eventually
/// return `true` and provides the fuzzer with an extra hint about the data.
//
// `noinline` to capture a unique return address
pub noinline fn eos(s: *Smith) bool {
    @disableInstrumentation();
    return s.eosWithHash(firstHash());
}

/// This is similar to `value(bool)` however it is gauraunteed to eventually
/// return `true` and provides the fuzzer with an extra hint about the data.
///
/// It is asserted that the weight of `true` is non-zero.
//
// `noinline` to capture a unique return address
pub noinline fn eosWeighted(s: *Smith, weights: []const Weight) bool {
    @disableInstrumentation();
    return s.eosWeightedWithHash(weights, firstHash());
}

/// This is similar to `value(bool)` however it is gauraunteed to eventually
/// return `true` and provides the fuzzer with an extra hint about the data.
///
/// It is asserted that the weight of `false` is non-zero.
/// It is asserted that the weight of `true` is non-zero.
//
// `noinline` to capture a unique return address
pub noinline fn eosWeightedSimple(s: *Smith, false_weight: u64, true_weight: u64) bool {
    @disableInstrumentation();
    return s.eosWeightedSimpleWithHash(false_weight, true_weight, firstHash());
}

// `noinline` to capture a unique return address
pub noinline fn bytes(s: *Smith, out: []u8) void {
    @disableInstrumentation();
    return s.bytesWithHash(out, firstHash());
}

// `noinline` to capture a unique return address
pub noinline fn bytesWeighted(s: *Smith, out: []u8, weights: []const Weight) void {
    @disableInstrumentation();
    return s.bytesWeightedWithHash(out, weights, firstHash());
}

/// Returns the length of the filled slice
///
/// It is asserted that `buf.len` fits within a u32
// `noinline` to capture a unique return address
pub noinline fn slice(s: *Smith, buf: []u8) u32 {
    @disableInstrumentation();
    return s.sliceWithHash(buf, firstHash());
}

/// Returns the length of the filled slice
///
/// It is asserted that `buf.len` fits within a u32
//
// `noinline` to capture a unique return address
pub noinline fn sliceWeightedBytes(s: *Smith, buf: []u8, byte_weights: []const Weight) u32 {
    @disableInstrumentation();
    return s.sliceWeightedBytesWithHash(buf, byte_weights, firstHash());
}

/// Returns the length of the filled slice
///
/// It is asserted that `buf.len` fits within a u32
//
// `noinline` to capture a unique return address
pub noinline fn sliceWeighted(
    s: *Smith,
    buf: []u8,
    len_weights: []const Weight,
    byte_weights: []const Weight,
) u32 {
    @disableInstrumentation();
    return s.sliceWeightedWithHash(buf, len_weights, byte_weights, firstHash());
}

fn weightsContain(int: u64, weights: []const Weight) bool {
    @disableInstrumentation();
    var contains: bool = false;
    for (weights) |w| {
        contains |= w.min <= int and int <= w.max;
    }
    return contains;
}

/// Asserts `T` can be a member of a packed type
//
// `inline` to propogate the `comptime`ness of the result
inline fn allBitPatternsValid(T: type) bool {
    return comptime switch (@typeInfo(T)) {
        .void, .bool, .int, .float => true,
        inline .@"struct", .@"union" => |c| c.layout == .@"packed" and for (c.fields) |f| {
            if (!allBitPatternsValid(f.type)) break false;
        } else true,
        .@"enum" => |e| !e.is_exhaustive,
        else => unreachable,
    };
}

test allBitPatternsValid {
    try std.testing.expect(allBitPatternsValid(packed struct {
        a: void,
        b: u8,
        c: f16,
        d: packed union {
            a: u16,
            b: i16,
            c: f16,
        },
        e: enum(u4) { _ },
    }));
    try std.testing.expect(!allBitPatternsValid(packed union {
        a: i4,
        b: enum(u4) { a },
    }));
}

fn UnionTagWithoutUninitializable(T: type) type {
    const u = @typeInfo(T).@"union";
    const Tag = u.tag_type orelse @compileError("union must have tag");
    const e = @typeInfo(Tag).@"enum";
    var field_names: [e.fields.len][]const u8 = undefined;
    var field_values: [e.fields.len]e.tag_type = undefined;
    var n_fields = 0;
    for (u.fields) |f| {
        switch (f.type) {
            noreturn => continue,
            else => {},
        }
        field_names[n_fields] = f.name;
        field_values[n_fields] = @intFromEnum(@field(Tag, f.name));
        n_fields += 1;
    }
    return @Enum(e.tag_type, .exhaustive, field_names[0..n_fields], field_values[0..n_fields]);
}

pub fn valueWithHash(s: *Smith, T: type, hash: u32) T {
    @disableInstrumentation();
    return switch (@typeInfo(T)) {
        .void => {},
        .bool, .int, .float => full: {
            var int: Backing(T) = 0;
            comptime var biti = 0;
            var rhash = hash; // 'running' hash
            inline while (biti < @bitSizeOf(T)) {
                const n = @min(@bitSizeOf(T) - biti, 64);
                const P = @Int(.unsigned, n);
                int |= @as(
                    @TypeOf(int),
                    s.valueWeightedWithHash(P, baselineWeights(P), rhash),
                ) << biti;
                biti += n;
                rhash = std.hash.int(rhash);
            }
            break :full @bitCast(int);
        },
        .@"enum" => |e| if (e.is_exhaustive) v: {
            if (@bitSizeOf(e.tag_type) <= 64) {
                break :v s.valueWeightedWithHash(T, baselineWeights(T), hash);
            }
            break :v std.enums.fromInt(T, s.valueWithHash(e.tag_type, hash)) orelse
                @enumFromInt(e.fields[0].value);
        } else @enumFromInt(s.valueWithHash(e.tag_type, hash)),
        .optional => |o| if (s.valueWithHash(bool, hash))
            null
        else
            s.valueWithHash(o.child, std.hash.int(hash)),
        inline .array, .vector => |a| arr: {
            var arr: [a.len]a.child = undefined; // `T` cannot be used due to the vector case
            if (a.child != u8) {
                for (&arr) |*v| {
                    v.* = s.valueWithHash(a.child, hash);
                }
            } else {
                s.bytesWithHash(&arr, hash);
            }
            break :arr arr;
        },
        .@"struct" => |st| if (!allBitPatternsValid(T)) v: {
            var v: T = undefined;
            var rhash = hash;
            inline for (st.fields) |f| {
                // rhash is incremented in the call so our rhash state is not reused (e.g. with
                // two nested structs. note that xor cannot work for this case as the bit would
                // be flipped back here)
                @field(v, f.name) = s.valueWithHash(f.type, rhash +% 1);
                rhash = std.hash.int(rhash);
            }
            break :v v;
        } else @bitCast(s.valueWithHash(st.backing_integer.?, hash)),
        .@"union" => if (!allBitPatternsValid(T))
            switch (s.valueWithHash(
                UnionTagWithoutUninitializable(T),
                // hash is incremented in the call so our hash state is not reused for below
                std.hash.int(hash +% 1),
            )) {
                inline else => |t| @unionInit(
                    T,
                    @tagName(t),
                    s.valueWithHash(@FieldType(T, @tagName(t)), hash),
                ),
            }
        else
            @bitCast(s.valueWithHash(Backing(T), hash)),
        else => @compileError("unexpected type '" ++ @typeName(T) ++ "'"),
    };
}

pub fn valueWeightedWithHash(s: *Smith, T: type, weights: []const Weight, hash: u32) T {
    @disableInstrumentation();
    checkWeights(weights, (1 << @bitSizeOf(T)) - 1);
    return valueFromInt(T, @intCast(s.valueWeightedWithHashInner(weights, hash)));
}

fn valueWeightedWithHashInner(s: *Smith, weights: []const Weight, hash: u32) u64 {
    @disableInstrumentation();
    return if (s.in) |*in| int: {
        if (in.len < 8) {
            @branchHint(.unlikely);
            in.* = &.{};
            break :int weights[0].min;
        }
        const int = std.mem.readInt(u64, in.*[0..8], .little);
        in.* = in.*[8..];
        break :int if (weightsContain(int, weights)) int else weights[0].min;
    } else if (builtin.fuzz) int: {
        @branchHint(.likely);
        break :int fuzz_abi.fuzzer_int(intUid(hash), .fromSlice(weights));
    } else unreachable;
}

pub fn valueRangeAtMostWithHash(s: *Smith, T: type, at_least: T, at_most: T, hash: u32) T {
    @disableInstrumentation();
    if (@typeInfo(T) == .int and @typeInfo(T).int.signedness == .signed) {
        return fromExcessK(T, s.valueRangeAtMostWithHash(
            Backing(T),
            toExcessK(T, at_least),
            toExcessK(T, at_most),
            hash,
        ));
    }
    return s.valueWeightedWithHash(T, &.{.rangeAtMost(T, at_least, at_most, 1)}, hash);
}

pub fn valueRangeLessThanWithHash(s: *Smith, T: type, at_least: T, less_than: T, hash: u32) T {
    @disableInstrumentation();
    if (@typeInfo(T) == .int and @typeInfo(T).int.signedness == .signed) {
        return fromExcessK(T, s.valueRangeLessThanWithHash(
            Backing(T),
            toExcessK(T, at_least),
            toExcessK(T, less_than),
            hash,
        ));
    }
    return s.valueWeightedWithHash(T, &.{.rangeLessThan(T, at_least, less_than, 1)}, hash);
}

/// It is asserted `len` is nonzero.
/// It is asserted `len` fits within 64 bits.
pub fn indexWithHash(s: *Smith, len: usize, hash: u32) usize {
    @disableInstrumentation();
    assert(len != 0);
    return @intCast(s.valueWeightedWithHash(u64, &.{.rangeLessThan(u64, 0, @intCast(len), 1)}, hash));
}

/// It is asserted that the weight of `false` is non-zero.
/// It is asserted that the weight of `true` is non-zero.
pub fn boolWeightedWithHash(s: *Smith, false_weight: u64, true_weight: u64, hash: u32) bool {
    @disableInstrumentation();
    return s.valueWeightedWithHash(bool, &.{
        .value(bool, false, false_weight),
        .value(bool, true, true_weight),
    }, hash);
}

/// This is similar to `value(bool)` however it is gauraunteed to eventually
/// return `true` and provides the fuzzer with an extra hint about the data.
pub fn eosWithHash(s: *Smith, hash: u32) bool {
    @disableInstrumentation();
    return s.eosWeightedWithHash(baselineWeights(bool), hash);
}

/// This is similar to `value(bool)` however it is gauraunteed to eventually
/// return `true` and provides the fuzzer with an extra hint about the data.
///
/// It is asserted that the weight of `true` is non-zero.
pub fn eosWeightedWithHash(s: *Smith, weights: []const Weight, hash: u32) bool {
    @disableInstrumentation();
    checkWeights(weights, 1);
    for (weights) |w| (if (w.max == 1) break) else unreachable; // `true` must have non-zero weight

    if (s.in) |*in| {
        if (in.len == 0) {
            @branchHint(.unlikely);
            return true;
        }
        const eos_val = in.*[0] != 0;
        in.* = in.*[1..];
        return eos_val or b: {
            var only_true: bool = true;
            for (weights) |w| {
                only_true &= @as(u1, @intCast(w.min)) == 1;
            }
            break :b only_true;
        };
    } else if (builtin.fuzz) {
        @branchHint(.likely);
        return fuzz_abi.fuzzer_eos(intUid(hash), .fromSlice(weights));
    } else unreachable;
}

/// This is similar to `value(bool)` however it is gauraunteed to eventually
/// return `true` and provides the fuzzer with an extra hint about the data.
///
/// It is asserted that the weight of `false` is non-zero.
/// It is asserted that the weight of `true` is non-zero.
pub fn eosWeightedSimpleWithHash(s: *Smith, false_weight: u64, true_weight: u64, hash: u32) bool {
    @disableInstrumentation();
    return s.eosWeightedWithHash(&.{
        .value(bool, false, false_weight),
        .value(bool, true, true_weight),
    }, hash);
}

pub fn bytesWithHash(s: *Smith, out: []u8, hash: u32) void {
    @disableInstrumentation();
    return s.bytesWeightedWithHash(out, baselineWeights(u8), hash);
}

pub fn bytesWeightedWithHash(s: *Smith, out: []u8, weights: []const Weight, hash: u32) void {
    @disableInstrumentation();
    checkWeights(weights, 255);

    if (s.in) |*in| {
        var present_weights: [256]bool = @splat(false);
        for (weights) |w| {
            @memset(present_weights[@intCast(w.min)..@intCast(w.max + 1)], true);
        }
        const default: u8 = @intCast(weights[0].min);

        const copy_len = @min(out.len, in.len);
        for (in.*[0..copy_len], out[0..copy_len]) |i, *o| {
            o.* = if (present_weights[i]) i else default;
        }
        in.* = in.*[copy_len..];
        @memset(out[copy_len..], default);
    } else if (builtin.fuzz) {
        @branchHint(.likely);
        fuzz_abi.fuzzer_bytes(bytesUid(hash), .fromSlice(out), .fromSlice(weights));
    } else unreachable;
}

/// Returns the length of the filled slice
///
/// It is asserted that `buf.len` fits within a u32
pub fn sliceWithHash(s: *Smith, buf: []u8, hash: u32) u32 {
    @disableInstrumentation();
    return s.sliceWeightedBytesWithHash(buf, baselineWeights(u8), hash);
}

/// Returns the length of the filled slice
///
/// It is asserted that `buf.len` fits within a u32
pub fn sliceWeightedBytesWithHash(
    s: *Smith,
    buf: []u8,
    byte_weights: []const Weight,
    hash: u32,
) u32 {
    @disableInstrumentation();
    return s.sliceWeightedWithHash(
        buf,
        &.{.rangeAtMost(u32, 0, @intCast(buf.len), 1)},
        byte_weights,
        hash,
    );
}

/// Returns the length of the filled slice
///
/// It is asserted that `buf.len` fits within a u32
pub fn sliceWeightedWithHash(
    s: *Smith,
    buf: []u8,
    len_weights: []const Weight,
    byte_weights: []const Weight,
    hash: u32,
) u32 {
    @disableInstrumentation();
    checkWeights(byte_weights, 255);
    checkWeights(len_weights, @as(u32, @intCast(buf.len)));

    if (s.in) |*in| {
        const in_len = len: {
            if (in.len < 4) {
                @branchHint(.unlikely);
                in.* = &.{};
                break :len 0;
            }
            const len = std.mem.readInt(u32, in.*[0..4], .little);
            in.* = in.*[4..];
            break :len @min(len, in.len);
        };
        const out_len: u32 = if (weightsContain(in_len, len_weights))
            in_len
        else
            @intCast(len_weights[0].min);

        var present_weights: [256]bool = @splat(false);
        for (byte_weights) |w| {
            @memset(present_weights[@intCast(w.min)..@intCast(w.max + 1)], true);
        }
        const default: u8 = @intCast(byte_weights[0].min);

        const copy_len = @min(out_len, in_len);
        for (in.*[0..copy_len], buf[0..copy_len]) |i, *o| {
            o.* = if (present_weights[i]) i else default;
        }
        in.* = in.*[in_len..];
        @memset(buf[copy_len..], default);
        return out_len;
    } else if (builtin.fuzz) {
        @branchHint(.likely);
        return fuzz_abi.fuzzer_slice(
            bytesUid(hash),
            .fromSlice(buf),
            .fromSlice(len_weights),
            .fromSlice(byte_weights),
        );
    } else unreachable;
}

fn constructInput(comptime values: []const union(enum) {
    eos: bool,
    int: u64,
    bytes: []const u8,
    slice: []const u8,
}) []const u8 {
    const result = comptime result: {
        var result: [
            len: {
                var len = 0;
                for (values) |v| len += switch (v) {
                    .eos => 1,
                    .int => 8,
                    .bytes => |b| b.len,
                    .slice => |s| 4 + s.len,
                };
                break :len len;
            }
        ]u8 = undefined;
        var w: std.Io.Writer = .fixed(&result);

        for (values) |v| switch (v) {
            .eos => |e| w.writeByte(@intFromBool(e)) catch unreachable,
            .int => |i| w.writeInt(u64, i, .little) catch unreachable,
            .bytes => |b| w.writeAll(b) catch unreachable,
            .slice => |s| {
                w.writeInt(u32, @intCast(s.len), .little) catch unreachable;
                w.writeAll(s) catch unreachable;
            },
        };

        break :result result;
    };
    return &result;
}

test value {
    if (@import("builtin").zig_backend == .stage2_c) return error.SkipZigTest; // TODO

    const S = struct {
        v: void = {},
        b: bool = true,
        ih: u16 = 123,
        iq: u64 = 55555,
        io: u128 = (1 << 80) | (1 << 23),
        fd: f64 = std.math.pi,
        ft: f80 = std.math.e,
        eh: enum(u16) { a, _ } = @enumFromInt(999),
        eo: enum(u128) { a, b, _ } = .b,
        aw: [3]u32 = .{ 1 << 30, 1 << 20, 1 << 10 },
        vw: @Vector(3, u32) = .{ 1 << 10, 1 << 20, 1 << 30 },
        ab: [3]u8 = .{ 55, 33, 88 },
        vb: @Vector(3, u8) = .{ 22, 44, 99 },
        s: struct { q: u64 } = .{ .q = 1 },
        sz: struct {} = .{},
        sp: packed struct(u8) { a: u5, b: u3 } = .{ .a = 31, .b = 3 },
        si: packed struct(u8) { a: u5, b: enum(u3) { a, b } } = .{ .a = 15, .b = .b },
        u: union(enum(u2)) {
            a: u64,
            b: u64,
            c: noreturn,
        } = .{ .b = 777777 },
        up: packed union {
            a: u16,
            b: f16,
        } = .{ .b = std.math.phi },

        invalid: struct {
            ib: u8 = 0,
            eb: enum(u8) { a, b } = .a,
            eo: enum(u128) { a, b } = .a,
            u: union(enum(u1)) { a: noreturn, b: void } = .{ .b = {} },
        } = .{},
    };
    const s: S = .{};
    const ft_bits: u80 = @bitCast(s.ft);
    const eo_bits = @intFromEnum(s.eo);

    var smith: Smith = .{
        .in = constructInput(&.{
            // v
            .{ .int = @intFromBool(s.b) }, // b
            .{ .int = s.ih }, // ih
            .{ .int = s.iq }, // iq
            .{ .int = @truncate(s.io) }, .{ .int = @intCast(s.io >> 64) }, // io
            .{ .int = @bitCast(s.fd) }, // fd
            .{ .int = @truncate(ft_bits) }, .{ .int = @intCast(ft_bits >> 64) }, // ft
            .{ .int = @intFromEnum(s.eh) }, // eh
            .{ .int = @truncate(eo_bits) }, .{ .int = @intCast(eo_bits >> 64) }, // eo
            .{ .int = s.aw[0] }, .{ .int = s.aw[1] }, .{ .int = s.aw[2] }, // aw
            .{ .int = s.vw[0] }, .{ .int = s.vw[1] }, .{ .int = s.vw[2] }, // vw
            .{ .bytes = &s.ab }, // ab
            .{ .bytes = &@as([3]u8, s.vb) }, // vb
            .{ .int = s.s.q }, // s.q
            //sz
            .{ .int = @as(u8, @bitCast(s.sp)) }, // sp
            .{ .int = s.si.a }, .{ .int = @intFromEnum(s.si.b) }, // si
            .{ .int = @intFromEnum(s.u) }, .{ .int = s.u.b }, // u
            .{ .int = @as(u16, @bitCast(s.up)) }, // up
            // invalid values
            .{ .int = 555 }, // invalid.ib
            .{ .int = 123 }, // invalid.eb
            .{ .int = 0 }, .{ .int = 1 }, // invalid.eo
            .{ .int = 0 }, // invalid.u
        }),
    };

    try std.testing.expectEqual(s, smith.value(S));
}

test valueWeighted {
    var smith: Smith = .{
        .in = constructInput(&.{
            .{ .int = 200 },
            .{ .int = 200 },
            .{ .int = 300 },
            .{ .int = 400 },
        }),
    };

    try std.testing.expectEqual(200, smith.valueWeighted(u8, &.{.rangeAtMost(u8, 50, 200, 1)}));
    try std.testing.expectEqual(50, smith.valueWeighted(u8, &.{.rangeLessThan(u8, 50, 200, 1)}));
    const E = enum(u64) { a = 100, b = 200, c = 300 };
    try std.testing.expectEqual(E.c, smith.valueWeighted(E, baselineWeights(E)));
    try std.testing.expectEqual(E.a, smith.valueWeighted(E, baselineWeights(E)));
    try std.testing.expectEqual(12345, smith.valueWeighted(u64, &.{.value(u64, 12345, 1)}));
}

test valueRangeAtMost {
    var smith: Smith = .{
        .in = constructInput(&.{
            .{ .int = 100 },
            .{ .int = 100 },
            .{ .int = 200 },
            .{ .int = 100 },
            .{ .int = 200 },
            .{ .int = 0 },
        }),
    };
    try std.testing.expectEqual(100, smith.valueRangeAtMost(u8, 0, 250));
    try std.testing.expectEqual(100, smith.valueRangeAtMost(u8, 100, 100));
    try std.testing.expectEqual(0, smith.valueRangeAtMost(u8, 0, 100));
    try std.testing.expectEqual(100 - 128, smith.valueRangeAtMost(i8, -100, 100));
    try std.testing.expectEqual(200 - 128, smith.valueRangeAtMost(i8, -100, 100));
    try std.testing.expectEqual(-100, smith.valueRangeAtMost(i8, -100, 100));
}

test valueRangeLessThan {
    var smith: Smith = .{
        .in = constructInput(&.{
            .{ .int = 100 },
            .{ .int = 100 },
            .{ .int = 100 },
            .{ .int = 100 + 128 },
        }),
    };
    try std.testing.expectEqual(100, smith.valueRangeLessThan(u8, 0, 250));
    try std.testing.expectEqual(0, smith.valueRangeLessThan(u8, 0, 100));
    try std.testing.expectEqual(100 - 128, smith.valueRangeLessThan(i8, -100, 100));
    try std.testing.expectEqual(-100, smith.valueRangeLessThan(i8, -100, 100));
}

test eos {
    var smith: Smith = .{
        .in = constructInput(&.{
            .{ .eos = false },
            .{ .eos = true },
        }),
    };
    try std.testing.expect(!smith.eos());
    try std.testing.expect(smith.eos());
    try std.testing.expect(smith.eos());
}

test eosWeighted {
    var smith: Smith = .{ .in = constructInput(&.{.{ .eos = false }}) };
    try std.testing.expect(smith.eosWeighted(&.{.value(bool, true, std.math.maxInt(u64))}));
}

test bytes {
    var smith: Smith = .{ .in = constructInput(&.{
        .{ .bytes = "testing!" },
        .{ .bytes = "ab" },
    }) };
    var buf: [8]u8 = undefined;

    smith.bytes(&buf);
    try std.testing.expectEqualSlices(u8, "testing!", &buf);
    smith.bytes(buf[0..0]);
    smith.bytes(buf[0..3]);
    try std.testing.expectEqualSlices(u8, "ab\x00", buf[0..3]);
}

test bytesWeighted {
    var smith: Smith = .{ .in = constructInput(&.{
        .{ .bytes = "testing!" },
        .{ .bytes = "ab" },
    }) };
    const weights: []const Weight = &.{.rangeAtMost(u8, 'a', 'z', 1)};
    var buf: [8]u8 = undefined;

    smith.bytesWeighted(&buf, weights);
    try std.testing.expectEqualSlices(u8, "testinga", &buf);
    smith.bytesWeighted(buf[0..0], weights);
    smith.bytesWeighted(buf[0..3], weights);
    try std.testing.expectEqualSlices(u8, "aba", buf[0..3]);
}

test slice {
    var smith: Smith = .{
        .in = constructInput(&.{
            .{ .slice = "testing!" },
            .{ .slice = "" },
            .{ .slice = "ab" },
            .{ .bytes = std.mem.asBytes(&std.mem.nativeToLittle(u32, 4)) }, // length past end
        }),
    };
    var buf: [8]u8 = undefined;

    try std.testing.expectEqualSlices(u8, "testing!", buf[0..smith.slice(&buf)]);
    try std.testing.expectEqualSlices(u8, "", buf[0..smith.slice(&buf)]);
    try std.testing.expectEqualSlices(u8, "ab", buf[0..smith.slice(&buf)]);
    try std.testing.expectEqualSlices(u8, "", buf[0..smith.slice(&buf)]);
}

test sliceWeightedBytes {
    const weights: []const Weight = &.{.rangeAtMost(u8, 'a', 'z', 1)};
    var smith: Smith = .{ .in = constructInput(&.{
        .{ .slice = "testing!" },
    }) };
    var buf: [8]u8 = undefined;

    try std.testing.expectEqualSlices(
        u8,
        "testinga",
        buf[0..smith.sliceWeightedBytes(&buf, weights)],
    );
    try std.testing.expectEqualSlices(u8, "", buf[0..smith.sliceWeightedBytes(&buf, weights)]);
}

test sliceWeighted {
    const len_weights: []const Weight = &.{.rangeAtMost(u8, 3, 6, 1)};
    const weights: []const Weight = &.{.rangeAtMost(u8, 'a', 'z', 1)};
    var smith: Smith = .{ .in = constructInput(&.{
        .{ .slice = "testing!" },
        .{ .slice = "ing!" },
        .{ .slice = "ab" },
    }) };
    var buf: [8]u8 = undefined;

    try std.testing.expectEqualSlices(
        u8,
        "tes",
        buf[0..smith.sliceWeighted(&buf, len_weights, weights)],
    );
    try std.testing.expectEqualSlices(
        u8,
        "inga",
        buf[0..smith.sliceWeighted(&buf, len_weights, weights)],
    );
    try std.testing.expectEqualSlices(
        u8,
        "aba",
        buf[0..smith.sliceWeighted(&buf, len_weights, weights)],
    );
    try std.testing.expectEqualSlices(
        u8,
        "aaa",
        buf[0..smith.sliceWeighted(&buf, len_weights, weights)],
    );
}
