const std = @import("std");

const expect = std.testing.expect;
const expectEqual = std.testing.expectEqual;
const expectEqualDeep = std.testing.expectEqualDeep;
const expectEqualSlices = std.testing.expectEqualSlices;
const expectEqualStrings = std.testing.expectEqualStrings;

test "bool" {
    try expectEqual(true, @as(bool, @import("zon/true.zon")));
    try expectEqual(false, @as(bool, @import("zon/false.zon")));
}

test "optional" {
    const some: ?u32 = @import("zon/some.zon");
    const none: ?u32 = @import("zon/none.zon");
    const @"null": @TypeOf(null) = @import("zon/none.zon");
    try expectEqual(@as(u32, 10), some);
    try expectEqual(@as(?u32, null), none);
    try expectEqual(null, @"null");
}

test "union" {
    // No tag
    {
        const Union = union {
            x: f32,
            y: bool,
            z: void,
        };

        const union1: Union = @import("zon/union1.zon");
        const union2: Union = @import("zon/union2.zon");
        const union3: Union = @import("zon/union3.zon");

        try expectEqual(1.5, union1.x);
        try expectEqual(true, union2.y);
        try expectEqual({}, union3.z);
    }

    // Inferred tag
    {
        const Union = union(enum) {
            x: f32,
            y: bool,
            z: void,
        };

        const union1: Union = comptime @import("zon/union1.zon");
        const union2: Union = @import("zon/union2.zon");
        const union3: Union = @import("zon/union3.zon");

        try expectEqual(1.5, union1.x);
        try expectEqual(true, union2.y);
        try expectEqual({}, union3.z);
    }

    // Explicit tag
    {
        const Tag = enum(i128) {
            x = -1,
            y = 2,
            z = 1,
        };
        const Union = union(Tag) {
            x: f32,
            y: bool,
            z: void,
        };

        const union1: Union = @import("zon/union1.zon");
        const union2: Union = @import("zon/union2.zon");
        const union3: Union = @import("zon/union3.zon");

        try expectEqual(1.5, union1.x);
        try expectEqual(true, union2.y);
        try expectEqual({}, union3.z);
    }
}

test "struct" {
    const Vec0 = struct {};
    const Vec1 = struct { x: f32 };
    const Vec2 = struct { x: f32, y: f32 };
    const Escaped = struct { @"0": f32, foo: f32 };
    try expectEqual(Vec0{}, @as(Vec0, @import("zon/vec0.zon")));
    try expectEqual(Vec1{ .x = 1.5 }, @as(Vec1, @import("zon/vec1.zon")));
    try expectEqual(Vec2{ .x = 1.5, .y = 2 }, @as(Vec2, @import("zon/vec2.zon")));
    try expectEqual(Escaped{ .@"0" = 1.5, .foo = 2 }, @as(Escaped, @import("zon/escaped_struct.zon")));
}

test "struct default fields" {
    const Vec3 = struct {
        x: f32,
        y: f32,
        z: f32 = 123.4,
    };
    try expectEqual(Vec3{ .x = 1.5, .y = 2.0, .z = 123.4 }, @as(Vec3, @import("zon/vec2.zon")));
    const ascribed: Vec3 = @import("zon/vec2.zon");
    try expectEqual(Vec3{ .x = 1.5, .y = 2.0, .z = 123.4 }, ascribed);

    const Vec2 = struct {
        x: f32 = 20.0,
        y: f32 = 10.0,
    };
    try expectEqual(Vec2{ .x = 1.5, .y = 2.0 }, @as(Vec2, @import("zon/vec2.zon")));
}

test "struct enum field" {
    const Struct = struct {
        x: enum { x, y, z },
    };
    try expectEqual(Struct{ .x = .z }, @as(Struct, @import("zon/enum_field.zon")));
}

test "tuple" {
    const Tuple = struct { f32, bool, []const u8, u16 };
    try expectEqualDeep(Tuple{ 1.2, true, "hello", 3 }, @as(Tuple, @import("zon/tuple.zon")));
}

test "comptime fields" {
    // Test setting comptime tuple fields to the correct value
    {
        const Tuple = struct {
            comptime f32 = 1.2,
            comptime bool = true,
            comptime []const u8 = "hello",
            comptime u16 = 3,
        };
        try expectEqualDeep(Tuple{ 1.2, true, "hello", 3 }, @as(Tuple, @import("zon/tuple.zon")));
    }

    // Test setting comptime struct fields to the correct value
    {
        const Vec2 = struct {
            comptime x: f32 = 1.5,
            comptime y: f32 = 2.0,
        };
        try expectEqualDeep(Vec2{}, @as(Vec2, @import("zon/vec2.zon")));
    }

    // Test allowing comptime tuple fields to be set to their defaults
    {
        const Tuple = struct {
            f32,
            bool,
            []const u8,
            u16,
            comptime u8 = 255,
        };
        try expectEqualDeep(Tuple{ 1.2, true, "hello", 3 }, @as(Tuple, @import("zon/tuple.zon")));
    }

    // Test allowing comptime struct fields to be set to their defaults
    {
        const Vec2 = struct {
            comptime x: f32 = 1.5,
            comptime y: f32 = 2.0,
        };
        try expectEqualDeep(Vec2{}, @as(Vec2, @import("zon/slice-empty.zon")));
    }
}

