using Test, Dates, UUIDs, StructUtils

struct TestStyle <: StructUtils.StructStyle end
struct StrictUnknownFieldStyle <: StructUtils.StructStyle end
struct UnknownFieldTestError <: Exception
    target::Any
    key::Any
end

StructUtils.unknownfield(::StrictUnknownFieldStyle, ::Type{T}, key, value) where {T} =
    throw(UnknownFieldTestError(T, key))

mutable struct CountingTagStyle <: StructUtils.StructStyle
    calls::Int
end

struct CountingTagged
    a::Int
    b::Int
    c::Int
end

function StructUtils.fieldtags(st::CountingTagStyle, ::Type{CountingTagged})
    st.calls += 1
    return (a=(name="a",), b=(name="b",), c=(name="c",))
end

mutable struct PerFieldTagStyle <: StructUtils.StructStyle
    calls::Int
end

struct PerFieldTagged
    x::Int
    y::Int
end

function StructUtils.fieldtags(st::PerFieldTagStyle, ::Type{PerFieldTagged}, field)
    st.calls += 1
    field === :x && return (name="a",)
    field === :y && return (name="b",)
    return (;)
end

include(joinpath(dirname(pathof(StructUtils)), "../test/macros.jl"))
include(joinpath(dirname(pathof(StructUtils)), "../test/struct.jl"))
include(joinpath(dirname(pathof(StructUtils)), "../test/selectors.jl"))
include(joinpath(dirname(pathof(StructUtils)), "../test/construction.jl"))
include(joinpath(dirname(pathof(StructUtils)), "../test/ignore_inbound.jl"))
include(joinpath(dirname(pathof(StructUtils)), "../test/iso_lift.jl"))

@testset "StructUtils" begin

# Dict{Symbol, Int}, NamedTuple, A struct, Vector Pair
d = Dict(:a => 1, :b => 2, :c => 3, :d => 4)
ds = Dict("a" => 1, "b" => 2, "c" => 3, "d" => 4)
nt = (a = 1, b = 2, c = 3, d = 4)
a = A(1, 2, 3, 4)
aa = AA(1, 2, 3, 4, 5)
b = B(); b.a = 1; b.b = 2; b.c = 3; b.d = 4; b
bb = BB()
vp = [:a => 1, :b => 2, :c => 3, :d => 4]
v = [1, 2, 3, 4]
t = (1, 2, 3, 4)

println("Dict{Symbol, Int}")
@test StructUtils.make(Dict{Symbol, Int}, d) == d
@test StructUtils.make(Dict{Symbol, Int}, d) !== d
@test StructUtils.make(Dict{Symbol, Int}, ds) == d
@test StructUtils.make(Dict{Symbol, Int}, nt) == d
@test StructUtils.make(Dict{Symbol, Int}, a) == d
@test StructUtils.make(Dict{Symbol, Int}, vp) == d
# @test StructUtils.make(Dict{Symbol, Int}, aa) == d # fails because of extra field
@test StructUtils.make(Dict{Symbol, Int}, v) == Dict(Symbol(1) => 1, Symbol(2) => 2, Symbol(3) => 3, Symbol(4) => 4)
@test StructUtils.make(Dict{Symbol, Int}, t) == Dict(Symbol(1) => 1, Symbol(2) => 2, Symbol(3) => 3, Symbol(4) => 4)
@test StructUtils.make(Dict{Symbol, Int}, b) == d
@test StructUtils.make(Dict{Symbol, Int}, bb) == d
dmut = Dict{Symbol, Int}()
StructUtils.make!(dmut, d)
@test dmut == d

println("NamedTuple")
@test StructUtils.make(typeof(nt), d) == nt
@test StructUtils.make(typeof(nt), ds) == nt
@test StructUtils.make(typeof(nt), nt) == nt
@test StructUtils.make(typeof(nt), a) == nt
@test StructUtils.make(typeof(nt), vp) == nt
@test StructUtils.make(typeof(nt), v) == nt
@test StructUtils.make(typeof(nt), t) == nt
@test StructUtils.make(typeof(nt), aa) == nt # extra field is ignored
@test StructUtils.make(typeof(nt), b) == nt
@test StructUtils.make(typeof(nt), bb) == nt

