const interface = @import("interface.zig");
const Interface = interface.Interface;
const SelfType = interface.SelfType;

const std = @import("std");
const mem = std.mem;
const expectEqual = std.testing.expectEqual;
const assert = std.debug.assert;

test "Simple NonOwning interface" {
    const NonOwningTest = struct {
        fn run() !void {
            const Fooer = Interface(struct {
                foo: fn (*SelfType) usize,
            }, interface.Storage.NonOwning);

            const TestFooer = struct {
                const Self = @This();

                state: usize,

                pub fn foo(self: *Self) usize {
                    const tmp = self.state;
                    self.state += 1;
                    return tmp;
                }
            };

            var f = TestFooer{ .state = 42 };
            var fooer = try Fooer.init(.{&f});
            defer fooer.deinit();

            expectEqual(@as(usize, 42), fooer.call("foo", .{}));
            expectEqual(@as(usize, 43), fooer.call("foo", .{}));
        }
    };

    try NonOwningTest.run();
    comptime try NonOwningTest.run();
}

test "Comptime only interface" {
    const TestIFace = Interface(struct {
        foo: fn (*SelfType, u8) u8,
    }, interface.Storage.Comptime);

    const TestType = struct {
        const Self = @This();

        state: u8,

        pub fn foo(self: Self, a: u8) u8 {
            return self.state + a;
        }
    };

    comptime var iface = try TestIFace.init(.{TestType{ .state = 0 }});
    expectEqual(@as(u8, 42), iface.call("foo", .{42}));
}

test "Owning interface with optional function" {
    const OwningOptionalFuncTest = struct {
        fn run() !void {
            const TestOwningIface = Interface(struct {
                someFn: ?fn (*const SelfType, usize, usize) usize,
                otherFn: fn (*SelfType, usize) anyerror!void,
            }, interface.Storage.Owning);

            const TestStruct = struct {
                const Self = @This();

                state: usize,

                pub fn someFn(self: Self, a: usize, b: usize) usize {
                    return self.state * a + b;
                }

                // Note that our return type need only coerce to the virtual function's
                // return type.
                pub fn otherFn(self: *Self, new_state: usize) void {
                    self.state = new_state;
                }
            };

            var iface_instance = try TestOwningIface.init(.{ comptime TestStruct{ .state = 0 }, std.testing.allocator });
            defer iface_instance.deinit();

            try iface_instance.call("otherFn", .{100});
            expectEqual(@as(usize, 42), iface_instance.call("someFn", .{ 0, 42 }).?);
        }
    };

    try OwningOptionalFuncTest.run();
}

test "Interface with virtual async function implemented by an async function" {
    const AsyncIFace = Interface(struct {
        const async_call_stack_size = 1024;

        foo: fn (*SelfType) callconv(.Async) void,
    }, interface.Storage.NonOwning);

    const Impl = struct {
        const Self = @This();

        state: usize,
        frame: anyframe = undefined,

        pub fn foo(self: *Self) void {
            suspend {
                self.frame = @frame();
            }
            self.state += 1;
            suspend;
            self.state += 1;
        }
    };

    var i = Impl{ .state = 0 };
    var instance = try AsyncIFace.init(.{&i});
    _ = async instance.call("foo", .{});

    expectEqual(@as(usize, 0), i.state);
    resume i.frame;
    expectEqual(@as(usize, 1), i.state);
    resume i.frame;
    expectEqual(@as(usize, 2), i.state);
}

test "Interface with virtual async function implemented by a blocking function" {
    const AsyncIFace = Interface(struct {
        readBytes: fn (*SelfType, []u8) callconv(.Async) anyerror!void,
    }, interface.Storage.Inline(8));

    const Impl = struct {
        const Self = @This();

        pub fn readBytes(self: Self, outBuf: []u8) void {
            for (outBuf) |*c| {
                c.* = 3;
            }
        }
    };

    var instance = try AsyncIFace.init(.{Impl{}});

    var buf: [256]u8 = undefined;
    try await async instance.call("readBytes", .{buf[0..]});

    expectEqual([_]u8{3} ** 256, buf);
}