test "char" {
    try expectEqual(@as(u8, 'a'), @as(u8, @import("zon/a.zon")));
    try expectEqual(@as(u8, 'z'), @as(u8, @import("zon/z.zon")));
}

test "arrays" {
    try expectEqual([0]u8{}, @as([0]u8, @import("zon/vec0.zon")));
    try expectEqual([0:1]u8{}, @as([0:1]u8, @import("zon/vec0.zon")));
    try expectEqual(1, @as([0:1]u8, @import("zon/vec0.zon"))[0]);
    try expectEqual([4]u8{ 'a', 'b', 'c', 'd' }, @as([4]u8, @import("zon/array.zon")));
    try expectEqual([4:2]u8{ 'a', 'b', 'c', 'd' }, @as([4:2]u8, @import("zon/array.zon")));
    try expectEqual(2, @as([4:2]u8, @import("zon/array.zon"))[4]);
}

test "slices, arrays, tuples" {
    {
        const expected_slice: []const u8 = &.{};
        const found_slice: []const u8 = @import("zon/slice-empty.zon");
        try expectEqualSlices(u8, expected_slice, found_slice);

        const expected_array: [0]u8 = .{};
        const found_array: [0]u8 = @import("zon/slice-empty.zon");
        try expectEqual(expected_array, found_array);

        const T = struct {};
        const expected_tuple: T = .{};
        const found_tuple: T = @import("zon/slice-empty.zon");
        try expectEqual(expected_tuple, found_tuple);
    }

    {
        const expected_slice: []const u8 = &.{1};
        const found_slice: []const u8 = @import("zon/slice1_no_newline.zon");
        try expectEqualSlices(u8, expected_slice, found_slice);

        const expected_array: [1]u8 = .{1};
        const found_array: [1]u8 = @import("zon/slice1_no_newline.zon");
        try expectEqual(expected_array, found_array);

        const T = struct { u8 };
        const expected_tuple: T = .{1};
        const found_tuple: T = @import("zon/slice1_no_newline.zon");
        try expectEqual(expected_tuple, found_tuple);
    }

    {
        const expected_slice: []const u8 = &.{ 'a', 'b', 'c' };
        const found_slice: []const u8 = @import("zon/slice-abc.zon");
        try expectEqualSlices(u8, expected_slice, found_slice);

        const expected_array: [3]u8 = .{ 'a', 'b', 'c' };
        const found_array: [3]u8 = @import("zon/slice-abc.zon");
        try expectEqual(expected_array, found_array);

        const T = struct { u8, u8, u8 };
        const expected_tuple: T = .{ 'a', 'b', 'c' };
        const found_tuple: T = @import("zon/slice-abc.zon");
        try expectEqual(expected_tuple, found_tuple);
    }
}

test "string literals" {
    try expectEqualSlices(u8, "abc", @import("zon/abc.zon"));
    try expectEqualSlices(u8, "ab\\c", @import("zon/abc-escaped.zon"));
    const zero_terminated: [:0]const u8 = @import("zon/abc.zon");
    try expectEqualDeep(zero_terminated, "abc");
    try expectEqual(0, zero_terminated[zero_terminated.len]);
    try expectEqualStrings(
        \\Hello, world!
        \\This is a multiline string!
        \\ There are no escapes, we can, for example, include \n in the string
    , @import("zon/multiline_string.zon"));
    try expectEqualStrings("a\nb\x00c", @import("zon/string_embedded_null.zon"));
}

test "enum literals" {
    const Enum = enum {
        foo,
        bar,
        baz,
        @"0\na",
    };
    try expectEqual(Enum.foo, @as(Enum, @import("zon/foo.zon")));
    try expectEqual(.foo, @as(@TypeOf(.foo), @import("zon/foo.zon")));
    try expectEqual(Enum.@"0\na", @as(Enum, @import("zon/escaped_enum.zon")));
}