println("A struct")
@test StructUtils.make(A, d) == a
@test StructUtils.make(A, ds) == a # slower
@test StructUtils.make(A, nt) == a
@test StructUtils.make(A, a) == a
@test StructUtils.make(A, vp) == a
@test StructUtils.make(A, v) == a # relies on order of vector elements
@test StructUtils.make(A, t) == a # relies on order of tuple elements
@test StructUtils.make(A, aa) == a # extra field is ignored
@test StructUtils.make(A, b) == a
@test StructUtils.make(A, bb) == a

println("AA struct")
@test StructUtils.make(AA, d) == aa # works because of AA field e default
@test StructUtils.make(AA, ds) == aa
@test StructUtils.make(AA, nt) == aa
@test StructUtils.make(AA, a) == aa
@test StructUtils.make(AA, vp) == aa
@test StructUtils.make(AA, v) == aa # relies on order of vector elements
@test StructUtils.make(AA, t) == aa # relies on order of tuple elements
@test StructUtils.make(AA, aa) == aa # extra field is ignored
@test StructUtils.make(AA, b) == aa
@test StructUtils.make(AA, bb) == aa

println("B struct")
@test StructUtils.make(B, d) == b
@test StructUtils.make(B, ds) == b
@test StructUtils.make(B, nt) == b
@test StructUtils.make(B, a) == b
@test StructUtils.make(B, bb) == b
bmut = B()
StructUtils.make!(bmut, d)
@test bmut == b

println("BB struct")
@test StructUtils.make(BB, d) == bb
@test StructUtils.make(BB, ds) == bb
@test StructUtils.make(BB, nt) == bb
@test StructUtils.make(BB, a) == bb
@test StructUtils.make(BB, vp) == bb
@test StructUtils.make(BB, v) == bb # relies on order of vector elements
@test StructUtils.make(BB, t) == bb # relies on order of tuple elements
@test StructUtils.make(BB, aa) == bb # extra field is ignored
@test StructUtils.make(BB, b) == bb
@test StructUtils.make(BB, bb) == bb

println("Vector Pair")
# @test StructUtils.make(typeof(vp), d) == vp # unordered Dict doesn't work
# @test StructUtils.make(typeof(vp), ds) == vp # unordered Dict doesn't work
@test StructUtils.make(typeof(vp), nt) == vp
@test StructUtils.make(typeof(vp), a) == vp
@test StructUtils.make(typeof(vp), vp) == vp

println("Vector")
# @test StructUtils.make(typeof(v), d) == v # relies on order of Dict elements
# @test StructUtils.make(typeof(v), ds) == v # relies on order of Dict elements
@test StructUtils.make(typeof(v), nt) == v
@test StructUtils.make(typeof(v), a) == v
@test StructUtils.make(typeof(v), vp) == v
@test StructUtils.make(typeof(v), v) == v
@test StructUtils.make(typeof(v), v) !== v
@test StructUtils.make(typeof(v), t) == v
# @test StructUtils.make(typeof(v), aa) == v # fails because of extra field

println("Abstract collections")
ad = Dict("a" => 1)
av = [1, 2, 3]

@test StructUtils.make(AbstractDict, ad) === ad
@test StructUtils.make(AbstractArray, av) === av
@test StructUtils.make(AbstractVector, av) === av
@test StructUtils.make!(AbstractDict, ad) === ad
@test StructUtils.make!(AbstractArray, av) === av
@test StructUtils.make!(AbstractVector, av) === av

x = StructUtils.make(AbstractDictHolder, Dict("d" => ad))
@test x.d === ad
x = StructUtils.make(AbstractArrayHolder, Dict("a" => av))
@test x.a === av
x = StructUtils.make(AbstractVectorHolder, Dict("v" => av))
@test x.v === av

