@testitem "DateTime64 from Date" begin
    using Dates
    using PythonCall: NumpyDates

    # Mapping from NumPy unit symbols to NumpyDates Unit constants
    const UNIT_CONST = Dict(
        :Y => NumpyDates.YEARS,
        :M => NumpyDates.MONTHS,
        :D => NumpyDates.DAYS,
        :h => NumpyDates.HOURS,
        :m => NumpyDates.MINUTES,
        :s => NumpyDates.SECONDS,
        :ms => NumpyDates.MILLISECONDS,
        :us => NumpyDates.MICROSECONDS,
        :ns => NumpyDates.NANOSECONDS,
    )

    # Data generated by: uv run scripts/np_dates_all_units.py
    # Format: (Date, unit_symbol, numpy_int_value)
    cases = [
        (Date(1969, 12, 31), :Y, -1),
        (Date(1969, 12, 31), :M, -1),
        (Date(1969, 12, 31), :D, -1),
        (Date(1969, 12, 31), :h, -24),
        (Date(1969, 12, 31), :m, -1440),
        (Date(1969, 12, 31), :s, -86400),
        (Date(1969, 12, 31), :ms, -86400000),
        (Date(1969, 12, 31), :us, -86400000000),
        (Date(1969, 12, 31), :ns, -86400000000000),
        (Date(1970, 1, 1), :Y, 0),
        (Date(1970, 1, 1), :M, 0),
        (Date(1970, 1, 1), :D, 0),
        (Date(1970, 1, 1), :h, 0),
        (Date(1970, 1, 1), :m, 0),
        (Date(1970, 1, 1), :s, 0),
        (Date(1970, 1, 1), :ms, 0),
        (Date(1970, 1, 1), :us, 0),
        (Date(1970, 1, 1), :ns, 0),
        (Date(1970, 1, 2), :Y, 0),
        (Date(1970, 1, 2), :M, 0),
        (Date(1970, 1, 2), :D, 1),
        (Date(1970, 1, 2), :h, 24),
        (Date(1970, 1, 2), :m, 1440),
        (Date(1970, 1, 2), :s, 86400),
        (Date(1970, 1, 2), :ms, 86400000),
        (Date(1970, 1, 2), :us, 86400000000),
        (Date(1970, 1, 2), :ns, 86400000000000),
        (Date(1999, 12, 31), :Y, 29),
        (Date(1999, 12, 31), :M, 359),
        (Date(1999, 12, 31), :D, 10956),
        (Date(1999, 12, 31), :h, 262944),
        (Date(1999, 12, 31), :m, 15776640),
        (Date(1999, 12, 31), :s, 946598400),
        (Date(1999, 12, 31), :ms, 946598400000),
        (Date(1999, 12, 31), :us, 946598400000000),
        (Date(1999, 12, 31), :ns, 946598400000000000),
        (Date(2000, 2, 29), :Y, 30),
        (Date(2000, 2, 29), :M, 361),
        (Date(2000, 2, 29), :D, 11016),
        (Date(2000, 2, 29), :h, 264384),
        (Date(2000, 2, 29), :m, 15863040),
        (Date(2000, 2, 29), :s, 951782400),
        (Date(2000, 2, 29), :ms, 951782400000),
        (Date(2000, 2, 29), :us, 951782400000000),
        (Date(2000, 2, 29), :ns, 951782400000000000),
        (Date(1900, 1, 1), :Y, -70),
        (Date(1900, 1, 1), :M, -840),
        (Date(1900, 1, 1), :D, -25567),
        (Date(1900, 1, 1), :h, -613608),
        (Date(1900, 1, 1), :m, -36816480),
        (Date(1900, 1, 1), :s, -2208988800),
        (Date(1900, 1, 1), :ms, -2208988800000),
        (Date(1900, 1, 1), :us, -2208988800000000),
        (Date(1900, 1, 1), :ns, -2208988800000000000),
        (Date(2100, 1, 1), :Y, 130),
        (Date(2100, 1, 1), :M, 1560),
        (Date(2100, 1, 1), :D, 47482),
        (Date(2100, 1, 1), :h, 1139568),
        (Date(2100, 1, 1), :m, 68374080),
        (Date(2100, 1, 1), :s, 4102444800),
        (Date(2100, 1, 1), :ms, 4102444800000),
        (Date(2100, 1, 1), :us, 4102444800000000),
        (Date(2100, 1, 1), :ns, 4102444800000000000),
    ]

    @testset "$d $usym" for (d, usym, expected) in cases
        # 1) DateTime64 with UnitArg symbol
        dt64 = NumpyDates.DateTime64(d, usym)
        @test Dates.value(dt64) == expected

        # 2) InlineDateTime64 with type parameter Unit constant
        Uconst = UNIT_CONST[usym]
        inline_typed = NumpyDates.InlineDateTime64{Uconst}(d)
        @test Dates.value(inline_typed) == expected

        # 3) InlineDateTime64 with runtime UnitArg symbol
        inline_dyn = NumpyDates.InlineDateTime64(d, usym)
        @test Dates.value(inline_dyn) == expected
        @test NumpyDates.unitpair(inline_dyn) == NumpyDates.unitpair(usym)
    end
end