test "int" {
    const T = struct {
        u8,
        i16,
        i14,
        i32,
        i8,
        i8,
        u8,
        u8,
        u65,
        u65,
        i128,
        i128,
        i66,
        i66,
        i8,
        i8,
        i16,
        i16,
        i16,
        i16,
        i16,
        i16,
        u65,
        i66,
        i66,
        u65,
        i66,
        i66,
        u65,
        i66,
        i66,
    };
    const expected: T = .{
        // Test various numbers and types
        10,
        24,
        -4,
        -123,

        // Test limits
        127,
        -128,

        // Test characters
        'a',
        'z',

        // Test big integers
        36893488147419103231,
        36893488147419103231,
        -18446744073709551615, // Only a big int due to negation
        -9223372036854775809, // Only a big int due to negation

        // Test big integer limits
        36893488147419103231,
        -36893488147419103232,

        // Test parsing whole number floats as integers
        -1,
        123,

        // Test non-decimal integers
        0xff,
        -0xff,
        0o77,
        -0o77,
        0b11,
        -0b11,

        // Test non-decimal big integers
        0x1ffffffffffffffff,
        0x1ffffffffffffffff,
        -0x1ffffffffffffffff,
        0x1ffffffffffffffff,
        0x1ffffffffffffffff,
        -0x1ffffffffffffffff,
        0x1ffffffffffffffff,
        0x1ffffffffffffffff,
        -0x1ffffffffffffffff,
    };
    const actual: T = @import("zon/ints.zon");
    try expectEqual(expected, actual);
}

test "floats" {
    const T = struct {
        f16,
        f32,
        f64,
        f128,
        f16,
        f16,
        f32,
        f32,
        f32,
        f32,
        f32,
        f32,
        f128,
        f32,
        f32,
        f32,
        f32,
        f32,
    };
    const expected: T = .{
        // Test decimals
        0.5,
        123.456,
        -123.456,
        42.5,

        // Test whole numbers with and without decimals
        5.0,
        5.0,
        -102,
        -102,

        // Test characters and negated characters
        'a',
        'z',

        // Test big integers
        36893488147419103231,
        -36893488147419103231,
        0x1ffffffffffffffff,
        0x1ffffffffffffffff,

        // Exponents, underscores
        123.0E+77,

        // Hexadecimal
        0x103.70p-5,
        -0x103.70,
        0x1234_5678.9ABC_CDEFp-10,
    };
    const actual: T = @import("zon/floats.zon");
    try expectEqual(expected, actual);
}

test "inf and nan" {
    // f32
    {
        const actual: struct { f32, f32, f32 } = @import("zon/inf_and_nan.zon");
        try expect(std.math.isNan(actual[0]));
        try expect(std.math.isPositiveInf(actual[1]));
        try expect(std.math.isNegativeInf(actual[2]));
    }

    // f128
    {
        const actual: struct { f128, f128, f128 } = @import("zon/inf_and_nan.zon");
        try expect(std.math.isNan(actual[0]));
        try expect(std.math.isPositiveInf(actual[1]));
        try expect(std.math.isNegativeInf(actual[2]));
    }
}

test "vector" {
    {
        const actual: @Vector(0, bool) = @import("zon/vec0.zon");
        const expected: @Vector(0, bool) = .{};
        try expectEqual(expected, actual);
    }
    {
        const actual: @Vector(3, bool) = @import("zon/vec3_bool.zon");
        const expected: @Vector(3, bool) = .{ false, false, true };
        try expectEqual(expected, actual);
    }

    {
        const actual: @Vector(0, f32) = @import("zon/vec0.zon");
        const expected: @Vector(0, f32) = .{};
        try expectEqual(expected, actual);
    }
    {
        const actual: @Vector(3, f32) = @import("zon/vec3_float.zon");
        const expected: @Vector(3, f32) = .{ 1.5, 2.5, 3.5 };
        try expectEqual(expected, actual);
    }

    {
        const actual: @Vector(0, u8) = @import("zon/vec0.zon");
        const expected: @Vector(0, u8) = .{};
        try expectEqual(expected, actual);
    }
    {
        const actual: @Vector(3, u8) = @import("zon/vec3_int.zon");
        const expected: @Vector(3, u8) = .{ 2, 4, 6 };
        try expectEqual(expected, actual);
    }

    {
        const actual: @Vector(0, *const u8) = @import("zon/vec0.zon");
        const expected: @Vector(0, *const u8) = .{};
        try expectEqual(expected, actual);
    }
    {
        const actual: @Vector(3, *const u8) = @import("zon/vec3_int.zon");
        const expected: @Vector(3, *const u8) = .{ &2, &4, &6 };
        try expectEqual(expected, actual);
    }

    {
        const actual: @Vector(0, ?*const u8) = @import("zon/vec0.zon");
        const expected: @Vector(0, ?*const u8) = .{};
        try expectEqual(expected, actual);
    }
    {
        const actual: @Vector(3, ?*const u8) = @import("zon/vec3_int_opt.zon");
        const expected: @Vector(3, ?*const u8) = .{ &2, null, &6 };
        try expectEqual(expected, actual);
    }
}