println("Tuple")
# @test StructUtils.make(typeof(t), d) == t # relies on order of Dict elements
# @test StructUtils.make(typeof(t), ds) == t # relies on order of Dict elements
@test StructUtils.make(typeof(t), nt) == t
@test StructUtils.make(typeof(t), a) == t
@test StructUtils.make(typeof(t), vp) == t
@test StructUtils.make(typeof(t), v) == t
@test StructUtils.make(typeof(t), t) == t
@test StructUtils.make(typeof(t), aa) == t

@testset "unknownfield hook" begin
    @test StructUtils.make(A, nt, StrictUnknownFieldStyle()) == a
    @test_throws UnknownFieldTestError StructUtils.make(A, (a=1, b=2, c=3, d=4, e=5), StrictUnknownFieldStyle())
    @test_throws UnknownFieldTestError StructUtils.make(typeof(nt), aa, StrictUnknownFieldStyle())
    @test_throws UnknownFieldTestError StructUtils.make(typeof(t), [1, 2, 3, 4, 5], StrictUnknownFieldStyle())

    pmut2 = P()
    StructUtils.make!(pmut2, (id=0, name="Jane"); style=StrictUnknownFieldStyle())
    @test pmut2.id == 0 && pmut2.name == "Jane"
    @test_throws UnknownFieldTestError StructUtils.make!(pmut2, (id=0, name="Joan", rate=3.14); style=StrictUnknownFieldStyle())
end

@testset "target fieldtags are cached during make" begin
    style = CountingTagStyle(0)
    @test StructUtils.make(CountingTagged, (a=1, b=2, c=3), style) == CountingTagged(1, 2, 3)
    @test style.calls == 1

    perfield = PerFieldTagStyle(0)
    @test StructUtils.make(PerFieldTagged, (a=10, b=20), perfield) == PerFieldTagged(10, 20)
    @test perfield.calls == 2
end

println("C")
@test StructUtils.make(C, ()) == C()
@test StructUtils.make(C, (1,)) == C()

println("D")
@test StructUtils.make(D, (1, 3.14, "hey")) == D(1, 3.14, "hey")

println("Wrapper")
@test StructUtils.make(Wrapper, Dict("x" => Dict("a" => 1, "b" => "hey"))) == Wrapper((a=1, b="hey"))

println("UndefGuy")
x = StructUtils.make(UndefGuy, (id=1, name="2"))
@test x.id == 1 && x.name == "2"
x = StructUtils.make(UndefGuy, (id=1,))
@test x.id == 1 && !isdefined(x, :name)
x = UndefGuy()
StructUtils.make!(x, (id=1, name="2"))
@test x.id == 1 && x.name == "2"
x = UndefGuy()
StructUtils.make!(x, (id=1,))
@test x.id == 1 && !isdefined(x, :name)

println("E")
@test StructUtils.make(E, Dict("id" => 1, "a" => (a=1, b=2, c=3, d=4))) == E(1, A(1, 2, 3, 4))

println("G")
@test StructUtils.make(G, Dict("id" => 1, "rate" => 3.14, "name" => "Jim", "f" => Dict("id" => 2, "rate" => 6.28, "name" => "Bob"))) == G(1, 3.14, "Jim", F(2, 6.28, "Bob"))

println("H")
x = StructUtils.make(H, (id=0, name="", properties=Dict("a" => 1), addresses=["a", "b", "c"]))
@test x.id == 0 && x.name == "" && x.properties == Dict("a" => 1) && x.addresses == ["a", "b", "c"]

println("I")
@test StructUtils.make(I, (id=2, name="Aubrey", fruit=apple)) == I(2, "Aubrey", apple)

println("Vehicle")
StructUtils.@choosetype Vehicle x -> x["type"] == "car" ? Car : x["type"] == "truck" ? Truck : throw(ArgumentError("Unknown vehicle type: $(x["type"])"))

x = StructUtils.make(Vehicle, Dict("type" => "car", "make" => "Toyota", "model" => "Corolla", "seatingCapacity" => 4, "topSpeed" => 120.5))
@test x == Car("Toyota", "Corolla", 4, 120.5)

