const std = @import("std");
const ast = @import("../ast.zig");
const Node = ast.Node;
const types = @import("types.zig");
const TypeTable = types.TypeTable;
const TypeId = types.TypeId;
const tr_mod = @import("type_resolver.zig");
const TypeResolver = tr_mod.TypeResolver;
const ResolveEnv = tr_mod.ResolveEnv;
const ProgramIndex = @import("program_index.zig").ProgramIndex;

fn typeExpr(name: []const u8) Node {
    return .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .type_expr = .{ .name = name, .is_generic = false } } };
}

/// Stand-in for `Lowering.resolveInner`: the real hook recurses the full
/// stateful resolver; here element types are always primitives, resolved via
/// the keyword table.
const PrimInner = struct {
    pub fn resolveInner(_: PrimInner, node: *const Node) TypeId {
        return switch (node.data) {
            .type_expr => |te| TypeResolver.resolvePrimitive(te.name) orelse .unresolved,
            else => .unresolved,
        };
    }
};

test "TypeResolver.resolvePrimitive maps builtin keywords, null otherwise" {
    try std.testing.expectEqual(@as(?TypeId, .s64), TypeResolver.resolvePrimitive("s64"));
    try std.testing.expectEqual(@as(?TypeId, .bool), TypeResolver.resolvePrimitive("bool"));
    try std.testing.expectEqual(@as(?TypeId, .f64), TypeResolver.resolvePrimitive("f64"));
    try std.testing.expectEqual(@as(?TypeId, .void), TypeResolver.resolvePrimitive("void"));
    try std.testing.expectEqual(@as(?TypeId, .any), TypeResolver.resolvePrimitive("Any"));
    try std.testing.expectEqual(@as(?TypeId, .any), TypeResolver.resolvePrimitive("Type"));
    try std.testing.expectEqual(@as(?TypeId, .usize), TypeResolver.resolvePrimitive("usize"));
    try std.testing.expectEqual(@as(?TypeId, .isize), TypeResolver.resolvePrimitive("isize"));
    try std.testing.expectEqual(@as(?TypeId, .noreturn), TypeResolver.resolvePrimitive("noreturn"));
    // Non-primitives (aliases / generics / named structs) defer to the caller.
    try std.testing.expect(TypeResolver.resolvePrimitive("List") == null);
    try std.testing.expect(TypeResolver.resolvePrimitive("ShaderHandle") == null);
    try std.testing.expect(TypeResolver.resolvePrimitive("") == null);
}

test "TypeResolver.resolveCompound builds structural compound types" {
    // Arena-backed: interned tuple field slices are owned by the type table and
    // reclaimed in bulk by the real compiler's arena (never freed individually).
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();
    const alloc = arena.allocator();
    var table = TypeTable.init(alloc);
    const inner = PrimInner{};

    var s64n = typeExpr("s64");
    var u8n = typeExpr("u8");
    var f32n = typeExpr("f32");
    var booln = typeExpr("bool");
    var s32n = typeExpr("s32");

    var ptr = Node{ .span = .{ .start = 0, .end = 0 }, .data = .{ .pointer_type_expr = .{ .pointee_type = &s64n } } };
    try std.testing.expectEqual(@as(?TypeId, table.ptrTo(.s64)), TypeResolver.resolveCompound(&table, &ptr, inner));

    var mptr = Node{ .span = .{ .start = 0, .end = 0 }, .data = .{ .many_pointer_type_expr = .{ .element_type = &u8n } } };
    try std.testing.expectEqual(@as(?TypeId, table.manyPtrTo(.u8)), TypeResolver.resolveCompound(&table, &mptr, inner));

    var slice = Node{ .span = .{ .start = 0, .end = 0 }, .data = .{ .slice_type_expr = .{ .element_type = &f32n } } };
    try std.testing.expectEqual(@as(?TypeId, table.sliceOf(.f32)), TypeResolver.resolveCompound(&table, &slice, inner));

    var opt = Node{ .span = .{ .start = 0, .end = 0 }, .data = .{ .optional_type_expr = .{ .inner_type = &booln } } };
    try std.testing.expectEqual(@as(?TypeId, table.optionalOf(.bool)), TypeResolver.resolveCompound(&table, &opt, inner));

    var len = Node{ .span = .{ .start = 0, .end = 0 }, .data = .{ .int_literal = .{ .value = 3 } } };
    var arr = Node{ .span = .{ .start = 0, .end = 0 }, .data = .{ .array_type_expr = .{ .length = &len, .element_type = &s32n } } };
    try std.testing.expectEqual(@as(?TypeId, table.arrayOf(.s32, 3)), TypeResolver.resolveCompound(&table, &arr, inner));

    // Function type `(s64) -> bool` — resolveCompound owns it (A2.3b).
    const fparams = [_]*Node{&s64n};
    var fnode = Node{ .span = .{ .start = 0, .end = 0 }, .data = .{ .function_type_expr = .{ .param_types = &fparams, .return_type = &booln } } };
    try std.testing.expectEqual(@as(?TypeId, table.functionTypeCC(&[_]TypeId{.s64}, .bool, .default)), TypeResolver.resolveCompound(&table, &fnode, inner));

    // Plain closure `Closure(s64) -> bool` (no pack) — owned here.
    const cparams = [_]*Node{&s64n};
    var cnode = Node{ .span = .{ .start = 0, .end = 0 }, .data = .{ .closure_type_expr = .{ .param_types = &cparams, .return_type = &booln } } };
    try std.testing.expectEqual(@as(?TypeId, table.closureType(&[_]TypeId{.s64}, .bool)), TypeResolver.resolveCompound(&table, &cnode, inner));

    // Plain positional tuple `(s64, bool)` — owned here.
    const tfields = [_]*Node{ &s64n, &booln };
    var tnode = Node{ .span = .{ .start = 0, .end = 0 }, .data = .{ .tuple_type_expr = .{ .field_types = &tfields, .field_names = null } } };
    const want_tuple = table.intern(.{ .tuple = .{ .fields = &[_]TypeId{ .s64, .bool }, .names = null } });
    try std.testing.expectEqual(@as(?TypeId, want_tuple), TypeResolver.resolveCompound(&table, &tnode, inner));

    // Pack-shaped `Closure(..p)` → null (needs caller pack state → PackResolver).
    var cpack = Node{ .span = .{ .start = 0, .end = 0 }, .data = .{ .closure_type_expr = .{ .param_types = &.{}, .return_type = &booln, .pack_name = "p" } } };
    try std.testing.expect(TypeResolver.resolveCompound(&table, &cpack, inner) == null);

    // Spread tuple `(..xs)` → null (a spread field needs pack expansion).
    var spread_op = typeExpr("xs");
    var spread = Node{ .span = .{ .start = 0, .end = 0 }, .data = .{ .spread_expr = .{ .operand = &spread_op } } };
    const sfields = [_]*Node{&spread};
    var snode = Node{ .span = .{ .start = 0, .end = 0 }, .data = .{ .tuple_type_expr = .{ .field_types = &sfields, .field_names = null } } };
    try std.testing.expect(TypeResolver.resolveCompound(&table, &snode, inner) == null);

    // Names / parameterized types are not this resolver's responsibility → null.
    var name = typeExpr("List");
    try std.testing.expect(TypeResolver.resolveCompound(&table, &name, inner) == null);
}

