//! Shared comptime contract-validation helpers.
//!
//! Both the TUI tab framework (`src/tui/tab_framework.zig`) and the CLI
//! command framework (`src/commands/framework.zig`) walk a candidate
//! module at comptime to assert it conforms to the framework contract.
//! The check shapes are identical:
//!
//!   - `expectDeclWithType` — a non-fn decl exists with the exact type.
//!   - `expectFn` — a fn decl exists with the exact fn-pointer type.
//!   - `expectFnInferredError` — a fn decl exists, params match, and
//!     the return is an error union ending in the expected payload
//!     (the error set itself is not checked because Zig infers an
//!     empty error set per fn for `pub fn foo() !void`).
//!
//! Each helper takes a caller-supplied `kind` prefix ("Tab module" /
//! "Command module") so the resulting compile errors point at the right
//! framework. Error messages include the offending decl name and a
//! verbatim `expected_signature` so the developer can copy-paste the fix.

const std = @import("std");

/// Assert a decl exists on `Container` with the exact expected type.
/// `kind` is a short prefix shown in error messages — typically
/// `"Tab module"` or `"Command module"`.
pub fn expectDeclWithType(
    comptime kind: []const u8,
    comptime mod_name: []const u8,
    comptime Container: type,
    comptime decl_name: []const u8,
    comptime ExpectedT: type,
    comptime expected_signature: []const u8,
) void {
    comptime {
        if (!@hasDecl(Container, decl_name)) {
            @compileError(kind ++ " `" ++ mod_name ++ "` is missing `" ++ decl_name ++
                "`. Expected:\n    " ++ expected_signature);
        }
        const ActualT = @TypeOf(@field(Container, decl_name));
        if (ActualT != ExpectedT) {
            @compileError(kind ++ " `" ++ mod_name ++ "`: `" ++ decl_name ++
                "` has wrong type.\n    Expected: " ++ @typeName(ExpectedT) ++
                "\n    Got:      " ++ @typeName(ActualT) ++
                "\n    Expected signature:\n    " ++ expected_signature);
        }
    }
}

/// Assert a function decl exists on `Container` with the exact fn-pointer
/// type. Use this for non-fallible functions (`fn (...) T`) or for fns
/// that explicitly use `anyerror`. For fns whose error set is inferred
/// (the common `pub fn foo() !void` shape), use `expectFnInferredError`.
pub fn expectFn(
    comptime kind: []const u8,
    comptime mod_name: []const u8,
    comptime Container: type,
    comptime decl_name: []const u8,
    comptime ExpectedT: type,
    comptime expected_signature: []const u8,
) void {
    comptime {
        if (!@hasDecl(Container, decl_name)) {
            @compileError(kind ++ " `" ++ mod_name ++ "` is missing `" ++ decl_name ++
                "`. Expected:\n    " ++ expected_signature);
        }
        const ActualT = @TypeOf(@field(Container, decl_name));
        if (ActualT != ExpectedT) {
            @compileError(kind ++ " `" ++ mod_name ++ "`: `" ++ decl_name ++
                "` has wrong signature.\n    Expected: " ++ @typeName(ExpectedT) ++
                "\n    Got:      " ++ @typeName(ActualT) ++
                "\n    Full expected declaration:\n    " ++ expected_signature);
        }
    }
}