println("J")
@test StructUtils.make(J, (id=1, name=nothing, rate=3.14)) == J(1, nothing, 3.14)
@test StructUtils.make(J, (id=nothing, name=nothing, rate=3)) == J(nothing, nothing, 3)

println("Recurs")
@test StructUtils.make(Recurs, (id=0, value=(id=1, value=(id=2, value=(id=3, value=(id=4, value=nothing)))))) == Recurs(0, Recurs(1, Recurs(2, Recurs(3, Recurs(4, nothing)))))

println("O")
@test StructUtils.make(O, (id=0, name=missing)) == O(0, missing)
@test StructUtils.make(O, (id=0, name=nothing)) == O(0, nothing)
@test StructUtils.make(O, (id=0, name=(id=2, first_name="Jim", rate=3.14))) == O(0, L(2, "Jim", 3.14))
@test StructUtils.make(O, (id=0, name=(id=2, name="Jane", fruit=banana))) == O(0, I(2, "Jane", banana))

println("P")
p = StructUtils.make(P, (id=0, name="Jane"))
@test p.id == 0 && p.name == "Jane"
pmut = P()
StructUtils.make!(pmut, (id=0, name="Jane"))
@test pmut.id == 0 && pmut.name == "Jane"
StructUtils.make!(pmut, (id=0, name="Joan", rate=3.14))
@test pmut.id == 0 && pmut.name == "Joan"

println("Multi-dimensional arrays")
x = StructUtils.make(Matrix{Int}, [[1, 2, 3], [4, 5, 6], [7, 8, 9]])
@test x == [1 4 7; 2 5 8; 3 6 9]

println("Point")
@test StructUtils.make(Point, (x=1, y=2)) == Point(1, 2)
@test StructUtils.make(Dict{Point, Dict{Point, Point}}, Dict(Point(1, 2) => Dict(Point(3, 4) => Point(5, 6)))) == Dict(Point(1, 2) => Dict(Point(3, 4) => Point(5, 6)))
StructUtils.lowerkey(::StructUtils.StructStyle, p::Point) = "$(p.x)_$(p.y)"
StructUtils.liftkey(::StructUtils.StructStyle, ::Type{Point}, key::String) = Point(parse(Int, split(key, "_")[1]), parse(Int, split(key, "_")[2]))
@test StructUtils.make(Dict{Point, Dict{Point, Point}}, Dict("1_2" => Dict("3_4" => Point(5, 6)))) == Dict(Point(1, 2) => Dict(Point(3, 4) => Point(5, 6)))
@test StructUtils.make(Dict{Point, Dict{Point, Point}}, Dict(Point(1, 2) => Dict(Point(3, 4) => Point(5, 6)))) == Dict(Point(1, 2) => Dict(Point(3, 4) => Point(5, 6)))

# utils
@test StructUtils.applylength([1, 2, 3]) == 3
p = P()
p.id = 1
@atomic p.name = "Jane"
StructUtils.reset!(p)
@test p.name == "Jim"

println("Q")
@test StructUtils.make(Q, (id=0, value="application/json")) == Q(0, MIME("application/json"))

@testset "arraylike public API" begin
    @test StructUtils.arraylike([1]) == true
    @test StructUtils.arraylike((1, 2)) == true
    @test StructUtils.arraylike(Set([1])) == true
    @test StructUtils.arraylike(1) == false
end

