// Tests for protocols.zig — the protocol/impl LOOKUP owner (`ProtocolResolver`).
// Reached via `ir.ProtocolResolver{ .l = &lowering }`, mirroring calls.test.zig /
// generics.test.zig. Covers the pure conformance queries moved out of `Lowering`
// in A4.2 sub-step 2 (lookup increment); registration + emission stay in
// `Lowering`, so their plan tests land with later increments.

const std = @import("std");
const ast = @import("../ast.zig");
const Node = ast.Node;
const errors = @import("../errors.zig");

const ir_mod = @import("ir.zig");
const TypeId = ir_mod.TypeId;
const FuncId = ir_mod.FuncId;
const Lowering = ir_mod.Lowering;
const ProtocolResolver = ir_mod.ProtocolResolver;

fn protoMethodReq(name: []const u8) ast.ProtocolMethodDecl {
    // A required (no default body) method, no params, void return.
    return .{ .name = name, .params = &.{}, .param_names = &.{}, .return_type = null, .default_body = null };
}

fn mk(alloc: std.mem.Allocator, data: ast.Node.Data) *Node {
    const n = alloc.create(Node) catch unreachable;
    n.* = .{ .span = .{ .start = 0, .end = 0 }, .data = data };
    return n;
}
fn typeExpr(alloc: std.mem.Allocator, name: []const u8) *Node {
    return mk(alloc, .{ .type_expr = .{ .name = name, .is_generic = false } });
}
fn emptyBody(alloc: std.mem.Allocator) *Node {
    return mk(alloc, .{ .block = .{ .stmts = &.{} } });
}

test "protocols: getProtocolInfo resolves registered protocol structs only" {
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();
    const alloc = arena.allocator();
    var module = ir_mod.Module.init(alloc);
    defer module.deinit();
    var l = Lowering.init(&module);
    const pr = ProtocolResolver{ .l = &l };

    const methods = [_]ast.ProtocolMethodDecl{protoMethodReq("draw")};
    const pd = ast.ProtocolDecl{ .name = "Drawable", .methods = &methods };
    l.registerProtocolDecl(&pd);

    // The registered protocol struct resolves to its decl info.
    const drawable_ty = module.types.findByName(module.types.internString("Drawable")).?;
    const info = pr.getProtocolInfo(drawable_ty).?;
    try std.testing.expectEqualStrings("Drawable", info.name);
    try std.testing.expectEqual(@as(usize, 1), info.methods.len);

    // A builtin and an unrelated plain struct are not protocols.
    try std.testing.expect(pr.getProtocolInfo(.s32) == null);
    const plain = module.types.intern(.{ .@"struct" = .{ .name = module.types.internString("Point"), .fields = &.{} } });
    try std.testing.expect(pr.getProtocolInfo(plain) == null);

    // The Lowering wrapper delegates to the same result.
    try std.testing.expect(l.getProtocolInfo(drawable_ty) != null);
}

test "protocols: hasImplPlain reflects materialized thunks for a (protocol, type) pair" {
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();
    const alloc = arena.allocator();
    var module = ir_mod.Module.init(alloc);
    defer module.deinit();
    var l = Lowering.init(&module);
    const pr = ProtocolResolver{ .l = &l };

    const circle = module.types.intern(.{ .@"struct" = .{ .name = module.types.internString("Circle"), .fields = &.{} } });

    // No thunks yet → not materialized.
    try std.testing.expect(!pr.hasImplPlain("Drawable", circle));

    // Materialize the (Drawable, Circle) thunk slot the way `getOrCreateThunks`
    // does — key "Proto\x00<formatTypeName>". hasImplPlain must then see it.
    const key = std.fmt.allocPrint(alloc, "Drawable\x00{s}", .{l.formatTypeName(circle)}) catch unreachable;
    l.protocol_thunk_map.put(key, &[_]FuncId{}) catch unreachable;
    try std.testing.expect(pr.hasImplPlain("Drawable", circle));

    // A different protocol over the same type is still unmaterialized.
    try std.testing.expect(!pr.hasImplPlain("Hash", circle));
}