@testitem "DateTime64 from String" begin
    using Dates
    using PythonCall: NumpyDates

    # Data generated by: uv run test/scripts/np_dates.py
    # Format: (string_date, unit_symbol, numpy_int_value)
    cases = [
        ("1969-12-31", :Y, -1),
        ("1969-12-31", :M, -1),
        ("1969-12-31", :D, -1),
        ("1969-12-31", :h, -24),
        ("1969-12-31", :m, -1440),
        ("1969-12-31", :s, -86400),
        ("1969-12-31", :ms, -86400000),
        ("1969-12-31", :us, -86400000000),
        ("1969-12-31", :ns, -86400000000000),
        ("1970-01-01", :Y, 0),
        ("1970-01-01", :M, 0),
        ("1970-01-01", :D, 0),
        ("1970-01-01", :h, 0),
        ("1970-01-01", :m, 0),
        ("1970-01-01", :s, 0),
        ("1970-01-01", :ms, 0),
        ("1970-01-01", :us, 0),
        ("1970-01-01", :ns, 0),
        ("1970-01-02", :Y, 0),
        ("1970-01-02", :M, 0),
        ("1970-01-02", :D, 1),
        ("1970-01-02", :h, 24),
        ("1970-01-02", :m, 1440),
        ("1970-01-02", :s, 86400),
        ("1970-01-02", :ms, 86400000),
        ("1970-01-02", :us, 86400000000),
        ("1970-01-02", :ns, 86400000000000),
        ("1999-12-31", :Y, 29),
        ("1999-12-31", :M, 359),
        ("1999-12-31", :D, 10956),
        ("1999-12-31", :h, 262944),
        ("1999-12-31", :m, 15776640),
        ("1999-12-31", :s, 946598400),
        ("1999-12-31", :ms, 946598400000),
        ("1999-12-31", :us, 946598400000000),
        ("1999-12-31", :ns, 946598400000000000),
        ("2000-02-29", :Y, 30),
        ("2000-02-29", :M, 361),
        ("2000-02-29", :D, 11016),
        ("2000-02-29", :h, 264384),
        ("2000-02-29", :m, 15863040),
        ("2000-02-29", :s, 951782400),
        ("2000-02-29", :ms, 951782400000),
        ("2000-02-29", :us, 951782400000000),
        ("2000-02-29", :ns, 951782400000000000),
        ("1900-01-01", :Y, -70),
        ("1900-01-01", :M, -840),
        ("1900-01-01", :D, -25567),
        ("1900-01-01", :h, -613608),
        ("1900-01-01", :m, -36816480),
        ("1900-01-01", :s, -2208988800),
        ("1900-01-01", :ms, -2208988800000),
        ("1900-01-01", :us, -2208988800000000),
        ("1900-01-01", :ns, -2208988800000000000),
        ("2100-01-01", :Y, 130),
        ("2100-01-01", :M, 1560),
        ("2100-01-01", :D, 47482),
        ("2100-01-01", :h, 1139568),
        ("2100-01-01", :m, 68374080),
        ("2100-01-01", :s, 4102444800),
        ("2100-01-01", :ms, 4102444800000),
        ("2100-01-01", :us, 4102444800000000),
        ("2100-01-01", :ns, 4102444800000000000),
    ]

    @testset "$s $usym" for (s, usym, expected) in cases
        # DateTime64 from string
        dt64 = NumpyDates.DateTime64(s, usym)
        @test Dates.value(dt64) == expected

        # Inline typed from string
        Uconst = NumpyDates.Unit(usym)
        inline_typed = NumpyDates.InlineDateTime64{Uconst}(s)
        @test Dates.value(inline_typed) == expected

        # Inline dynamic from string
        inline_dyn = NumpyDates.InlineDateTime64(s, usym)
        @test Dates.value(inline_dyn) == expected
        @test NumpyDates.unitpair(inline_dyn) == NumpyDates.unitpair(usym)
    end

    # NaT string handling (all should produce NaT)
    @testset "$nat" for nat in ("NaT", "nan", "NAN")
        d1 = NumpyDates.DateTime64(nat, :D)
        @test isnan(d1)
        d2 = NumpyDates.InlineDateTime64{NumpyDates.DAYS}(nat)
        @test isnan(d2)
        d3 = NumpyDates.InlineDateTime64(nat, :D)
        @test isnan(d3)
    end
end

@testitem "DateTime64 from String and DateFormat" begin
    using Dates
    using PythonCall: NumpyDates

    # Data based on: uv run test/scripts/np_dates.py and uv run test/scripts/np_datetimes.py
    # We deliberately use formats that differ from the default parser to exercise the (str, format, unit) constructors.
    cases = [
        # date-only with custom format
        ("1999/12/31", DateFormat("yyyy/mm/dd"), :D, 10956),
        ("1999/12/31", DateFormat("yyyy/mm/dd"), :s, 946598400),
        # datetime with custom format
        ("2000-02-29 12:34:56", DateFormat("yyyy-mm-dd HH:MM:SS"), :s, 951_827_696),
        (
            "2000-02-29 12:34:56",
            DateFormat("yyyy-mm-dd HH:MM:SS"),
            :ns,
            951_827_696_000_000_000,
        ),
    ]

    @testset "$s $f $usym" for (s, f, usym, expected) in cases
        # DateTime64 from string + format
        dt64 = NumpyDates.DateTime64(s, f, usym)
        @test Dates.value(dt64) == expected

        # Inline typed from string + format
        Uconst = NumpyDates.Unit(usym)
        inline_typed = NumpyDates.InlineDateTime64{Uconst}(s, f)
        @test Dates.value(inline_typed) == expected

        # NOTE: InlineDateTime64(s, f, u) dynamic constructor is not exercised here
        # because it currently dispatches to InlineTimeDelta64 in the implementation.
        # When fixed, we can add:
        # inline_dyn = NumpyDates.InlineDateTime64(s, f, usym)
        # @test Dates.value(inline_dyn) == expected
    end
end