@testset "applyeach with Pair" begin
    # Basic functionality - collect key-value pairs
    collected = []
    ret = StructUtils.applyeach(StructUtils.DefaultStyle(), (k, v) -> push!(collected, (k, v)), :a => 42)
    @test collected == [(:a, 42)]
    @test ret === nothing

    # Test EarlyReturn short-circuiting
    collected = []
    ret = StructUtils.applyeach(StructUtils.DefaultStyle(), (k, v) -> begin
        push!(collected, (k, v))
        return StructUtils.EarlyReturn("found")
    end, :x => 100)
    @test collected == [(:x, 100)]
    @test ret isa StructUtils.EarlyReturn
    @test ret.value == "found"

    # Test that lowerkey and lower are applied
    struct CustomStyle <: StructUtils.StructStyle end
    StructUtils.lowerkey(::CustomStyle, x::Symbol) = string(x)
    StructUtils.lower(::CustomStyle, x::Int) = x * 2
    StructUtils.defaultstate(::CustomStyle) = :custom_state

    collected = []
    ret = StructUtils.applyeach(CustomStyle(), (k, v) -> push!(collected, (k, v)), :test => 5)
    @test collected == [("test", 10)]  # key lowered to string, value doubled
    @test ret === :custom_state

    # Test EarlyReturn with custom style
    collected = []
    ret = StructUtils.applyeach(CustomStyle(), (k, v) -> begin
        push!(collected, (k, v))
        k == "test" && return StructUtils.EarlyReturn(:early)
        return nothing
    end, :test => 3)
    @test collected == [("test", 6)]
    @test ret isa StructUtils.EarlyReturn
    @test ret.value === :early

    # Test with nested structures
    collected = []
    StructUtils.applyeach(StructUtils.DefaultStyle(), (k, v) -> push!(collected, (k, v)), :nested => Dict(:a => 1, :b => 2))
    @test length(collected) == 1
    @test collected[1][1] === :nested
    @test collected[1][2] == Dict(:a => 1, :b => 2)

    # Test with nothing as value
    collected = []
    ret = StructUtils.applyeach(StructUtils.DefaultStyle(), (k, v) -> push!(collected, (k, v)), :key => nothing)
    @test collected == [(:key, nothing)]
    @test ret === nothing

    # Test that make works with Pair as source
    @test StructUtils.make(Dict{Symbol, Int}, :a => 1) == Dict(:a => 1)
    @test StructUtils.make(NamedTuple{(:a,), Tuple{Int}}, :a => 42) == (a=42,)
end

@testset "multidimensional make" begin
    # 2D Int matrix from nested vectors
    x2 = StructUtils.make(Matrix{Int}, [[1, 3], [2, 4]])
    @test x2 == [1 2; 3 4]

    # 2D Float64 matrix from singleton inner vectors
    x2f = StructUtils.make(Matrix{Float64}, [[1.0], [2.0]])
    m2f = Matrix{Float64}(undef, 1, 2)
    m2f[1] = 1.0
    m2f[2] = 2.0
    @test x2f == m2f

    # 2D Missing matrix
    xm = StructUtils.make(Matrix{Missing}, [[missing, missing], [missing, missing]])
    @test isequal(xm, [missing missing; missing missing])

    # 3D Int array
    # structure: two slabs, each a 2×1 slice
    x3 = StructUtils.make(Array{Int,3}, [[[1, 2]], [[3, 4]]])
    exp3 = Array{Int,3}(undef, 2, 1, 2)
    # fill slice [:, :, 1] from first slab [[1,2]]
    exp3[:, :, 1] = reshape([1,2], 2, 1)
    # fill slice [:, :, 2] from second slab [[3,4]]
    exp3[:, :, 2] = reshape([3,4], 2, 1)
    @test x3 == exp3

    # Jagged nested vectors (Vector of Vectors)
    jagged = [[1, 2], [3, 4, 5], [6]]
    result = StructUtils.make(Vector{Vector{Int}}, jagged)
    @test result == jagged

    # Multi-dimensional case with jagged inner vectors
    vv_jagged = [[[1, 2], [3]], [[4, 5, 6], [7, 8]]]
    result_md = StructUtils.make(Array{Vector{Int}, 2}, vv_jagged)

    md_jagged = Array{Vector{Int}}(undef, 2, 2)
    md_jagged[1, 1] = [1, 2]
    md_jagged[2, 1] = [3]
    md_jagged[1, 2] = [4, 5, 6]
    md_jagged[2, 2] = [7, 8]
    @test result_md == md_jagged
end

