const std = @import("std");
const sx = struct {
    pub const sema = @import("../sema.zig");
    pub const types = @import("../types.zig");
};
const doc_mod = @import("document.zig");

// Minimal LSP test harness: drive the editor analyzer through the
// real didOpen path (`DocumentStore.analyzeDocument`) and inspect the resulting
// editor index. This is the FIRST `src/lsp/*.test.zig`; it is pulled into the
// `zig build test` graph via the `_ = lsp.document;` reference in `src/root.zig`
// (its tests live one struct deeper than `refAllDecls` reaches).

var g_test_threaded: ?std.Io.Threaded = null;
fn test_io() std.Io {
    if (g_test_threaded == null) {
        g_test_threaded = std.Io.Threaded.init(std.heap.page_allocator, .{});
    }
    return g_test_threaded.?.io();
}

/// The editor `Type` recorded for field `field` of struct `type_name` in the
/// document's index, or null if the document/struct/field isn't present.
fn fieldTypeOf(doc: *doc_mod.Document, type_name: []const u8, field: []const u8) ?sx.types.Type {
    const sema = doc.sema orelse return null;
    const info = sema.struct_types.get(type_name) orelse return null;
    for (info.field_names, info.field_types) |fname, fty| {
        if (std.mem.eql(u8, fname, field)) return fty;
    }
    return null;
}

test "analyzeDocument: identifier array dimension folds to the const value (issue 0099)" {
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();
    const alloc = arena.allocator();

    var store = doc_mod.DocumentStore.init(alloc, test_io(), &.{});
    const src: [:0]const u8 =
        \\MAX :: 4;
        \\Thing :: struct { buf: [MAX]u8; }
    ;
    const doc = try store.openOrUpdate("main.sx", src, 1);
    // Pre-fix this aborts inside `resolveTypeNode`: the array `.length` node is
    // an `identifier` (named const), but the code read `.int_literal`
    // unconditionally. Reaching the assertions at all proves the crash is gone.
    try store.analyzeDocument(doc);

    const buf_ty = fieldTypeOf(doc, "Thing", "buf") orelse return error.SkipZigTest;
    try std.testing.expect(buf_ty == .array_type);
    try std.testing.expectEqual(@as(?u32, 4), buf_ty.array_type.length);
    try std.testing.expectEqualStrings("u8", buf_ty.array_type.element_name);
}

test "analyzeDocument: int-literal array dimension still resolves to its length" {
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();
    const alloc = arena.allocator();

    var store = doc_mod.DocumentStore.init(alloc, test_io(), &.{});
    const src: [:0]const u8 = "Buf :: struct { data: [64]u8; }";
    const doc = try store.openOrUpdate("main.sx", src, 1);
    try store.analyzeDocument(doc);

    const data_ty = fieldTypeOf(doc, "Buf", "data") orelse return error.SkipZigTest;
    try std.testing.expect(data_ty == .array_type);
    try std.testing.expectEqual(@as(?u32, 64), data_ty.array_type.length);
    try std.testing.expectEqualStrings("u8", data_ty.array_type.element_name);
}

test "analyzeDocument: unresolvable array dimension records an explicit unknown length" {
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();
    const alloc = arena.allocator();

    var store = doc_mod.DocumentStore.init(alloc, test_io(), &.{});
    // `N` is never declared as an integer const → the dimension is unknown.
    // Must not panic and must not fabricate a concrete length.
    const src: [:0]const u8 = "Holder :: struct { slots: [N]u8; }";
    const doc = try store.openOrUpdate("main.sx", src, 1);
    try store.analyzeDocument(doc);

    const slots_ty = fieldTypeOf(doc, "Holder", "slots") orelse return error.SkipZigTest;
    try std.testing.expect(slots_ty == .array_type);
    try std.testing.expectEqual(@as(?u32, null), slots_ty.array_type.length);
}
