pub const Duration = @import("Duration.zig");
pub const Instant = @import("Instant.zig");
pub const Now = @import("Now.zig");
pub const PlainDate = @import("PlainDate.zig");
pub const PlainDateTime = @import("PlainDateTime.zig");
pub const PlainMonthDay = @import("PlainMonthDay.zig");
pub const PlainTime = @import("PlainTime.zig");
pub const PlainYearMonth = @import("PlainYearMonth.zig");
pub const ZonedDateTime = @import("ZonedDateTime.zig");

const Temporal = @This();

test Temporal {
    const std = @import("std");
    _ = @import("abi.zig");

    const expected_scopes = .{
        "Duration",
        "Instant",
        "Now",
        "PlainDate",
        "PlainDateTime",
        "PlainMonthDay",
        "PlainTime",
        "PlainYearMonth",
        "ZonedDateTime",
    };

    inline for (expected_scopes) |scope| {
        const has = @hasDecl(Temporal, scope);
        if (!has) std.log.err("Missing Temporal scope: {s}", .{scope});
        try std.testing.expect(has);
    }
}

test Duration {
    const checks = .{
        // Constructor
        "init", // Temporal.Duration()
        // Static methods
        "compare",
        "from",

        // Instance methods
        "abs",
        "add",
        "negated",
        "round",
        "subtract",
        "toJSON",
        "toLocaleString",
        "toString",
        "total",
        "valueOf",
        // "with",

        // Properties
        "blank",
        "days",
        "hours",
        "microseconds",
        "milliseconds",
        "minutes",
        "months",
        "nanoseconds",
        "seconds",
        "sign",
        "weeks",
        "years",

        // Public types
        "ToStringOptions",
        "PartialDuration",
        "RelativeTo",
        "Unit",
        "RoundingMode",
        "RoundingOptions",
        "ToStringRoundingOptions",
        "Sign",
        "TotalOptions",
    };

    try assertDecls(Duration, checks);
}

test Instant {
    const checks = .{
        // Constructor
        "init", // Temporal.Instant()

        // Static methods
        "compare",
        "from",
        "fromEpochMilliseconds",
        "fromEpochNanoseconds",

        // Instance methods
        "add",
        "equals",
        "round",
        "since",
        "subtract",
        "toJSON",
        "toLocaleString",
        "toString",
        "toZonedDateTimeISO",
        "until",
        "valueOf",

        // Properties
        "epochMilliseconds",
        "epochNanoseconds",

        // Public types
        "ToStringOptions",
        "TimeZone",
        "Unit",
        "RoundingMode",
        "Sign",
        "RoundingOptions",
        "DifferenceSettings",
    };

    try assertDecls(Instant, checks);
}

test Now {
    const checks = .{
        // Static methods
        "instant",
        "plainDateISO",
        "plainDateTimeISO",
        "plainTimeISO",
        "timeZoneId",
        "zonedDateTimeISO",
    };

    try assertDecls(Now, checks);
}

test PlainDate {
    const checks = .{
        // Constructor
        "init",
        "initWithCalendar",

        // Static methods
        "compare",
        "from",

        // Instance methods
        "add",
        "equals",
        "since",
        "subtract",
        "toJSON",
        "toLocaleString",
        "toPlainDateTime",
        "toPlainMonthDay",
        "toPlainYearMonth",
        "toString",
        "toZonedDateTime",
        "until",
        "valueOf",
        "with",
        "withCalendar",

        // Properties (now as methods)
        "calendarId",
        "day",
        "dayOfWeek",
        "dayOfYear",
        "daysInMonth",
        "daysInWeek",
        "daysInYear",
        "era",
        "eraYear",
        "inLeapYear",
        "month",
        "monthCode",
        "monthsInYear",
        "weekOfYear",
        "year",
        "yearOfWeek",

        // Public types
        "ToStringOptions",
        "CalendarDisplay",
        "ToZonedDateTimeOptions",
        "Unit",
        "RoundingMode",
        "Sign",
        "DifferenceSettings",
    };

    try assertDecls(PlainDate, checks);
}