@testset "@nonstruct macro" begin
    # Test basic @nonstruct functionality
    @test !StructUtils.structlike(StructUtils.DefaultStyle(), NonStructUnit)
    @test !StructUtils.structlike(StructUtils.DefaultStyle(), NonStructComplex)
    
    # Test that structlike returns false for @nonstruct types with style
    @test !StructUtils.structlike(StructUtils.DefaultStyle(), NonStructUnit)
    @test !StructUtils.structlike(StructUtils.DefaultStyle(), NonStructComplex)
    
    # Test that @nonstruct works with type parameters
    @test !StructUtils.structlike(StructUtils.DefaultStyle(), NonStructUnit)
    @test !StructUtils.structlike(StructUtils.DefaultStyle(), NonStructComplex)
end

@testset "@nonstruct representation in make: #29" begin
    mapping = Dict{String,Any}("leaf" => 1)
    value = NonStructMapping(mapping)

    # Root values use the same representation as values nested in a source.
    @test StructUtils.make(Dict{String,Any}, value) == mapping
    @test StructUtils.make(NonStructMappingTarget, value) == NonStructMappingTarget(1)

    @test StructUtils.make(
        Dict{String,Any},
        NonStructMappingHolder(value),
    ) == Dict{String,Any}("value" => mapping)
    @test StructUtils.make(Vector{Any}, Any[value]) == Any[mapping]
    @test StructUtils.make(
        Dict{String,Any},
        Dict{String,Any}("value" => value),
    ) == Dict{String,Any}("value" => mapping)

    # Lowering may produce a concrete value satisfying an abstract target.
    @test StructUtils.make(AbstractDict, value) === mapping
    @test StructUtils.make(
        AbstractNonStructMappingTarget,
        value,
    ) == ConcreteNonStructMappingTarget(1)

    # Style-specific lowering is honored exactly once at the root.
    style = NonStructMappingStyle(0)
    @test StructUtils.make(Dict{String,Any}, value, style) == mapping
    @test style.lower_calls == 1

    # A target-owned make hook still receives the raw source.
    hook_style = NonStructMappingStyle(0)
    made = StructUtils.make(NonStructCustomMakeTarget, value, hook_style)
    @test made.saw_raw_source
    @test hook_style.lower_calls == 0

    # Making a non-struct target continues to use lift on the raw source.
    @test StructUtils.make(NonStructMapping, mapping).value === mapping
end

@testset "struct-like lower may call make: #66" begin
    style = RecursiveLowerStyle()
    value = RecursiveLowerRoot(RecursiveLowerLeaf(2), "root")
    expected = Dict{String,Any}(
        "leaf" => Dict{String,Any}("value" => 2),
        "label" => "root",
    )

    @test StructUtils.lower(style, value) == expected
    @test StructUtils.make(style, Dict{String,Any}, value) == (expected, nothing)
    @test StructUtils.make(Dict{String,Any}, value, style) == expected
end

@testset "Set make: #13" begin
    @test StructUtils.make(Set{Symbol}, Any["FACTOR"]) == Set{Symbol}([:FACTOR])
end

@testset "Empty NamedTuple structlike" begin
    # Test that empty named tuple value is struct-like
    empty_nt = (;)
    @test StructUtils.structlike(empty_nt) == true
    @test StructUtils.structlike(StructUtils.DefaultStyle(), empty_nt) == true
    
    # Test that empty named tuple type is struct-like
    @test StructUtils.structlike(typeof(empty_nt)) == true
    @test StructUtils.structlike(StructUtils.DefaultStyle(), typeof(empty_nt)) == true
    @test StructUtils.structlike(NamedTuple{(), Tuple{}}) == true
    @test StructUtils.structlike(StructUtils.DefaultStyle(), NamedTuple{(), Tuple{}}) == true
    
    # Test that make works with empty named tuples
    @test StructUtils.make(typeof(empty_nt), (;)) == (;)
    @test StructUtils.make(NamedTuple{(), Tuple{}}, Dict()) == (;)
    @test StructUtils.make(Dict{Symbol, Int}, (;)) == Dict{Symbol, Int}()
end