test "ResolveEnv default-constructs with all-null context" {
    const env = ResolveEnv{};
    try std.testing.expect(env.type_bindings == null);
    try std.testing.expect(env.pack_bindings == null);
    try std.testing.expect(env.target_type == null);
}

test "TypeResolver.resolveBinding reads ResolveEnv type bindings ($T)" {
    const alloc = std.testing.allocator;
    var tb = std.StringHashMap(TypeId).init(alloc);
    defer tb.deinit();
    try tb.put("T", .s64);
    const env = ResolveEnv{ .type_bindings = &tb };

    var bound = typeExpr("T");
    try std.testing.expectEqual(@as(?TypeId, .s64), TypeResolver.resolveBinding(&bound, env));
    // Unbound name → null (caller continues with primitive / alias / struct).
    var unbound = typeExpr("U");
    try std.testing.expect(TypeResolver.resolveBinding(&unbound, env) == null);
    // No active bindings → null.
    try std.testing.expect(TypeResolver.resolveBinding(&bound, ResolveEnv{}) == null);
}

test "TypeResolver.resolveName resolves aliases via ProgramIndex (not the TypeTable.aliases borrow)" {
    const alloc = std.testing.allocator;
    var table = TypeTable.init(alloc);
    defer table.deinit();
    var index = ProgramIndex.init(alloc);
    defer index.deinit();
    try index.type_alias_map.put("ShaderHandle", .u32); // alias → primitive
    const ptr_s64 = table.ptrTo(.s64);
    try index.type_alias_map.put("NodeRef", ptr_s64); // alias → pointer
    const tr = TypeResolver{ .alloc = alloc, .types = &table, .diagnostics = null, .index = &index };

    try std.testing.expectEqual(@as(TypeId, .u32), tr.resolveName("ShaderHandle"));
    try std.testing.expectEqual(ptr_s64, tr.resolveName("NodeRef"));
    // Primitive is checked before alias.
    try std.testing.expectEqual(@as(TypeId, .s64), tr.resolveName("s64"));
}

test "TypeResolver.resolveNamed: width-int, string-prefix, unknown→stub" {
    const alloc = std.testing.allocator;
    var table = TypeTable.init(alloc);
    defer table.deinit();
    try std.testing.expectEqual(table.intern(.{ .signed = 7 }), TypeResolver.resolveNamed("s7", &table, null));
    try std.testing.expectEqual(table.ptrTo(.s64), TypeResolver.resolveNamed("*s64", &table, null));
    // Unknown name, no alias map → empty-struct stub (preserved behavior;
    // never `.unresolved`, which is reserved for failed *generic* resolution).
    try std.testing.expect(TypeResolver.resolveNamed("Unknown", &table, null) != .unresolved);
}