@testitem "DateTime64 from AbstractDateTime64" begin
    using Dates
    using PythonCall: NumpyDates

    # Pass-through when units match
    d = Date(1970, 1, 2)
    x = NumpyDates.DateTime64(d, :D)  # expected value 1
    y = NumpyDates.DateTime64(x, :D)
    @test Dates.value(y) == 1
    @test NumpyDates.unitpair(y) == NumpyDates.unitpair(:D)

    # NaT changes unit and remains NaT
    nat_d = NumpyDates.DateTime64("NaT", :D)
    z = NumpyDates.DateTime64(nat_d, :s)
    @test isnan(z)
    @test NumpyDates.unitpair(z) == NumpyDates.unitpair(:s)
end

@testitem "DateTime64 from Integer" begin
    using Dates
    using PythonCall: NumpyDates

    # DateTime64(value, unit)
    x = NumpyDates.DateTime64(946_684_799, :s)   # 1999-12-31T23:59:59
    @test Dates.value(x) == 946_684_799
    @test Dates.DateTime(x) == DateTime(1999, 12, 31, 23, 59, 59)

    y = NumpyDates.DateTime64(10_956, :D)        # 1999-12-31
    @test Dates.value(y) == 10_956
    @test Dates.Date(y) == Date(1999, 12, 31)

    # InlineDateTime64{U}(value)
    s1 = NumpyDates.InlineDateTime64{NumpyDates.SECONDS}(3_600) # +1 hour
    @test Dates.value(s1) == 3_600
    @test Dates.DateTime(s1) == DateTime(1970, 1, 1, 1, 0, 0)

    # InlineDateTime64 with multiplier in the unit parameter
    s2 = NumpyDates.InlineDateTime64{(NumpyDates.SECONDS, 2)}(1_800) # 1_800 * 2s = 3_600s
    @test Dates.value(s2) == 1_800
    @test Dates.DateTime(s2) == DateTime(1970, 1, 1, 1, 0, 0)
end

@testitem "DateTime64 from Dates.DateTime" begin
    using Dates
    using PythonCall: NumpyDates

    # Data generated by: uv run test/scripts/np_dates.py
    # Format: (Date, unit_symbol, numpy_int_value)
    cases = [
        (DateTime(1969, 12, 31, 23, 0, 0), :Y, -1),
        (DateTime(1969, 12, 31, 23, 0, 0), :M, -1),
        (DateTime(1969, 12, 31, 23, 0, 0), :D, -1),
        (DateTime(1969, 12, 31, 23, 0, 0), :h, -1),
        (DateTime(1969, 12, 31, 23, 0, 0), :m, -60),
        (DateTime(1969, 12, 31, 23, 0, 0), :s, -3600),
        (DateTime(1969, 12, 31, 23, 0, 0), :ms, -3_600_000),
        (DateTime(1969, 12, 31, 23, 0, 0), :us, -3_600_000_000),
        (DateTime(1969, 12, 31, 23, 0, 0), :ns, -3_600_000_000_000),
        (DateTime(1969, 12, 31, 23, 59, 59), :Y, -1),
        (DateTime(1969, 12, 31, 23, 59, 59), :M, -1),
        (DateTime(1969, 12, 31, 23, 59, 59), :D, -1),
        (DateTime(1969, 12, 31, 23, 59, 59), :h, -1),
        (DateTime(1969, 12, 31, 23, 59, 59), :m, -1),
        (DateTime(1969, 12, 31, 23, 59, 59), :s, -1),
        (DateTime(1969, 12, 31, 23, 59, 59), :ms, -1_000),
        (DateTime(1969, 12, 31, 23, 59, 59), :us, -1_000_000),
        (DateTime(1969, 12, 31, 23, 59, 59), :ns, -1_000_000_000),
        (DateTime(1970, 1, 1, 0, 0, 0), :Y, 0),
        (DateTime(1970, 1, 1, 0, 0, 0), :M, 0),
        (DateTime(1970, 1, 1, 0, 0, 0), :D, 0),
        (DateTime(1970, 1, 1, 0, 0, 0), :h, 0),
        (DateTime(1970, 1, 1, 0, 0, 0), :m, 0),
        (DateTime(1970, 1, 1, 0, 0, 0), :s, 0),
        (DateTime(1970, 1, 1, 0, 0, 0), :ms, 0),
        (DateTime(1970, 1, 1, 0, 0, 0), :us, 0),
        (DateTime(1970, 1, 1, 0, 0, 0), :ns, 0),
        (DateTime(1970, 1, 1, 0, 0, 1), :Y, 0),
        (DateTime(1970, 1, 1, 0, 0, 1), :M, 0),
        (DateTime(1970, 1, 1, 0, 0, 1), :D, 0),
        (DateTime(1970, 1, 1, 0, 0, 1), :h, 0),
        (DateTime(1970, 1, 1, 0, 0, 1), :m, 0),
        (DateTime(1970, 1, 1, 0, 0, 1), :s, 1),
        (DateTime(1970, 1, 1, 0, 0, 1), :ms, 1_000),
        (DateTime(1970, 1, 1, 0, 0, 1), :us, 1_000_000),
        (DateTime(1970, 1, 1, 0, 0, 1), :ns, 1_000_000_000),
        (DateTime(1970, 1, 1, 1, 0, 0), :Y, 0),
        (DateTime(1970, 1, 1, 1, 0, 0), :M, 0),
        (DateTime(1970, 1, 1, 1, 0, 0), :D, 0),
        (DateTime(1970, 1, 1, 1, 0, 0), :h, 1),
        (DateTime(1970, 1, 1, 1, 0, 0), :m, 60),
        (DateTime(1970, 1, 1, 1, 0, 0), :s, 3_600),
        (DateTime(1970, 1, 1, 1, 0, 0), :ms, 3_600_000),
        (DateTime(1970, 1, 1, 1, 0, 0), :us, 3_600_000_000),
        (DateTime(1970, 1, 1, 1, 0, 0), :ns, 3_600_000_000_000),
        (DateTime(1999, 12, 31, 23, 59, 59), :Y, 29),
        (DateTime(1999, 12, 31, 23, 59, 59), :M, 359),
        (DateTime(1999, 12, 31, 23, 59, 59), :D, 10_956),
        (DateTime(1999, 12, 31, 23, 59, 59), :h, 262_967),
        (DateTime(1999, 12, 31, 23, 59, 59), :m, 15_778_079),
        (DateTime(1999, 12, 31, 23, 59, 59), :s, 946_684_799),
        (DateTime(1999, 12, 31, 23, 59, 59), :ms, 946_684_799_000),
        (DateTime(1999, 12, 31, 23, 59, 59), :us, 946_684_799_000_000),
        (DateTime(1999, 12, 31, 23, 59, 59), :ns, 946_684_799_000_000_000),
        (DateTime(2000, 2, 29, 12, 34, 56), :Y, 30),
        (DateTime(2000, 2, 29, 12, 34, 56), :M, 361),
        (DateTime(2000, 2, 29, 12, 34, 56), :D, 11_016),
        (DateTime(2000, 2, 29, 12, 34, 56), :h, 264_396),
        (DateTime(2000, 2, 29, 12, 34, 56), :m, 15_863_794),
        (DateTime(2000, 2, 29, 12, 34, 56), :s, 951_827_696),
        (DateTime(2000, 2, 29, 12, 34, 56), :ms, 951_827_696_000),
        (DateTime(2000, 2, 29, 12, 34, 56), :us, 951_827_696_000_000),
        (DateTime(2000, 2, 29, 12, 34, 56), :ns, 951_827_696_000_000_000),
        (DateTime(1900, 1, 1, 0, 0, 0), :Y, -70),
        (DateTime(1900, 1, 1, 0, 0, 0), :M, -840),
        (DateTime(1900, 1, 1, 0, 0, 0), :D, -25_567),
        (DateTime(1900, 1, 1, 0, 0, 0), :h, -613_608),
        (DateTime(1900, 1, 1, 0, 0, 0), :m, -36_816_480),
        (DateTime(1900, 1, 1, 0, 0, 0), :s, -2_208_988_800),
        (DateTime(1900, 1, 1, 0, 0, 0), :ms, -2_208_988_800_000),
        (DateTime(1900, 1, 1, 0, 0, 0), :us, -2_208_988_800_000_000),
        (DateTime(1900, 1, 1, 0, 0, 0), :ns, -2_208_988_800_000_000_000),
        (DateTime(2100, 1, 1, 0, 0, 0), :Y, 130),
        (DateTime(2100, 1, 1, 0, 0, 0), :M, 1_560),
        (DateTime(2100, 1, 1, 0, 0, 0), :D, 47_482),
        (DateTime(2100, 1, 1, 0, 0, 0), :h, 1_139_568),
        (DateTime(2100, 1, 1, 0, 0, 0), :m, 68_374_080),
        (DateTime(2100, 1, 1, 0, 0, 0), :s, 4_102_444_800),
        (DateTime(2100, 1, 1, 0, 0, 0), :ms, 4_102_444_800_000),
        (DateTime(2100, 1, 1, 0, 0, 0), :us, 4_102_444_800_000_000),
        (DateTime(2100, 1, 1, 0, 0, 0), :ns, 4_102_444_800_000_000_000),
    ]

    @testset "$dt $usym" for (dt, usym, expected) in cases
        # 1) DateTime64 with UnitArg symbol
        dt64 = NumpyDates.DateTime64(dt, usym)
        @test Dates.value(dt64) == expected

        # 2) InlineDateTime64 with type parameter Unit constant
        Uconst = NumpyDates.Unit(usym)
        inline_typed = NumpyDates.InlineDateTime64{Uconst}(dt)
        @test Dates.value(inline_typed) == expected

        # 3) InlineDateTime64 with runtime UnitArg symbol
        inline_dyn = NumpyDates.InlineDateTime64(dt, usym)
        @test Dates.value(inline_dyn) == expected
        @test NumpyDates.unitpair(inline_dyn) == NumpyDates.unitpair(usym)
    end