@testset "Union{scalar, array} disambiguation" begin
    # basic: scalar source → scalar type, array source → array type
    @test StructUtils.make(ScalarOrVec, (val=3.14,)).val === 3.14
    @test StructUtils.make(ScalarOrVec, (val=[1.0, 2.0],)).val == [1.0, 2.0]
    @test StructUtils.make(ScalarOrVec, (val=[1.0, 2.0],)).val isa Vector{Float64}

    # Any-typed sources (the case that actually fails without disambiguation)
    @test StructUtils.make(ScalarOrVec, Dict{String,Any}("val" => 3.14)).val === 3.14
    x = StructUtils.make(ScalarOrVec, Dict{String,Any}("val" => Any[1.0, 2.0]))
    @test x.val isa Vector{Float64}
    @test x.val == [1.0, 2.0]

    # String variant
    @test StructUtils.make(ScalarOrVecStr, (val="hello",)).val == "hello"
    x = StructUtils.make(ScalarOrVecStr, Dict{String,Any}("val" => Any["a", "b"]))
    @test x.val isa Vector{String}
    @test x.val == ["a", "b"]

    # Int variant
    @test StructUtils.make(ScalarOrVecInt, (val=42,)).val === 42
    x = StructUtils.make(ScalarOrVecInt, Dict{String,Any}("val" => Any[1, 2, 3]))
    @test x.val isa Vector{Int}
    @test x.val == [1, 2, 3]

    # with Nothing — Nothing is peeled first, then array/scalar split
    @test StructUtils.make(ScalarOrVecNothing, (val=nothing,)).val === nothing
    @test StructUtils.make(ScalarOrVecNothing, (val=5,)).val === 5
    x = StructUtils.make(ScalarOrVecNothing, Dict{String,Any}("val" => Any[1, 2]))
    @test x.val isa Vector{Int}
    @test x.val == [1, 2]

    # with Missing — Missing is peeled first, then array/scalar split
    @test StructUtils.make(ScalarOrVecMissing, (val=missing,)) == ScalarOrVecMissing(missing)
    @test StructUtils.make(ScalarOrVecMissing, (val=1.5,)).val === 1.5
    x = StructUtils.make(ScalarOrVecMissing, Dict{String,Any}("val" => Any[1.0, 2.0]))
    @test x.val isa Vector{Float64}
    @test x.val == [1.0, 2.0]

    # empty array → array type
    x = StructUtils.make(ScalarOrVec, (val=Float64[],))
    @test x.val isa Vector{Float64}
    @test isempty(x.val)

    # nested struct with Union field
    x = StructUtils.make(ScalarOrVecNested, Dict{String,Any}("id" => 1, "data" => Any[1.0, 2.0, 3.0]))
    @test x.id == 1
    @test x.data isa Vector{Float64}
    @test x.data == [1.0, 2.0, 3.0]
    x = StructUtils.make(ScalarOrVecNested, (id=1, data=3.14))
    @test x.id == 1
    @test x.data === 3.14

    # the original issue: Tuple with complex nested Union types
    x = StructUtils.make(FrankenTuple, (params=(nothing, [1.0, 2.0, 3.0], nothing),))
    @test x.params[1] === nothing
    @test x.params[2] isa Vector{Float64}
    @test x.params[2] == [1.0, 2.0, 3.0]
    @test x.params[3] === nothing
    # same tuple, all scalars
    x = StructUtils.make(FrankenTuple, (params=(1.0, 2.0, 3.0),))
    @test x.params[1] === 1.0
    @test x.params[2] === 2.0
    @test x.params[3] === 3.0

    # ambiguous: two arraylike types → should NOT be handled, falls through
    @test_throws Exception StructUtils.make(@NamedTuple{val::Union{Vector{Int}, Set{Int}}}, Dict{String,Any}("val" => Any[1, 2]))

    # Vector of structs with Union fields
    x = StructUtils.make(Vector{ScalarOrVec}, [(val=1.0,), (val=[2.0, 3.0],)])
    @test x[1].val === 1.0
    @test x[2].val isa Vector{Float64}
    @test x[2].val == [2.0, 3.0]