test PlainDateTime {
    const checks = .{
        // Constructor
        "init", // Temporal.PlainDateTime()
        "initWithCalendar",

        // Static methods
        "compare",
        "from",
        // "fromUtf8",
        // "fromUtf16",

        // Instance methods
        "add",
        "equals",
        "round",
        "since",
        "subtract",
        "toJSON",
        "toLocaleString",
        "toPlainDate",
        "toPlainTime",
        "toString",
        "toZonedDateTime",
        "until",
        "valueOf",
        "with",
        "withCalendar",
        "withPlainTime",

        // Properties
        "calendarId",
        "day",
        "dayOfWeek",
        "dayOfYear",
        "daysInMonth",
        "daysInWeek",
        "daysInYear",
        "era",
        "eraYear",
        "hour",
        "inLeapYear",
        "microsecond",
        "millisecond",
        "minute",
        "month",
        "monthCode",
        "monthsInYear",
        "nanosecond",
        "second",
        "weekOfYear",
        "year",
        "yearOfWeek",

        // Public types
        "Unit",
        "RoundingMode",
        "Sign",
        "CalendarDisplay",
        "DifferenceSettings",
        "RoundOptions",
        "ToStringOptions",
        "ToZonedDateTimeOptions",
        "WithOptions",
    };

    try assertDecls(PlainDateTime, checks);
}

test PlainMonthDay {
    const checks = .{
        // Constructor
        "init", // Temporal.PlainMonthDay()

        // Static methods
        "from",

        // Instance methods
        "equals",
        "toJSON",
        "toLocaleString",
        "toPlainDate",
        "toString",
        "valueOf",
        "with",

        // Properties
        "calendarId",
        "day",
        "monthCode",

        // Public types
        "CalendarDisplay",
        "ToStringOptions",
        "WithOptions",
    };

    try assertDecls(PlainMonthDay, checks);
}

test PlainTime {
    const checks = .{
        // Constructor
        "init", // Temporal.PlainTime()

        // Static methods
        "compare",
        "from",

        // Instance methods
        "add",
        "equals",
        "round",
        "since",
        "subtract",
        "toJSON",
        "toLocaleString",
        "toString",
        "until",
        "valueOf",
        "with",

        // Properties
        "hour",
        "microsecond",
        "millisecond",
        "minute",
        "nanosecond",
        "second",

        // Public types
        "Unit",
        "RoundingMode",
        "DifferenceSettings",
        "RoundOptions",
        "WithOptions",
    };

    try assertDecls(PlainTime, checks);
}

test PlainYearMonth {
    const checks = .{
        // Constructor
        "init", // Temporal.PlainYearMonth()

        // Static methods
        "compare",
        "from",

        // Instance methods
        "add",
        "equals",
        "since",
        "subtract",
        "toJSON",
        "toLocaleString",
        "toPlainDate",
        "toString",
        "until",
        "valueOf",
        "with",

        // Properties
        "calendarId",
        "daysInMonth",
        "daysInYear",
        "era",
        "eraYear",
        "inLeapYear",
        "month",
        "monthCode",
        "monthsInYear",
        "year",

        // Public types
        "Unit",
        "RoundingMode",
        "CalendarDisplay",
        "DifferenceSettings",
        "RoundOptions",
        "ToStringOptions",
        "WithOptions",
    };

    try assertDecls(PlainYearMonth, checks);
}

