const builtin = @import("builtin");
const TypeId = builtin.TypeId;
const std = @import("std.zig");

/// This function is intended to be used only in tests. It prints diagnostics to stderr
/// and then aborts when actual_error_union is not expected_error.
pub fn expectError(expected_error: anyerror, actual_error_union: var) void {
    if (actual_error_union) |actual_payload| {
        // TODO remove workaround here for https://github.com/ziglang/zig/issues/557
        if (@sizeOf(@typeOf(actual_payload)) == 0) {
            std.debug.panic("expected error.{}, found {} value", @errorName(expected_error), @typeName(@typeOf(actual_payload)));
        } else {
            std.debug.panic("expected error.{}, found {}", @errorName(expected_error), actual_payload);
        }
    } else |actual_error| {
        if (expected_error != actual_error) {
            std.debug.panic("expected error.{}, found error.{}", @errorName(expected_error), @errorName(actual_error));
        }
    }
}

/// This function is intended to be used only in tests. When the two values are not
/// equal, prints diagnostics to stderr to show exactly how they are not equal,
/// then aborts.
/// The types must match exactly.
pub fn expectEqual(expected: var, actual: @typeOf(expected)) void {
    switch (@typeInfo(@typeOf(actual))) {
        TypeId.NoReturn,
        TypeId.BoundFn,
        TypeId.ArgTuple,
        TypeId.Opaque,
        => @compileError("value of type " ++ @typeName(@typeOf(actual)) ++ " encountered"),

        TypeId.Undefined,
        TypeId.Null,
        TypeId.Void,
        => return,

        TypeId.Type,
        TypeId.Bool,
        TypeId.Int,
        TypeId.Float,
        TypeId.ComptimeFloat,
        TypeId.ComptimeInt,
        TypeId.EnumLiteral,
        TypeId.Enum,
        TypeId.Fn,
        TypeId.Promise,
        TypeId.Vector,
        TypeId.ErrorSet,
        => {
            if (actual != expected) {
                std.debug.panic("expected {}, found {}", expected, actual);
            }
        },

        TypeId.Pointer => |pointer| {
            switch (pointer.size) {
                builtin.TypeInfo.Pointer.Size.One,
                builtin.TypeInfo.Pointer.Size.Many,
                builtin.TypeInfo.Pointer.Size.C,
                => {
                    if (actual != expected) {
                        std.debug.panic("expected {}, found {}", expected, actual);
                    }
                },

                builtin.TypeInfo.Pointer.Size.Slice => {
                    if (actual.ptr != expected.ptr) {
                        std.debug.panic("expected slice ptr {}, found {}", expected.ptr, actual.ptr);
                    }
                    if (actual.len != expected.len) {
                        std.debug.panic("expected slice len {}, found {}", expected.len, actual.len);
                    }
                },
            }
        },

        TypeId.Array => |array| expectEqualSlices(array.child, &expected, &actual),

        TypeId.Struct => {
            @compileError("TODO implement testing.expectEqual for structs");
        },

        TypeId.Union => |union_info| {
            if (union_info.tag_type == null) {
                @compileError("Unable to compare untagged union values");
            }
            @compileError("TODO implement testing.expectEqual for tagged unions");
        },

        TypeId.Optional => {
            if (expected) |expected_payload| {
                if (actual) |actual_payload| {
                    expectEqual(expected_payload, actual_payload);
                } else {
                    std.debug.panic("expected {}, found null", expected_payload);
                }
            } else {
                if (actual) |actual_payload| {
                    std.debug.panic("expected null, found {}", actual_payload);
                }
            }
        },

        TypeId.ErrorUnion => {
            if (expected) |expected_payload| {
                if (actual) |actual_payload| {
                    expectEqual(expected_payload, actual_payload);
                } else |actual_err| {
                    std.debug.panic("expected {}, found {}", expected_payload, actual_err);
                }
            } else |expected_err| {
                if (actual) |actual_payload| {
                    std.debug.panic("expected {}, found {}", expected_err, actual_payload);
                } else |actual_err| {
                    expectEqual(expected_err, actual_err);
                }
            }
        },
    }
}

/// This function is intended to be used only in tests. When the two slices are not
/// equal, prints diagnostics to stderr to show exactly how they are not equal,
/// then aborts.
pub fn expectEqualSlices(comptime T: type, expected: []const T, actual: []const T) void {
    // TODO better printing of the difference
    // If the arrays are small enough we could print the whole thing
    // If the child type is u8 and no weird bytes, we could print it as strings
    // Even for the length difference, it would be useful to see the values of the slices probably.
    if (expected.len != actual.len) {
        std.debug.panic("slice lengths differ. expected {}, found {}", expected.len, actual.len);
    }
    var i: usize = 0;
    while (i < expected.len) : (i += 1) {
        if (expected[i] != actual[i]) {
            std.debug.panic("index {} incorrect. expected {}, found {}", i, expected[i], actual[i]);
        }
    }
}

/// This function is intended to be used only in tests. When `ok` is false, the test fails.
/// A message is printed to stderr and then abort is called.
pub fn expect(ok: bool) void {
    if (!ok) @panic("test failure");
}