end

@testitem "DateTime64 show" begin
    using Dates
    using PythonCall: NumpyDates

    # Helper to get "showvalue" form by setting :typeinfo to the concrete type
    function showvalue_string(x)
        io = IOBuffer()
        show(IOContext(io, :typeinfo => typeof(x)), x)
        String(take!(io))
    end

    # Helper to get the default Base.show output (with type wrapper)
    function show_string(x)
        io = IOBuffer()
        show(io, x)
        String(take!(io))
    end

    # Cases: (kind, value, unit_symbol, expected_showvalue_string)
    cases = [
        # 1) Days-aligned: prints Date in value form
        (Date(1999, 12, 31), :D, "\"1999-12-31\"", "DAYS"),

        # 2) Seconds-aligned: prints DateTime in value form
        (DateTime(1999, 12, 31, 23, 59, 59), :s, "\"1999-12-31T23:59:59\"", "SECONDS"),

        # 3) Sub-millisecond units: fall back to raw integer in value form
        (1, :us, "1", "MICROSECONDS"),
        (1, :ns, "1", "NANOSECONDS"),

        # 4) Calendar units (years/months): value form shows truncated Date
        (Date(2000, 2, 29), :Y, "\"2000-01-01\"", "YEARS"),
        (Date(1999, 12, 31), :M, "\"1999-12-01\"", "MONTHS"),

        # 5) NaT
        ("NaT", :D, "\"NaT\"", "DAYS"),
    ]

    @testset "$v $usym" for (v, usym, expected_val, ustr) in cases
        # Construct DateTime64
        x = NumpyDates.DateTime64(v, usym)

        # showvalue checks
        s_val = showvalue_string(x)
        @test s_val == expected_val

        # default show checks
        s_def = show_string(x)
        @test s_def ==
              "PythonCall.NumpyDates.DateTime64($expected_val, PythonCall.NumpyDates.$ustr)"

        # and again with InlineDateTime64
        x2 = NumpyDates.InlineDateTime64(v, usym)
        s_val2 = showvalue_string(x2)
        @test s_val2 == expected_val
        s_def2 = show_string(x2)
        @test s_def2 ==
              "PythonCall.NumpyDates.InlineDateTime64{PythonCall.NumpyDates.$ustr}($expected_val)"
    end