/// Assert a fallible function decl exists on `Container` whose
/// parameter types match `expected_params` and whose return is an
/// error union ending in `ExpectedReturn`. The error set itself is
/// NOT checked — Zig infers a per-fn empty error set for `pub fn foo()
/// !void` whose name varies per call site, so exact-equality fails.
/// This loosens equality to "params match, return is `<some_err>!Return`".
pub fn expectFnInferredError(
    comptime kind: []const u8,
    comptime mod_name: []const u8,
    comptime Container: type,
    comptime decl_name: []const u8,
    comptime expected_params: []const type,
    comptime ExpectedReturn: type,
    comptime expected_signature: []const u8,
) void {
    comptime {
        if (!@hasDecl(Container, decl_name)) {
            @compileError(kind ++ " `" ++ mod_name ++ "` is missing `" ++ decl_name ++
                "`. Expected:\n    " ++ expected_signature);
        }
        const ActualT = @TypeOf(@field(Container, decl_name));
        const info = @typeInfo(ActualT);
        if (info != .@"fn") {
            @compileError(kind ++ " `" ++ mod_name ++ "`: `" ++ decl_name ++
                "` must be a function, got " ++ @typeName(ActualT) ++
                "\n    Full expected declaration:\n    " ++ expected_signature);
        }
        const fn_info = info.@"fn";

        // Verify params count + types.
        if (fn_info.params.len != expected_params.len) {
            @compileError(kind ++ " `" ++ mod_name ++ "`: `" ++ decl_name ++
                "` has wrong arity.\n    Got: " ++ @typeName(ActualT) ++
                "\n    Full expected declaration:\n    " ++ expected_signature);
        }
        for (expected_params, 0..) |Expected, i| {
            const ActualParam = fn_info.params[i].type orelse {
                @compileError(kind ++ " `" ++ mod_name ++ "`: `" ++ decl_name ++
                    "` parameter " ++ std.fmt.comptimePrint("{d}", .{i}) ++
                    " is `anytype`, but the contract requires `" ++ @typeName(Expected) ++ "`.\n" ++
                    "    Full expected declaration:\n    " ++ expected_signature);
            };
            if (ActualParam != Expected) {
                @compileError(kind ++ " `" ++ mod_name ++ "`: `" ++ decl_name ++
                    "` parameter " ++ std.fmt.comptimePrint("{d}", .{i}) ++
                    " has wrong type.\n    Expected: " ++ @typeName(Expected) ++
                    "\n    Got:      " ++ @typeName(ActualParam) ++
                    "\n    Full expected declaration:\n    " ++ expected_signature);
            }
        }

        // Verify return is an error union ending in ExpectedReturn.
        const ActualReturn = fn_info.return_type orelse {
            @compileError(kind ++ " `" ++ mod_name ++ "`: `" ++ decl_name ++
                "` has no return type.\n    Full expected declaration:\n    " ++ expected_signature);
        };
        const ret_info = @typeInfo(ActualReturn);
        if (ret_info != .error_union) {
            @compileError(kind ++ " `" ++ mod_name ++ "`: `" ++ decl_name ++
                "` must return an error union (`!" ++ @typeName(ExpectedReturn) ++
                "`), got " ++ @typeName(ActualReturn) ++
                "\n    Full expected declaration:\n    " ++ expected_signature);
        }
        if (ret_info.error_union.payload != ExpectedReturn) {
            @compileError(kind ++ " `" ++ mod_name ++ "`: `" ++ decl_name ++
                "` returns wrong payload type.\n    Expected: !" ++ @typeName(ExpectedReturn) ++
                "\n    Got:      " ++ @typeName(ActualReturn) ++
                "\n    Full expected declaration:\n    " ++ expected_signature);
        }
    }
}

// ── Tests ─────────────────────────────────────────────────────
//
// The validators themselves are comptime-only — runtime tests can
// only exercise the happy path (a decl with the right shape passes
// silently). The error paths are covered by the fact that every
// existing tab module compiles, so any breakage to these helpers
// surfaces immediately at `zig build`.

test "expectDeclWithType: matching type passes" {
    const M = struct {
        pub const label: []const u8 = "ok";
    };
    comptime expectDeclWithType("Test", "M", M, "label", []const u8, "pub const label: []const u8 = \"...\";");
}

test "expectFn: matching fn signature passes" {
    const M = struct {
        pub fn handler(_: u32) void {}
    };
    comptime expectFn("Test", "M", M, "handler", fn (u32) void, "pub fn handler(x: u32) void");
}

test "expectFnInferredError: !void function with correct params passes" {
    const M = struct {
        pub fn doit(_: u32, _: []const u8) !void {}
    };
    comptime expectFnInferredError(
        "Test",
        "M",
        M,
        "doit",
        &.{ u32, []const u8 },
        void,
        "pub fn doit(x: u32, s: []const u8) !void",
    );
}

test "expectFnInferredError: !T payload check works for non-void" {
    const M = struct {
        pub fn doit(_: u32) !u64 {
            return 0;
        }
    };
    comptime expectFnInferredError(
        "Test",
        "M",
        M,
        "doit",
        &.{u32},
        u64,
        "pub fn doit(x: u32) !u64",
    );
}