end

@testset "BigInt/BigFloat structlike" begin
    # BigInt and BigFloat are mutable structs in Julia, but should be treated
    # as non-struct types for serialization purposes (JuliaIO/JSON.jl#424)
    @test StructUtils.structlike(BigInt) == false
    @test StructUtils.structlike(BigFloat) == false
    @test StructUtils.structlike(StructUtils.DefaultStyle(), BigInt) == false
    @test StructUtils.structlike(StructUtils.DefaultStyle(), BigFloat) == false
end

@testset "keyeq with Tuple" begin
    # Test basic tuple functionality
    @test StructUtils.keyeq(:a, ("a", "b", "c"))
    @test StructUtils.keyeq("a", ("a", "b", "c"))
    @test StructUtils.keyeq("b", ("a", "b", "c"))
    @test StructUtils.keyeq("c", ("a", "b", "c"))
    @test !StructUtils.keyeq("d", ("a", "b", "c"))
    @test !StructUtils.keyeq(:d, ("a", "b", "c"))
    
    # Test with Symbol/String mixing
    @test StructUtils.keyeq(:myfield, ("MyField", "myfield", "field_my"))
    @test StructUtils.keyeq("MyField", ("MyField", "myfield", "field_my"))
    @test StructUtils.keyeq("myfield", ("MyField", "myfield", "field_my"))
    @test StructUtils.keyeq("field_my", ("MyField", "myfield", "field_my"))
    @test !StructUtils.keyeq("other", ("MyField", "myfield", "field_my"))
    
    # Test with empty tuple
    @test !StructUtils.keyeq("a", ())
    
    # Test with single element tuple
    @test StructUtils.keyeq("a", ("a",))
    @test !StructUtils.keyeq("b", ("a",))
    
    # Test that existing keyeq methods still work
    @test StructUtils.keyeq(:a, "a")
    @test StructUtils.keyeq("a", :a)
    @test StructUtils.keyeq("a", "a")
    @test StructUtils.keyeq(1, 1)
    @test !StructUtils.keyeq(1, 2)
    
    # Test practical use case: struct with alternative field names
    # Use name directly (not in a namespace) for DefaultStyle
    @tags struct SomeStruct
        my_field::Int & (name=("MyField", "myfield", "field_my"),)
    end
    
    # Test that make works with different key names
    @test StructUtils.make(SomeStruct, Dict("MyField" => 42)).my_field == 42
    @test StructUtils.make(SomeStruct, Dict("myfield" => 43)).my_field == 43
    @test StructUtils.make(SomeStruct, Dict("field_my" => 44)).my_field == 44
    # Test that the original field name still works
    @test StructUtils.make(SomeStruct, Dict(:my_field => 45)).my_field == 45
end

using StaticArrays

@testset "fixedsizearray trait" begin
    @test StructUtils.fixedsizearray(Matrix{Int}) == true
    @test StructUtils.fixedsizearray(Array{Float64, 3}) == true
    @test StructUtils.fixedsizearray(Vector{Int}) == false
    @test StructUtils.fixedsizearray(Set{Int}) == false
    @test StructUtils.fixedsizearray(SVector{3, Int}) == true
end

@testset "StaticArrays" begin
    @test StructUtils.make(SVector{3,Int}, [1, 2, 3]) == SVector{3,Int}((1, 2, 3))
    @test StructUtils.make(SVector{2,Float64}, [1, 2]) == SVector{2,Float64}((1.0, 2.0))
    @test StructUtils.make(SMatrix{2,2,Int}, [[1, 3], [2, 4]]) == SMatrix{2,2,Int}((1, 3, 2, 4))
    @test StructUtils.make(MVector{3,Int}, [1, 2, 3]) == MVector{3,Int}((1, 2, 3))
    @test StructUtils.make(Vector{SVector{2,Int}}, [[1, 2], [3, 4]]) == [SVector{2,Int}((1, 2)), SVector{2,Int}((3, 4))]
end

end

include("trim_compile_tests.jl")