test "pointers" {
    // Primitive with varying levels of pointers
    try expectEqual(@as(u8, 'a'), @as(*const u8, @import("zon/a.zon")).*);
    try expectEqual(@as(u8, 'a'), @as(*const *const u8, @import("zon/a.zon")).*.*);
    try expectEqual(@as(u8, 'a'), @as(*const *const *const u8, @import("zon/a.zon")).*.*.*);

    // Primitive optional with varying levels of pointers
    try expectEqual(@as(u8, 'a'), @as(?*const u8, @import("zon/a.zon")).?.*);
    try expectEqual(null, @as(?*const u8, @import("zon/none.zon")));

    try expectEqual(@as(u8, 'a'), @as(*const ?u8, @import("zon/a.zon")).*.?);
    try expectEqual(null, @as(*const ?u8, @import("zon/none.zon")).*);

    try expectEqual(@as(u8, 'a'), @as(?*const *const u8, @import("zon/a.zon")).?.*.*);
    try expectEqual(null, @as(?*const *const u8, @import("zon/none.zon")));

    try expectEqual(@as(u8, 'a'), @as(*const ?*const u8, @import("zon/a.zon")).*.?.*);
    try expectEqual(null, @as(*const ?*const u8, @import("zon/none.zon")).*);

    try expectEqual(@as(u8, 'a'), @as(*const *const ?u8, @import("zon/a.zon")).*.*.?);
    try expectEqual(null, @as(*const *const ?u8, @import("zon/none.zon")).*.*);

    try expectEqual([3]u8{ 2, 4, 6 }, @as(*const [3]u8, @import("zon/vec3_int.zon")).*);

    // A complicated type with nested internal pointers and string allocations
    {
        const Inner = struct {
            f1: *const ?*const []const u8,
            f2: *const ?*const []const u8,
        };
        const Outer = struct {
            f1: *const ?*const Inner,
            f2: *const ?*const Inner,
        };
        const expected: Outer = .{
            .f1 = &&.{
                .f1 = &null,
                .f2 = &&"foo",
            },
            .f2 = &null,
        };

        const found: ?*const Outer = @import("zon/complex.zon");
        try std.testing.expectEqualDeep(expected, found.?.*);
    }
}

test "recursive" {
    const Recursive = struct { foo: ?*const @This() };
    const expected: Recursive = .{ .foo = &.{ .foo = null } };
    try expectEqualDeep(expected, @as(Recursive, @import("zon/recursive.zon")));
}

test "anon" {
    const expected = .{
        .{
            .bool_true = true,
            .bool_false = false,
            .string = "foo",
        },
        .{
            null,
            10,
            36893488147419103232,
            1.234,
            'z',
            .bar,
            .{},
        },
    };

    const actual = @import("zon/anon.zon");
    try expectEqual(expected.len, actual.len);
    try expectEqual(expected[1], actual[1]);
    const expected_struct = expected[0];
    const actual_struct = actual[0];
    const expected_fields = @typeInfo(@TypeOf(expected_struct)).@"struct".fields;
    const actual_fields = @typeInfo(@TypeOf(actual_struct)).@"struct".fields;
    try expectEqual(expected_fields.len, actual_fields.len);
    inline for (expected_fields) |field| {
        try expectEqual(@field(expected_struct, field.name), @field(actual_struct, field.name));
    }
}

test "build.zig.zon" {
    const build = @import("zon/build.zig.zon");

    try expectEqual(4, @typeInfo(@TypeOf(build)).@"struct".fields.len);
    try expectEqualStrings("temp", build.name);
    try expectEqualStrings("0.0.0", build.version);

    const dependencies = build.dependencies;
    try expectEqual(2, @typeInfo(@TypeOf(dependencies)).@"struct".fields.len);

    const example_0 = dependencies.example_0;
    try expectEqual(2, @typeInfo(@TypeOf(dependencies)).@"struct".fields.len);
    try expectEqualStrings("https://example.com/foo.tar.gz", example_0.url);
    try expectEqualStrings("...", example_0.hash);

    const example_1 = dependencies.example_1;
    try expectEqual(2, @typeInfo(@TypeOf(dependencies)).@"struct".fields.len);
    try expectEqualStrings("../foo", example_1.path);
    try expectEqual(false, example_1.lazy);

    try expectEqual(.{ "build.zig", "build.zig.zon", "src" }, build.paths);
}