test ZonedDateTime {
    const checks = .{
        // Constructor
        "init", // Temporal.ZonedDateTime()

        // Static methods
        "compare",
        "from",
        "fromEpochMilliseconds",
        "fromEpochNanoseconds",

        // Instance methods
        "add",
        "clone",
        "equals",
        "getTimeZoneTransition",
        "round",
        "since",
        "startOfDay",
        "subtract",
        "toInstant",
        "toJSON",
        "toLocaleString",
        "toPlainDate",
        "toPlainDateTime",
        "toPlainTime",
        "toString",
        "until",
        "valueOf",
        "with",
        "withCalendar",
        "withPlainTime",
        "withTimeZone",

        // Properties
        "calendarId",
        "day",
        "dayOfWeek",
        "dayOfYear",
        "daysInMonth",
        "daysInWeek",
        "daysInYear",
        "epochMilliseconds",
        "epochNanoseconds",
        "era",
        "eraYear",
        "hour",
        "hoursInDay",
        "inLeapYear",
        "microsecond",
        "millisecond",
        "minute",
        "month",
        "monthCode",
        "monthsInYear",
        "nanosecond",
        "offset",
        "offsetNanoseconds",
        "second",
        "timeZoneId",
        "weekOfYear",
        "year",
        "yearOfWeek",

        // Public types
        "Unit",
        "RoundingMode",
        "Sign",
        "DifferenceSettings",
        "RoundOptions",
        "TimeZone",
        "Disambiguation",
        "OffsetDisambiguation",
        "CalendarDisplay",
        "DisplayOffset",
        "DisplayTimeZone",
        "ToStringOptions",
    };

    try assertDecls(ZonedDateTime, checks);
}

fn assertDecls(comptime T: type, checks: anytype) !void {
    @setEvalBranchQuota(5000); // Increase branch quota for large check lists
    const std = @import("std");
    const typeInfo = @typeInfo(T);

    // Check: all items in checks exist (either as decls or as fields)
    inline for (checks) |check| {
        const should_ignore =
            std.mem.eql(u8, check, "deinit") or
            std.mem.eql(u8, check, "valueOf");

        if (!should_ignore) {
            const hasDecl = @hasDecl(T, check);

            // Also check if it's a field (property)
            var hasField = false;
            if (typeInfo == .@"struct") {
                const struct_info = typeInfo.@"struct";
                inline for (struct_info.fields) |field| {
                    // Check both camelCase and snake_case
                    if (std.mem.eql(u8, field.name, check) or
                        std.mem.eql(u8, field.name, camelToSnakeCase(check)))
                    {
                        hasField = true;
                        break;
                    }
                }
            }

            const has = hasDecl or hasField;
            if (!has) std.log.err("Missing {s} decl or field: {s}", .{ @typeName(T), check });
            try std.testing.expect(has);
        }
    }

    // Check: no extraneous declarations or fields beyond checks
    if (typeInfo == .@"struct") {
        const struct_info = typeInfo.@"struct";

        // Check declarations
        inline for (struct_info.decls) |decl| {
            // Allow deinit as extraneous
            if (comptime std.mem.eql(u8, decl.name, "deinit")) continue;

            var found = false;
            inline for (checks) |check| {
                if (std.mem.eql(u8, decl.name, check)) {
                    found = true;
                    break;
                }
            }

            if (!found) {
                std.log.err("Extraneous {s} decl: {s}", .{ @typeName(T), decl.name });
                try std.testing.expect(false);
            }
        }

        // Check fields (properties)
        inline for (struct_info.fields) |field| {
            // Allow internal fields (starting with underscore)
            if (comptime std.mem.startsWith(u8, field.name, "_")) continue;

            var found = false;
            inline for (checks) |check| {
                if (std.mem.eql(u8, field.name, camelToSnakeCase(check))) {
                    found = true;
                    break;
                }
            }

            if (!found) {
                std.log.err("Extraneous {s} field: {s}", .{ @typeName(T), field.name });
                try std.testing.expect(false);
            }
        }
    }
}

fn camelToSnakeCase(comptime input: []const u8) []const u8 {
    comptime var len: usize = undefined;
    comptime {
        var llen: usize = input.len;
        for (input, 0..) |c, i| {
            if (c >= 'A' and c <= 'Z' and i != 0) {
                llen += 1;
            }
        }
        len = llen;
    }

    comptime var result: [len]u8 = undefined;
    comptime var write_index: usize = 0;

    comptime {
        for (input, 0..) |c, i| {
            if (c >= 'A' and c <= 'Z') {
                if (i != 0) {
                    result[write_index] = '_';
                    write_index += 1;
                }
                result[write_index] = c + 32; // Convert to lowercase
            } else {
                result[write_index] = c;
            }
            write_index += 1;
        }
    }

    const final = result;
    return &final;
}