test "protocols: packArgConformsTo at the impl-declaration level (non-parameterised)" {
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();
    const alloc = arena.allocator();
    var module = ir_mod.Module.init(alloc);
    defer module.deinit();
    var l = Lowering.init(&module);
    const pr = ProtocolResolver{ .l = &l };

    // Shape :: protocol { draw :: (); }  (non-parameterised, one required method)
    const methods = [_]ast.ProtocolMethodDecl{protoMethodReq("draw")};
    const pd = ast.ProtocolDecl{ .name = "Shape", .methods = &methods };
    l.registerProtocolDecl(&pd);

    const circle = module.types.intern(.{ .@"struct" = .{ .name = module.types.internString("Circle"), .fields = &.{} } });

    // No `Circle.draw` registered → does NOT conform.
    try std.testing.expect(!pr.packArgConformsTo("Shape", circle));

    // Register the impl method `Circle.draw` (how registerImplBlock records a
    // non-parameterised impl) → now conforms.
    const body = alloc.create(Node) catch unreachable;
    body.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .block = .{ .stmts = &.{} } } };
    const draw_fd = ast.FnDecl{ .name = "Circle.draw", .params = &.{}, .return_type = null, .body = body };
    l.program_index.fn_ast_map.put("Circle.draw", &draw_fd) catch unreachable;
    try std.testing.expect(pr.packArgConformsTo("Shape", circle));

    // An arg already erased to the protocol struct itself trivially conforms.
    const shape_ty = module.types.findByName(module.types.internString("Shape")).?;
    try std.testing.expect(pr.packArgConformsTo("Shape", shape_ty));

    // An unregistered protocol name conforms to nothing.
    try std.testing.expect(!pr.packArgConformsTo("Nope", circle));
}

test "protocols: registerImplBlock records <Target>.<method> in fn_ast_map" {
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();
    const alloc = arena.allocator();
    var module = ir_mod.Module.init(alloc);
    defer module.deinit();
    var l = Lowering.init(&module);
    const pr = ProtocolResolver{ .l = &l };

    // Drawable :: protocol { draw :: (); }  +  impl Drawable for Circle { draw :: (){} }
    const proto_methods = [_]ast.ProtocolMethodDecl{protoMethodReq("draw")};
    const pd = ast.ProtocolDecl{ .name = "Drawable", .methods = &proto_methods };
    l.registerProtocolDecl(&pd);

    const draw_node = mk(alloc, .{ .fn_decl = .{ .name = "draw", .params = &.{}, .return_type = null, .body = emptyBody(alloc) } });
    const methods = [_]*Node{draw_node};
    const ib = ast.ImplBlock{ .protocol_name = "Drawable", .target_type = "Circle", .methods = &methods };
    const decl = mk(alloc, .{ .impl_block = ib });

    // Not registered before; the non-parameterised impl registers `Circle.draw`.
    try std.testing.expect(!l.program_index.fn_ast_map.contains("Circle.draw"));
    pr.registerImplBlock(&ib, false, decl);
    try std.testing.expect(l.program_index.fn_ast_map.contains("Circle.draw"));
    // And it now conforms (same fn_ast_map entry packArgConformsTo checks).
    const circle = module.types.intern(.{ .@"struct" = .{ .name = module.types.internString("Circle"), .fields = &.{} } });
    try std.testing.expect(pr.packArgConformsTo("Drawable", circle));
}

test "protocols: registerParamImpl flags a same-file duplicate impl" {
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();
    const alloc = arena.allocator();
    var module = ir_mod.Module.init(alloc);
    defer module.deinit();
    var diags = errors.DiagnosticList.init(alloc, "", "test.sx");
    defer diags.deinit();
    var l = Lowering.init(&module);
    l.diagnostics = &diags;
    l.current_source_file = "test.sx"; // both impls share a defining module

    const pr = ProtocolResolver{ .l = &l };
    _ = module.types.intern(.{ .@"struct" = .{ .name = module.types.internString("IntCell"), .fields = &.{} } });

    // impl Into(s64) for IntCell { ... } — a parameterised-protocol impl.
    const args = [_]*Node{typeExpr(alloc, "s64")};
    const conv = mk(alloc, .{ .fn_decl = .{ .name = "convert", .params = &.{}, .return_type = null, .body = emptyBody(alloc) } });
    const methods = [_]*Node{conv};
    const ib = ast.ImplBlock{
        .protocol_name = "Into",
        .target_type = "IntCell",
        .methods = &methods,
        .protocol_type_args = &args,
    };
    const decl = mk(alloc, .{ .impl_block = ib });

    // First registration is fine; the second (same key, same module) is a dup.
    pr.registerImplBlock(&ib, false, decl);
    pr.registerImplBlock(&ib, false, decl);

    var dup_reported = false;
    for (diags.items.items) |d| {
        if (d.level == .err and std.mem.indexOf(u8, d.message, "duplicate impl 'Into'") != null) dup_reported = true;
    }
    try std.testing.expect(dup_reported);
}

// ── Planning (lookup-only; emission stays in Lowering) ───────────────

