const std = @import("std");
const zx = @import("zx");

const allocator = std.testing.allocator;

var kv_fs = zx.Kv.Fs{
    .io = std.testing.io,
    .subdir = "zig-out" ++ std.fs.path.sep_str ++ "test-data" ++ std.fs.path.sep_str ++ "kv",
};

const kv = kv_fs.kv();

const User = struct {
    id: u32,
    name: []const u8,
    active: bool,
};

const UserAlias = struct {
    id: u32,
    label: []const u8,
    active: bool,
};

fn uniqueLabel(comptime prefix: []const u8, buf: []u8) ![]const u8 {
    var source: std.Random.IoSource = .{ .io = std.testing.io };
    const value = source.interface().int(u64);
    return std.fmt.bufPrint(buf, "{s}-{x}", .{ prefix, value });
}

fn freeUser(user: User) void {
    allocator.free(user.name);
}

test "put/get/delete roundtrip" {
    var key_buf: [64]u8 = undefined;
    const key = try uniqueLabel("kv-default-key", &key_buf);
    const value = "hello from kv";

    defer kv.delete(key) catch {};

    try kv.put(key, value, .{});

    const found = (try kv.get(std.testing.allocator, key)).?;
    defer std.testing.allocator.free(found);

    try std.testing.expectEqualStrings(value, found);

    try kv.delete(key);
    try std.testing.expect((try kv.get(std.testing.allocator, key)) == null);
}

test "list returns prefixed keys" {
    var prefix_buf: [64]u8 = undefined;
    const prefix = try uniqueLabel("kv-list-prefix", &prefix_buf);

    var key1_buf: [96]u8 = undefined;
    var key2_buf: [96]u8 = undefined;
    var other_key_buf: [96]u8 = undefined;

    const key1 = try std.fmt.bufPrint(&key1_buf, "{s}-a", .{prefix});
    const key2 = try std.fmt.bufPrint(&key2_buf, "{s}-b", .{prefix});
    const other_key = try uniqueLabel("kv-list-other", &other_key_buf);

    defer kv.delete(key1) catch {};
    defer kv.delete(key2) catch {};
    defer kv.delete(other_key) catch {};

    try kv.put(key1, "value-a", .{});
    try kv.put(key2, "value-b", .{});
    try kv.put(other_key, "value-c", .{});

    const keys = try kv.list(std.testing.allocator, prefix);
    defer {
        for (keys) |key| std.testing.allocator.free(key);
        std.testing.allocator.free(keys);
    }

    var saw_key1 = false;
    var saw_key2 = false;
    for (keys) |key| {
        try std.testing.expect(std.mem.startsWith(u8, key, prefix));
        if (std.mem.eql(u8, key, key1)) saw_key1 = true;
        if (std.mem.eql(u8, key, key2)) saw_key2 = true;
        try std.testing.expect(!std.mem.eql(u8, key, other_key));
    }

    try std.testing.expect(saw_key1);
    try std.testing.expect(saw_key2);
}

test "scoped namespaces are isolated" {
    var ns_buf: [64]u8 = undefined;
    const namespace = try uniqueLabel("kv-scope", &ns_buf);

    // Runtime namespace: set the field directly (scoped() takes a comptime literal).
    const scoped = zx.Kv{ .vtable = kv.vtable, .userdata = kv.userdata, .namespace = namespace };
    const key = "shared-key";

    defer scoped.delete(key) catch {};
    defer kv.delete(key) catch {};

    try scoped.put(key, "scoped-value", .{});
    try kv.put(key, "default-value", .{});

    const scoped_value = (try scoped.get(std.testing.allocator, key)).?;
    defer std.testing.allocator.free(scoped_value);

    const default_value = (try kv.get(std.testing.allocator, key)).?;
    defer std.testing.allocator.free(default_value);

    try std.testing.expectEqualStrings("scoped-value", scoped_value);
    try std.testing.expectEqualStrings("default-value", default_value);
}

test "scoped() enum literal routes to a distinct namespace" {
    const users = kv.scoped(.users);
    const key = "u-1";

    defer users.delete(key) catch {};
    defer kv.delete(key) catch {};

    try users.put(key, "in-users", .{});
    try kv.put(key, "in-default", .{});

    const from_users = (try users.get(std.testing.allocator, key)).?;
    defer std.testing.allocator.free(from_users);
    const from_default = (try kv.get(std.testing.allocator, key)).?;
    defer std.testing.allocator.free(from_default);

    try std.testing.expectEqualStrings("in-users", from_users);
    try std.testing.expectEqualStrings("in-default", from_default);
}

test "putAs/as roundtrip typed value" {
    var key_buf: [64]u8 = undefined;
    const key = try uniqueLabel("kv-typed-key", &key_buf);

    defer kv.delete(key) catch {};

    try kv.putAs(key, User{
        .id = 42,
        .name = "nurul",
        .active = true,
    }, .{});

    const user = (try kv.as(allocator, key, User)).?;
    defer freeUser(user);

    try std.testing.expectEqual(@as(u32, 42), user.id);
    try std.testing.expectEqualStrings("nurul", user.name);
    try std.testing.expect(user.active);
}

test "as returns invalid type on schema mismatch" {
    var key_buf: [64]u8 = undefined;
    const key = try uniqueLabel("kv-typed-mismatch", &key_buf);

    defer kv.delete(key) catch {};

    try kv.putAs(key, User{
        .id = 7,
        .name = "mismatch",
        .active = false,
    }, .{});

    try std.testing.expectError(error.InvalidType, kv.as(allocator, key, UserAlias));
}

test "scoped putAs/as roundtrip typed value" {
    var ns_buf: [64]u8 = undefined;
    var key_buf: [64]u8 = undefined;

    const namespace = try uniqueLabel("kv-typed-scope", &ns_buf);
    const key = try uniqueLabel("profile", &key_buf);
    const scoped = zx.Kv{ .vtable = kv.vtable, .userdata = kv.userdata, .namespace = namespace };

    defer scoped.delete(key) catch {};

    try scoped.putAs(key, User{
        .id = 99,
        .name = "scoped-user",
        .active = true,
    }, .{});

    const user = (try scoped.as(allocator, key, User)).?;
    defer freeUser(user);

    try std.testing.expectEqual(@as(u32, 99), user.id);
    try std.testing.expectEqualStrings("scoped-user", user.name);
    try std.testing.expect(user.active);
}