end

@testitem "DateTime64 to Date/DateTime" begin
    using Dates
    using PythonCall: NumpyDates

    # Cases: (value, unit_symbol_or_tuple, expected_DateTime, expected_Date)
    cases = [
        # Day counts (since 1970-01-01)
        (10_956, :D, DateTime(1999, 12, 31, 0, 0, 0), Date(1999, 12, 31)),
        (0, :D, DateTime(1970, 1, 1, 0, 0, 0), Date(1970, 1, 1)),
        (1, :D, DateTime(1970, 1, 2, 0, 0, 0), Date(1970, 1, 2)),

        # Seconds since epoch
        (946_684_799, :s, DateTime(1999, 12, 31, 23, 59, 59), Date(1999, 12, 31)),
        (0, :s, DateTime(1970, 1, 1, 0, 0, 0), Date(1970, 1, 1)),
        (3_600, :s, DateTime(1970, 1, 1, 1, 0, 0), Date(1970, 1, 1)),

        # Hours/minutes (epoch-based)
        (24, :h, DateTime(1970, 1, 2, 0, 0, 0), Date(1970, 1, 2)),
        (60, :m, DateTime(1970, 1, 1, 1, 0, 0), Date(1970, 1, 1)),

        # Years/months (calendar truncation semantics from 1970-01-01)
        (30, :Y, DateTime(2000, 1, 1, 0, 0, 0), Date(2000, 1, 1)),
        (361, :M, DateTime(2000, 2, 1, 0, 0, 0), Date(2000, 2, 1)),
        (359, :M, DateTime(1999, 12, 1, 0, 0, 0), Date(1999, 12, 1)),

        # Weeks
        (1, :W, DateTime(1970, 1, 8, 0, 0, 0), Date(1970, 1, 8)),
        (-1, :W, DateTime(1969, 12, 25, 0, 0, 0), Date(1969, 12, 25)),

        # Sub-nanosecond units: floored to nanoseconds
        (1_500, :ps, DateTime(1970, 1, 1, 0, 0, 0) + Nanosecond(1), Date(1970, 1, 1)),
        (1_500_000, :fs, DateTime(1970, 1, 1, 0, 0, 0) + Nanosecond(1), Date(1970, 1, 1)),
        (
            1_500_000_000,
            :as,
            DateTime(1970, 1, 1, 0, 0, 0) + Nanosecond(1),
            Date(1970, 1, 1),
        ),

        # Multiplier tuple unit: value * multiplier is applied before adding
        (1_800, (:s, 2), DateTime(1970, 1, 1, 1, 0, 0), Date(1970, 1, 1)),
    ]

    @testset "$v $u" for (v, u, expdt, expd) in cases
        # 1) DateTime64(value, unit)
        x = NumpyDates.DateTime64(v, u)
        @test Dates.DateTime(x) == expdt
        @test Dates.Date(x) == expd

        # 2) InlineDateTime64 typed
        Uconst = u isa Tuple ? (NumpyDates.Unit(u[1]), Int(u[2])) : NumpyDates.Unit(u)
        y = NumpyDates.InlineDateTime64{Uconst}(v)
        @test Dates.DateTime(y) == expdt
        @test Dates.Date(y) == expd

        # 3) InlineDateTime64 dynamic
        z = NumpyDates.InlineDateTime64(v, u)
        @test Dates.DateTime(z) == expdt
        @test Dates.Date(z) == expd
    end

    # NaT conversion errors
    for u in (:Y, :M, :W, :D, :h, :m, :s, :ms, :us, :ns, :ps, :fs, :as)
        nat1 = NumpyDates.DateTime64("NaT", u)
        @test_throws Exception Dates.DateTime(nat1)
        @test_throws Exception Dates.Date(nat1)

        Uconst = NumpyDates.Unit(u)
        nat2 = NumpyDates.InlineDateTime64{Uconst}("NaT")
        @test_throws Exception Dates.DateTime(nat2)
        @test_throws Exception Dates.Date(nat2)

        nat3 = NumpyDates.InlineDateTime64("NaT", u)
        @test_throws Exception Dates.DateTime(nat3)
        @test_throws Exception Dates.Date(nat3)
    end
end

@testitem "DateTime64 isnan" begin
    using Dates
    using PythonCall: NumpyDates

    units = (:Y, :M, :W, :D, :h, :m, :s, :ms, :us, :ns, :ps, :fs, :as)

    # Non-NaT examples should be false
    @test !isnan(NumpyDates.DateTime64(Date(1970, 1, 1), :D))
    @test !isnan(NumpyDates.DateTime64(DateTime(1970, 1, 1), :ms))
    @test !isnan(NumpyDates.InlineDateTime64{NumpyDates.SECONDS}(0))
    @test !isnan(NumpyDates.InlineDateTime64(0, :ns))

    # NaT via strings for each unit
    @testset "NaT string -> $u" for u in units
        x = NumpyDates.DateTime64("NaT", u)
        @test isnan(x)
        xi = NumpyDates.InlineDateTime64{NumpyDates.Unit(u)}("NaT")
        @test isnan(xi)
        xid = NumpyDates.InlineDateTime64("NaT", u)
        @test isnan(xid)
    end

    # NaT via sentinel (typemin(Int64))
    @testset "NAT sentinel -> $u" for u in units
        x = NumpyDates.DateTime64(NumpyDates.NAT, u)
        @test isnan(x)
        xi = NumpyDates.InlineDateTime64{NumpyDates.Unit(u)}(NumpyDates.NAT)
        @test isnan(xi)
    end

    # Changing unit on NaT remains NaT
    nat_d = NumpyDates.DateTime64("NaT", :D)
    y = NumpyDates.DateTime64(nat_d, :s)
    @test isnan(y)