test "protocols: protocolMethodInfos lists the methods to materialize thunks for" {
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();
    const alloc = arena.allocator();
    var module = ir_mod.Module.init(alloc);
    defer module.deinit();
    var l = Lowering.init(&module);
    const pr = ProtocolResolver{ .l = &l };

    const methods = [_]ast.ProtocolMethodDecl{ protoMethodReq("draw"), protoMethodReq("area") };
    const pd = ast.ProtocolDecl{ .name = "Drawable", .methods = &methods };
    l.registerProtocolDecl(&pd);

    // The registry knows exactly which methods getOrCreateThunks must thunk.
    const infos = pr.protocolMethodInfos("Drawable").?;
    try std.testing.expectEqual(@as(usize, 2), infos.len);
    try std.testing.expectEqualStrings("draw", infos[0].name);
    try std.testing.expectEqualStrings("area", infos[1].name);
    // Unknown protocol → null (no silent empty-table default).
    try std.testing.expect(pr.protocolMethodInfos("Nope") == null);
}

test "protocols: findVisibleImpls filters by transitive import visibility" {
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();
    const alloc = arena.allocator();
    var module = ir_mod.Module.init(alloc);
    defer module.deinit();
    var l = Lowering.init(&module);
    const pr = ProtocolResolver{ .l = &l };

    const here_entry: Lowering.ParamImplEntry = .{ .methods = &.{}, .source_ty = .s64, .target_args = &.{}, .defining_module = "a.sx", .span = .{ .start = 0, .end = 0 } };
    const other_entry: Lowering.ParamImplEntry = .{ .methods = &.{}, .source_ty = .s64, .target_args = &.{}, .defining_module = "b.sx", .span = .{ .start = 0, .end = 0 } };
    const entries = [_]Lowering.ParamImplEntry{ here_entry, other_entry };

    // No source-file context → falls open (all entries visible).
    {
        var out = std.ArrayList(Lowering.ParamImplEntry).empty;
        defer out.deinit(alloc);
        pr.findVisibleImpls(&entries, &out);
        try std.testing.expectEqual(@as(usize, 2), out.items.len);
    }

    // From `a.sx`, which imports nothing: only the `a.sx` impl is visible.
    {
        var graph = std.StringHashMap(std.StringHashMap(void)).init(alloc);
        graph.put("a.sx", std.StringHashMap(void).init(alloc)) catch unreachable;
        l.program_index.import_graph = &graph;
        l.current_source_file = "a.sx";
        var out = std.ArrayList(Lowering.ParamImplEntry).empty;
        defer out.deinit(alloc);
        pr.findVisibleImpls(&entries, &out);
        try std.testing.expectEqual(@as(usize, 1), out.items.len);
        try std.testing.expectEqualStrings("a.sx", out.items[0].defining_module);
    }
}

test "protocols: matchPackImpl selects a pack impl whose prefix + return match" {
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();
    const alloc = arena.allocator();
    var module = ir_mod.Module.init(alloc);
    defer module.deinit();
    var l = Lowering.init(&module);
    const pr = ProtocolResolver{ .l = &l };

    // A pack impl source `Closure(..$args) -> void` (pack_start = 0).
    const pack_src = module.types.closureTypePack(&.{}, .void, 0);
    const convert = ast.FnDecl{ .name = "convert", .params = &.{}, .return_type = null, .body = emptyBody(alloc) };
    const conv_methods = [_]*const ast.FnDecl{&convert};
    const pack_entry: Lowering.PackParamImplEntry = .{
        .methods = &conv_methods,
        .source_pack_ty = pack_src,
        .target_args = &.{},
        .defining_module = "test.sx",
        .span = .{ .start = 0, .end = 0 },
        .pack_var_name = "args",
        .ret_var_name = null,
    };
    var list = std.ArrayList(Lowering.PackParamImplEntry).empty;
    list.append(alloc, pack_entry) catch unreachable;
    const pack_key = "Into\x00Block";
    l.param_impl_pack_map.put(pack_key, list) catch unreachable;

    // A concrete `Closure() -> void` source matches (no fixed prefix, void ret).
    const src = module.types.closureType(&.{}, .void);
    const m = pr.matchPackImpl(src, pack_key).?;
    try std.testing.expectEqualStrings("convert", m.convert_fd.name);
    try std.testing.expectEqual(@as(usize, 0), m.src_params.len);
    try std.testing.expectEqual(TypeId.void, m.src_ret);

    // A non-closure source does not match; an unknown key does not match.
    try std.testing.expect(pr.matchPackImpl(.s64, pack_key) == null);
    try std.testing.expect(pr.matchPackImpl(src, "Into\x00Nope") == null);
}