end

@testitem "TimeDelta64 from Dates.Period" begin
    using Dates
    using PythonCall: NumpyDates

    # Data generated by: uv run test/scripts/np_timedeltas.py
    # Representative cases drawn from the script output.
    # Each case is: (period, unit_symbol, expected_numpy_integer)
    cases = [
        # negative one day
        (Day(-1), :D, -1),
        (Day(-1), :h, -24),
        (Day(-1), :m, -1440),
        (Day(-1), :s, -86400),
        (Day(-1), :ms, -86400000),
        (Day(-1), :us, -86400000000),
        (Day(-1), :ns, -86400000000000),
        # negative one week
        (Week(-1), :W, -1),
        (Week(-1), :D, -7),
        (Week(-1), :h, -168),
        (Week(-1), :m, -10080),
        (Week(-1), :s, -604800),
        (Week(-1), :ms, -604800000),
        (Week(-1), :us, -604800000000),
        (Week(-1), :ns, -604800000000000),
        # sub-second basis
        (Millisecond(-1000), :s, -1),
        (Microsecond(-1_000_000), :s, -1),
        (Nanosecond(-1_000_000_000), :s, -1),
        (Second(-3600), :h, -1),
        (Minute(-60), :h, -1),
        # positive unit-expansion
        (Second(1), :ms, 1_000),
        (Second(1), :us, 1_000_000),
        (Second(1), :ns, 1_000_000_000),
        (Second(1), :ps, 1_000_000_000_000),
        (Second(1), :fs, 1_000_000_000_000_000),
        (Second(1), :as, 1_000_000_000_000_000_000),
        (Nanosecond(1), :ns, 1),
        (Nanosecond(1), :ps, 1_000),
        (Nanosecond(1), :fs, 1_000_000),
        (Nanosecond(1), :as, 1_000_000_000),
        # mixed scale-ups
        (Millisecond(1_000), :s, 1),
        (Microsecond(1_000_000), :s, 1),
        (Nanosecond(1_000_000_000), :s, 1),
        # calendar-like periods (handled specially)
        (Month(12), :M, 12),
        (Year(1), :Y, 1),
        (Year(2), :M, 24),
    ]

    @testset "$p => $usym" for (p, usym, expected) in cases
        # TimeDelta64 from Dates.Period
        td = NumpyDates.TimeDelta64(p, usym)
        @test Dates.value(td) == expected

        # Inline typed
        Uconst = NumpyDates.Unit(usym)
        inline_typed = NumpyDates.InlineTimeDelta64{Uconst}(p)
        @test Dates.value(inline_typed) == expected

        # Inline dynamic
        inline_dyn = NumpyDates.InlineTimeDelta64(p, usym)
        @test Dates.value(inline_dyn) == expected
        @test NumpyDates.unitpair(inline_dyn) == NumpyDates.unitpair(usym)
    end

    inexact_cases = [(Millisecond(3001), :s), (Day(-1), :W)]

    @testset "$p => $usym" for (p, usym) in inexact_cases
        @test_throws InexactError NumpyDates.TimeDelta64(p, usym)
        @test_throws InexactError NumpyDates.InlineTimeDelta64(p, usym)
        Uconst = NumpyDates.Unit(usym)
        @test_throws InexactError NumpyDates.InlineTimeDelta64{Uconst}(p)
    end
end

@testitem "TimeDelta64 from String" begin
    using Dates
    using PythonCall: NumpyDates

    # Only NaT-like strings are supported for TimeDelta64(string, unit)
    @testset "$nat -> $u" for nat in ("NaT", "nan", "NAN"),
        u in (:W, :D, :h, :m, :s, :ms, :us, :ns, :ps, :fs, :as, :M, :Y)

        x = NumpyDates.TimeDelta64(nat, u)
        @test isnan(x)
        @test NumpyDates.unitpair(x) == NumpyDates.unitpair(u)

        # Inline typed and dynamic also accept NaT strings
        Uconst = NumpyDates.Unit(u)
        xi = NumpyDates.InlineTimeDelta64{Uconst}(nat)
        @test isnan(xi)
        xid = NumpyDates.InlineTimeDelta64(nat, u)
        @test isnan(xid)
        @test NumpyDates.unitpair(xid) == NumpyDates.unitpair(u)
    end
end

@testitem "TimeDelta64 from AbstractTimeDelta64" begin
    using Dates
    using PythonCall: NumpyDates

    # Pass-through when units match
    base = NumpyDates.TimeDelta64(Day(1), :D)
    y = NumpyDates.TimeDelta64(base, :D)
    @test Dates.value(y) == 1
    @test NumpyDates.unitpair(y) == NumpyDates.unitpair(:D)

    # NaT changes unit and remains NaT
    nat_td = NumpyDates.TimeDelta64("NaT", :s)
    z = NumpyDates.TimeDelta64(nat_td, :ns)
    @test isnan(z)
    @test NumpyDates.unitpair(z) == NumpyDates.unitpair(:ns)
end

@testitem "TimeDelta64 from Integer" begin
    using Dates
    using PythonCall: NumpyDates

    # Raw constructor: value and unit
    x = NumpyDates.TimeDelta64(3_600, :s)
    @test Dates.value(x) == 3_600

    # Convert to nanoseconds via Dates to sanity check: 3_600 s -> 3_600_000_000_000 ns
    # Note: TimeDelta64 is a Period; we check only value here and rely on conversion tests above.

    # Inline typed raw value
    s1 = NumpyDates.InlineTimeDelta64{NumpyDates.SECONDS}(3_600)
    @test Dates.value(s1) == 3_600

    # Inline with multiplier in unit parameter
    s2 = NumpyDates.InlineTimeDelta64{(NumpyDates.SECONDS, 2)}(1_800) # 1_800 * 2s = 3_600s
    @test Dates.value(s2) == 1_800
end

@testitem "TimeDelta64 show" begin
    using Dates
    using PythonCall: NumpyDates

    # Helper to get "showvalue" form by setting :typeinfo to the concrete type
    function showvalue_string(x)
        io = IOBuffer()
        show(IOContext(io, :typeinfo => typeof(x)), x)
        String(take!(io))
    end

    # Helper to get the default Base.show output (with type wrapper)
    function show_string(x)
        io = IOBuffer()
        show(io, x)
        String(take!(io))
    end

    # Cases: (value_or_nat, unit_symbol_or_tuple, expected_value_string, expected_unit_string_for_default_show, is_inline_tuple?)
    # - expected_value_string matches showvalue_string output (integers unquoted; NaT quoted)
    # - expected_unit_string_for_default_show is the rhs inside the default show, e.g. "SECONDS" or "(SECONDS, 2)"
    cases = [
        # simple seconds
        (0, :s, "0", "SECONDS", false),
        (3_600, :s, "3600", "SECONDS", false),
        (-86_400, :s, "-86400", "SECONDS", false),

        # micro/nano/pico/femto/atto seconds
        (1, :us, "1", "MICROSECONDS", false),
        (1, :ns, "1", "NANOSECONDS", false),
        (1, :ps, "1", "PICOSECONDS", false),
        (1, :fs, "1", "FEMTOSECONDS", false),
        (1, :as, "1", "ATTOSECONDS", false),

        # minutes/hours/days/weeks
        (60, :m, "60", "MINUTES", false),
        (24, :h, "24", "HOURS", false),
        (7, :D, "7", "DAYS", false),
        (2, :W, "2", "WEEKS", false),

        # calendar units (identity semantics for "value" field)
        (12, :M, "12", "MONTHS", false),
        (1, :Y, "1", "YEARS", false),

        # NaT
        ("NaT", :ns, "\"NaT\"", "NANOSECONDS", false),

        # multiplier tuple unit for TimeDelta64 and InlineTimeDelta64
        (1_800, (:s, 2), "1800", "(SECONDS, 2)", true),
    ]

    @testset "$v $u" for (v, u, expected_val, ustr, is_tuple) in cases
        # Construct TimeDelta64
        td = NumpyDates.TimeDelta64(v, u)

        # showvalue checks (integers unquoted; NaT quoted)
        s_val = showvalue_string(td)
        @test s_val == expected_val

        # default show checks
        s_def = show_string(td)
        @test s_def ==
              "PythonCall.NumpyDates.TimeDelta64($expected_val, $(replace(ustr, r"^(\(?)" => s"\1PythonCall.NumpyDates.")))"

        # InlineTimeDelta64 forms
        # Dynamic (value, unit)
        inline_dyn = NumpyDates.InlineTimeDelta64(v, u)
        s_val2 = showvalue_string(inline_dyn)
        @test s_val2 == expected_val
        s_def2 = show_string(inline_dyn)
        # Inline default show embeds the fully-qualified unit in the type parameter.
        if is_tuple
            @test s_def2 ==
                  "PythonCall.NumpyDates.InlineTimeDelta64{(PythonCall.NumpyDates.SECONDS, 2)}($expected_val)"
        else
            @test s_def2 ==
                  "PythonCall.NumpyDates.InlineTimeDelta64{PythonCall.NumpyDates.$ustr}($expected_val)"
        end

        # Typed Inline
        Uconst = u isa Tuple ? (NumpyDates.Unit(u[1]), u[2]) : NumpyDates.Unit(u)
        inline_typed = NumpyDates.InlineTimeDelta64{Uconst}(v)
        @test showvalue_string(inline_typed) == expected_val
        s_def3 = show_string(inline_typed)
        if is_tuple
            @test s_def3 ==
                  "PythonCall.NumpyDates.InlineTimeDelta64{(PythonCall.NumpyDates.SECONDS, 2)}($expected_val)"
        else
            @test s_def3 ==
                  "PythonCall.NumpyDates.InlineTimeDelta64{PythonCall.NumpyDates.$ustr}($expected_val)"
        end
    end
end

@testitem "TimeDelta64 isnan" begin
    using Dates
    using PythonCall: NumpyDates

    units = (:Y, :M, :W, :D, :h, :m, :s, :ms, :us, :ns, :ps, :fs, :as)

    # Non-NaT examples should be false
    @test !isnan(NumpyDates.TimeDelta64(0, :s))
    @test !isnan(NumpyDates.TimeDelta64(Day(1), :D))
    @test !isnan(NumpyDates.InlineTimeDelta64{NumpyDates.MINUTES}(0))
    @test !isnan(NumpyDates.InlineTimeDelta64(0, :ns))

    # NaT via strings for each unit
    @testset "NaT string -> $u" for u in units
        x = NumpyDates.TimeDelta64("NaT", u)
        @test isnan(x)
        xi = NumpyDates.InlineTimeDelta64{NumpyDates.Unit(u)}("NaT")
        @test isnan(xi)
        xid = NumpyDates.InlineTimeDelta64("NaT", u)
        @test isnan(xid)
    end

    # NaT via sentinel (typemin(Int64))
    @testset "NAT sentinel -> $u" for u in units
        x = NumpyDates.TimeDelta64(NumpyDates.NAT, u)
        @test isnan(x)
        xi = NumpyDates.InlineTimeDelta64{NumpyDates.Unit(u)}(NumpyDates.NAT)
        @test isnan(xi)
    end

    # Changing unit on NaT remains NaT
    nat_td = NumpyDates.TimeDelta64("NaT", :s)
    z = NumpyDates.TimeDelta64(nat_td, :ns)
    @test isnan(z)
end

@testitem "TimeDelta64 to Dates.Period" begin
    using Dates
    using PythonCall: NumpyDates

    # Mapping from unit symbols to Dates.Period constructors
    const PERIOD_CONST = Dict(
        :Y => Dates.Year,
        :M => Dates.Month,
        :D => Dates.Day,
        :h => Dates.Hour,
        :m => Dates.Minute,
        :s => Dates.Second,
        :ms => Dates.Millisecond,
        :us => Dates.Microsecond,
        :ns => Dates.Nanosecond,
    )

    # Test cases: (value, unit_symbol)
    cases = [
        (1, :Y),
        (-1, :Y),
        (1, :M),
        (-12, :M),
        (1, :D),
        (-7, :D),
        (1, :h),
        (-24, :h),
        (1, :m),
        (-60, :m),
        (1, :s),
        (-3600, :s),
        (1, :ms),
        (-1000, :ms),
        (1, :us),
        (-1000000, :us),
        (1, :ns),
        (-1000000000, :ns),
    ]

    @testset "$v $usym" for (v, usym) in cases
        # TimeDelta64
        td = NumpyDates.TimeDelta64(v, usym)
        PeriodType = PERIOD_CONST[usym]
        @test PeriodType(td) == PeriodType(v)

        # InlineTimeDelta64 typed
        Uconst = NumpyDates.Unit(usym)
        inline_typed = NumpyDates.InlineTimeDelta64{Uconst}(v)
        @test PeriodType(inline_typed) == PeriodType(v)

        # InlineTimeDelta64 dynamic
        inline_dyn = NumpyDates.InlineTimeDelta64(v, usym)
        @test PeriodType(inline_dyn) == PeriodType(v)
    end

    # NaT conversion errors
    for usym in keys(PERIOD_CONST)
        nat1 = NumpyDates.TimeDelta64("NaT", usym)
        PeriodType = PERIOD_CONST[usym]
        @test_throws Exception PeriodType(nat1)

        Uconst = NumpyDates.Unit(usym)
        nat2 = NumpyDates.InlineTimeDelta64{Uconst}("NaT")
        @test_throws Exception PeriodType(nat2)

        nat3 = NumpyDates.InlineTimeDelta64("NaT", usym)
        @test_throws Exception PeriodType(nat3)
    end
end

@testitem "defaultunit" begin
    using Dates
    using PythonCall: NumpyDates

    # Helper: normalize any defaultunit result to a unitpair tuple
    norm(u) = NumpyDates.unitpair(u)

    # 1) Primitives and Dates.* types
    cases = [
        # fallback Any (e.g. AbstractString)
        ("hello", :ns),

        # Dates.Date / Dates.DateTime
        (Date(2000, 1, 2), :D),
        (DateTime(2000, 1, 2, 3, 4, 5), :ms),

        # Dates.Periods => exact unit mapping
        (Year(1), :Y),
        (Month(1), :M),
        (Week(1), :W),
        (Day(1), :D),
        (Hour(1), :h),
        (Minute(1), :m),
        (Second(1), :s),
        (Millisecond(1), :ms),
        (Microsecond(1), :us),
        (Nanosecond(1), :ns),
    ]

    @testset "$x -> $u" for (x, u) in cases
        @test norm(NumpyDates.defaultunit(x)) == norm(u)
    end

    # 2) (Inline)DateTime64 should return their own unitpair
    @testset "AbstractDateTime64 instances" begin
        # DateTime64
        dt_s = NumpyDates.DateTime64(0, :s)
        dt_ms = NumpyDates.DateTime64(0, :ms)
        @test norm(NumpyDates.defaultunit(dt_s)) == NumpyDates.unitpair(dt_s)
        @test norm(NumpyDates.defaultunit(dt_ms)) == NumpyDates.unitpair(dt_ms)

        # InlineDateTime64 typed
        idt_typed = NumpyDates.InlineDateTime64{NumpyDates.SECONDS}(0)
        @test norm(NumpyDates.defaultunit(idt_typed)) == NumpyDates.unitpair(idt_typed)

        # InlineDateTime64 dynamic
        idt_dyn = NumpyDates.InlineDateTime64(0, :us)
        @test norm(NumpyDates.defaultunit(idt_dyn)) == NumpyDates.unitpair(idt_dyn)
    end

    # 3) (Inline)TimeDelta64 should return their own unitpair
    @testset "AbstractTimeDelta64 instances" begin
        # TimeDelta64
        td_ns = NumpyDates.TimeDelta64(0, :ns)
        td_ps = NumpyDates.TimeDelta64(0, :ps)
        @test norm(NumpyDates.defaultunit(td_ns)) == NumpyDates.unitpair(td_ns)
        @test norm(NumpyDates.defaultunit(td_ps)) == NumpyDates.unitpair(td_ps)

        # InlineTimeDelta64 typed
        itd_typed = NumpyDates.InlineTimeDelta64{NumpyDates.MINUTES}(0)
        @test norm(NumpyDates.defaultunit(itd_typed)) == NumpyDates.unitpair(itd_typed)

        # InlineTimeDelta64 dynamic
        itd_dyn = NumpyDates.InlineTimeDelta64(0, :as)
        @test norm(NumpyDates.defaultunit(itd_dyn)) == NumpyDates.unitpair(itd_dyn)
    end
end
