# Tests for typegroup blocks (mutually recursive type definitions)
# See https://github.com/JuliaLang/julia/issues/269

using Test

@testset "typegroup blocks" begin

    @testset "basic mutual recursion" begin
        # Classic graph example: nodes and edges reference each other
        typegroup
            struct TG_Node
                edges::Vector{TG_Edge}
            end
            struct TG_Edge
                from::TG_Node
                to::TG_Node
            end
        end
        @test fieldtype(TG_Node, :edges) == Vector{TG_Edge}
        @test fieldtype(TG_Edge, :from) == TG_Node
        @test fieldtype(TG_Edge, :to) == TG_Node

        # Can construct instances
        n1 = TG_Node(TG_Edge[])
        n2 = TG_Node(TG_Edge[])
        e = TG_Edge(n1, n2)
        push!(n1.edges, e)
        @test n1.edges[1].to === n2
    end

    @testset "parametric types" begin
        # Parametric mutual recursion
        typegroup
            struct TG_PNode{T}
                data::T
                edges::Vector{TG_PEdge{T}}
            end
            struct TG_PEdge{T}
                from::TG_PNode{T}
                to::TG_PNode{T}
            end
        end
        @test fieldtype(TG_PNode{Int}, :edges) == Vector{TG_PEdge{Int}}
        @test fieldtype(TG_PEdge{String}, :from) == TG_PNode{String}

        # Can construct parametric instances
        n1 = TG_PNode(42, TG_PEdge{Int}[])
        n2 = TG_PNode(99, TG_PEdge{Int}[])
        e = TG_PEdge(n1, n2)
        @test e.from.data == 42
        @test e.to.data == 99
    end

    @testset "self-referential types" begin
        # Single type referencing itself (degenerate case)
        typegroup
            struct TG_SelfRef
                next::Union{Nothing, TG_SelfRef}
            end
        end
        @test fieldtype(TG_SelfRef, :next) == Union{Nothing, TG_SelfRef}

        node3 = TG_SelfRef(nothing)
        node2 = TG_SelfRef(node3)
        node1 = TG_SelfRef(node2)
        @test node1.next.next === node3
    end

    @testset "mutable structs" begin
        typegroup
            mutable struct TG_MutNode
                edges::Vector{TG_MutEdge}
            end
            mutable struct TG_MutEdge
                from::TG_MutNode
                to::TG_MutNode
            end
        end
        @test ismutabletype(TG_MutNode)
        @test ismutabletype(TG_MutEdge)

        n1 = TG_MutNode(TG_MutEdge[])
        n2 = TG_MutNode(TG_MutEdge[])
        e = TG_MutEdge(n1, n2)
        push!(n1.edges, e)
        # Can mutate
        e.to = n1
        @test e.to === n1
    end

    @testset "return value" begin
        # typegroup returns nothing (types are defined as side effect)
        result = typegroup
            struct TG_ReturnTest
                x::Int
            end
        end
        @test result === nothing
    end

    @testset "where clause in field types" begin
        # Field types with where clauses (UnionAll)
        typegroup
            struct TG_Container
                # Field type uses where clause with reference to TG_Item
                items::Vector{TG_Item{T} where T}
            end
            struct TG_Item{T}
                value::T
                parent::TG_Container
            end
        end
        @test fieldtype(TG_Container, :items) == Vector{TG_Item{T} where T}
        @test fieldtype(TG_Item{Int}, :parent) == TG_Container

        # Can construct and use
        c = TG_Container(TG_Item[])
        item = TG_Item(42, c)
        push!(c.items, item)
        @test c.items[1].value == 42
        @test c.items[1].parent === c
    end

    @testset "parametric mutual recursion with Union" begin
        # Issue: parametric types with Union{Nothing, OtherType{T}} fields
        # This tests cycle detection during type instantiation
        typegroup
            struct TG_UnionA{T}
                value::T
                other::Union{Nothing, TG_UnionB{T}}
            end
            struct TG_UnionB{T}
                value::T
                other::Union{Nothing, TG_UnionA{T}}
            end
        end
        @test fieldtype(TG_UnionA{Int}, :other) == Union{Nothing, TG_UnionB{Int}}
        @test fieldtype(TG_UnionB{Int}, :other) == Union{Nothing, TG_UnionA{Int}}

        # Construct instances
        a = TG_UnionA{Int}(1, nothing)
        b = TG_UnionB{Int}(2, nothing)
        a2 = TG_UnionA{Int}(3, b)
        b2 = TG_UnionB{Int}(4, a)
        @test a.other === nothing
        @test a2.other.value == 2
        @test b2.other.value == 1
    end

    @testset "parametric direct mutual reference" begin
        # Issue: direct reference (not through Union) caused stack overflow
        # This tests mayinlinealloc cycle detection
        typegroup
            struct TG_DirectA{T}
                value::T
                other::Union{Nothing, TG_DirectB{T}}
            end
            struct TG_DirectB{T}
                target::TG_DirectA{T}  # Direct reference, not Union
                weight::Float64
            end
        end
        @test fieldtype(TG_DirectA{Int}, :other) == Union{Nothing, TG_DirectB{Int}}
        @test fieldtype(TG_DirectB{Int}, :target) == TG_DirectA{Int}

        # Construct instances
        a = TG_DirectA{Int}(42, nothing)
        b = TG_DirectB{Int}(a, 1.5)
        a2 = TG_DirectA{Int}(99, b)
        @test a2.other.target.value == 42
        @test a2.other.weight == 1.5
    end

    @testset "parametric with Vector wrapping" begin
        # Issue: Vector{OtherType{T}} caused stack overflow during layout computation
        typegroup
            struct TG_VecNode{T}
                value::T
                edges::Vector{TG_VecEdge{T}}
            end
            struct TG_VecEdge{T}
                target::TG_VecNode{T}
                weight::Float64
            end
        end
        @test fieldtype(TG_VecNode{Int}, :edges) == Vector{TG_VecEdge{Int}}
        @test fieldtype(TG_VecEdge{String}, :target) == TG_VecNode{String}

        # Construct graph
        n1 = TG_VecNode{Int}(1, TG_VecEdge{Int}[])
        n2 = TG_VecNode{Int}(2, TG_VecEdge{Int}[])
        e1 = TG_VecEdge{Int}(n2, 1.0)
        e2 = TG_VecEdge{Int}(n1, 2.0)
        n3 = TG_VecNode{Int}(3, [e1, e2])
        @test length(n3.edges) == 2
        @test n3.edges[1].target.value == 2
        @test n3.edges[2].target.value == 1
    end

    @testset "three-way parametric mutual recursion" begin
        # Test cycle detection with more than two types
        typegroup
            struct TG_ThreeA{T}
                value::T
                b::Union{Nothing, TG_ThreeB{T}}
            end
            struct TG_ThreeB{T}
                value::T
                c::Union{Nothing, TG_ThreeC{T}}
            end
            struct TG_ThreeC{T}
                value::T
                a::Union{Nothing, TG_ThreeA{T}}
            end
        end
        @test fieldtype(TG_ThreeA{Int}, :b) == Union{Nothing, TG_ThreeB{Int}}
        @test fieldtype(TG_ThreeB{Int}, :c) == Union{Nothing, TG_ThreeC{Int}}
        @test fieldtype(TG_ThreeC{Int}, :a) == Union{Nothing, TG_ThreeA{Int}}

        # Construct chain
        a = TG_ThreeA{Int}(1, nothing)
        c = TG_ThreeC{Int}(3, a)
        b = TG_ThreeB{Int}(2, c)
        a2 = TG_ThreeA{Int}(4, b)
        @test a2.b.c.a.value == 1
    end

    @testset "multiple type parameters" begin
        # Mutual recursion with multiple type parameters
        typegroup
            struct TG_MultiA{K,V}
                key::K
                value::V
                other::Union{Nothing, TG_MultiB{K,V}}
            end
            struct TG_MultiB{K,V}
                key::K
                value::V
                other::Union{Nothing, TG_MultiA{K,V}}
            end
        end
        @test fieldtype(TG_MultiA{String,Int}, :other) == Union{Nothing, TG_MultiB{String,Int}}

        a = TG_MultiA{String,Int}("a", 1, nothing)
        b = TG_MultiB{String,Int}("b", 2, a)
        @test b.other.key == "a"
        @test b.other.value == 1
    end

    @testset "four-way mutual recursion" begin
        typegroup
            struct TG_FourA{T}
                b::Union{Nothing, TG_FourB{T}}
                d::Union{Nothing, TG_FourD{T}}
            end
            struct TG_FourB{T}
                c::Union{Nothing, TG_FourC{T}}
                a::Union{Nothing, TG_FourA{T}}
            end
            struct TG_FourC{T}
                d::Union{Nothing, TG_FourD{T}}
                b::Union{Nothing, TG_FourB{T}}
            end
            struct TG_FourD{T}
                a::Union{Nothing, TG_FourA{T}}
                c::Union{Nothing, TG_FourC{T}}
            end
        end
        a = TG_FourA{Int}(nothing, nothing)
        b = TG_FourB{Int}(nothing, a)
        c = TG_FourC{Int}(nothing, b)
        d = TG_FourD{Int}(a, c)
        @test d.a === a
        @test d.c.b.a === a
    end

    @testset "graph with typed edges" begin
        # Pattern from Rust's petgraph
        typegroup
            struct TG_Graph{N, E}
                nodes::Vector{TG_GraphNode{N, E}}
            end
            struct TG_GraphNode{N, E}
                data::N
                edges::Vector{TG_GraphEdge{N, E}}
            end
            struct TG_GraphEdge{N, E}
                weight::E
                target::TG_GraphNode{N, E}
            end
        end
        n1 = TG_GraphNode{String, Float64}("A", TG_GraphEdge{String,Float64}[])
        n2 = TG_GraphNode{String, Float64}("B", TG_GraphEdge{String,Float64}[])
        e = TG_GraphEdge{String, Float64}(1.5, n2)
        push!(n1.edges, e)
        g = TG_Graph{String, Float64}([n1, n2])
        @test g.nodes[1].edges[1].target.data == "B"
    end

    @testset "JSON-like recursive structure" begin
        typegroup
            struct TG_JSONValue
                data::Union{Nothing, Bool, Int, Float64, String, TG_JSONArray, TG_JSONObject}
            end
            struct TG_JSONArray
                elements::Vector{TG_JSONValue}
            end
            struct TG_JSONObject
                pairs::Vector{Pair{String, TG_JSONValue}}
            end
        end
        arr = TG_JSONArray([TG_JSONValue(42), TG_JSONValue("hello")])
        obj = TG_JSONObject([Pair("array", TG_JSONValue(arr))])
        @test obj.pairs[1].second.data.elements[1].data == 42
    end

    @testset "doubly-linked list" begin
        typegroup
            mutable struct TG_DLNode{T}
                value::T
                prev::Union{Nothing, TG_DLNode{T}}
                next::Union{Nothing, TG_DLNode{T}}
            end
        end
        n1 = TG_DLNode(1, nothing, nothing)
        n2 = TG_DLNode(2, n1, nothing)
        n1.next = n2
        @test n1.next.value == 2
        @test n2.prev.value == 1
    end

    @testset "binary tree with parent pointer" begin
        typegroup
            mutable struct TG_BinTree{T}
                value::T
                parent::Union{Nothing, TG_BinTree{T}}
                left::Union{Nothing, TG_BinTree{T}}
                right::Union{Nothing, TG_BinTree{T}}
            end
        end
        root = TG_BinTree(10, nothing, nothing, nothing)
        left = TG_BinTree(5, root, nothing, nothing)
        right = TG_BinTree(15, root, nothing, nothing)
        root.left = left
        root.right = right
        @test root.left.parent === root
        @test root.right.value == 15
    end

    @testset "lambda calculus AST" begin
        typegroup
            struct TG_LamVar
                name::Symbol
            end
            struct TG_LamAbs
                param::Symbol
                body::Union{TG_LamVar, TG_LamAbs, TG_LamApp}
            end
            struct TG_LamApp
                func::Union{TG_LamVar, TG_LamAbs, TG_LamApp}
                arg::Union{TG_LamVar, TG_LamAbs, TG_LamApp}
            end
        end
        v = TG_LamVar(:x)
        abs = TG_LamAbs(:x, v)
        app = TG_LamApp(abs, v)
        @test app.func.param == :x
    end

    @testset "entity-component pattern" begin
        typegroup
            struct TG_Entity
                id::Int
                components::Dict{Symbol, TG_Component}
            end
            struct TG_Component
                owner::TG_Entity
                data::Any
            end
        end
        e = TG_Entity(1, Dict{Symbol,TG_Component}())
        c = TG_Component(e, "health")
        e.components[:health] = c
        @test e.components[:health].owner === e
    end

    @testset "NamedTuple fields" begin
        typegroup
            struct TG_NTNode
                data::@NamedTuple{value::Int, edge::Union{Nothing, TG_NTEdge}}
            end
            struct TG_NTEdge
                info::@NamedTuple{from::TG_NTNode, to::TG_NTNode, weight::Float64}
            end
        end
        n1 = TG_NTNode((value=1, edge=nothing))
        n2 = TG_NTNode((value=2, edge=nothing))
        e = TG_NTEdge((from=n1, to=n2, weight=1.0))
        @test e.info.from.data.value == 1
    end

    @testset "bounded type parameters" begin
        typegroup
            struct TG_BoundedA{T <: Number}
                b::Union{Nothing, TG_BoundedB{T}}
            end
            struct TG_BoundedB{T <: Number}
                a::Union{Nothing, TG_BoundedA{T}}
            end
        end
        a = TG_BoundedA{Int}(nothing)
        b = TG_BoundedB{Float64}(nothing)
        @test fieldtype(TG_BoundedA{Int}, :b) == Union{Nothing, TG_BoundedB{Int}}
    end

    @testset "deeply nested Union" begin
        typegroup
            struct TG_DeepUnionA
                x::Union{Nothing, Union{Int, Union{String, Union{Float64, TG_DeepUnionB}}}}
            end
            struct TG_DeepUnionB
                y::Union{Nothing, TG_DeepUnionA}
            end
        end
        a = TG_DeepUnionA(nothing)
        b = TG_DeepUnionB(a)
        @test b.y === a
    end

    @testset "supertype referencing incomplete type" begin
        typegroup
            struct TG_SuperRefA <: AbstractVector{TG_SuperRefB}
                data::Vector{TG_SuperRefB}
            end
            struct TG_SuperRefB
                a::TG_SuperRefA
            end
        end
        @test TG_SuperRefA <: AbstractVector{TG_SuperRefB}
    end

    @testset "self-referential supertype parameter" begin
        # Node{T} <: AbstractVector{Node{T}} -- type references itself in supertype params
        typegroup
            struct TG_SelfSuperNode{T} <: AbstractVector{TG_SelfSuperNode{T}}
                data::T
            end
        end
        @test TG_SelfSuperNode{Int} <: AbstractVector{TG_SelfSuperNode{Int}}
        @test supertype(TG_SelfSuperNode{Int}) == AbstractVector{TG_SelfSuperNode{Int}}
        n = TG_SelfSuperNode{Int}(42)
        @test n.data == 42

        # Two types where one references itself in supertype
        typegroup
            struct TG_SelfSuperA{T} <: AbstractVector{TG_SelfSuperA{T}}
                b::Union{Nothing, TG_SelfSuperB{T}}
            end
            struct TG_SelfSuperB{T}
                a::TG_SelfSuperA{T}
            end
        end
        @test TG_SelfSuperA{Int} <: AbstractVector{TG_SelfSuperA{Int}}
        @test fieldtype(TG_SelfSuperB{Int}, :a) == TG_SelfSuperA{Int}
    end

    @testset "red/black list with AbstractArray{T,0} supertype" begin
        # Mutually recursive supertypes: each list node subtypes a 0-dimensional
        # AbstractArray whose element type is the opposite-color node
        typegroup
            struct TG_RedNode <: AbstractArray{TG_BlackNode, 0}
                child::Union{Nothing, TG_BlackNode}
            end
            struct TG_BlackNode <: AbstractArray{TG_RedNode, 0}
                child::Union{Nothing, TG_RedNode}
            end
        end
        @test TG_RedNode <: AbstractArray{TG_BlackNode, 0}
        @test TG_BlackNode <: AbstractArray{TG_RedNode, 0}
        @test eltype(TG_RedNode) == TG_BlackNode
        @test eltype(TG_BlackNode) == TG_RedNode

        # Construct alternating red/black chain
        r1 = TG_RedNode(nothing)
        b1 = TG_BlackNode(r1)
        r2 = TG_RedNode(b1)
        b2 = TG_BlackNode(r2)
        @test b2.child.child.child === r1
        @test b2.child.child.child.child === nothing
    end

    @testset "Tuple fields with incomplete types" begin
        # Self-referential Tuple field
        typegroup
            struct TG_TupleSelf
                data::Tuple{TG_TupleSelf}
            end
        end
        @test fieldtype(TG_TupleSelf, :data) == Tuple{TG_TupleSelf}

        # Tuple with two types from typegroup
        typegroup
            struct TG_TupleA
                data::Tuple{Int, TG_TupleB}
            end
            struct TG_TupleB
                x::Int
            end
        end
        @test fieldtype(TG_TupleA, :data) == Tuple{Int, TG_TupleB}
        a = TG_TupleA((42, TG_TupleB(99)))
        @test a.data[1] == 42
        @test a.data[2].x == 99

        # NTuple with self-reference through Union
        typegroup
            struct TG_NTupleNode
                neighbors::NTuple{3, Union{Nothing, TG_NTupleNode}}
            end
        end
        @test fieldtype(TG_NTupleNode, :neighbors) == NTuple{3, Union{Nothing, TG_NTupleNode}}
        n = TG_NTupleNode((nothing, nothing, nothing))
        @test n.neighbors[1] === nothing

        # Tuple with Union containing incomplete type
        typegroup
            struct TG_TupleUnion
                data::Tuple{Int, Union{Nothing, TG_TupleUnion}}
            end
        end
        t = TG_TupleUnion((42, nothing))
        @test t.data[1] == 42
        @test t.data[2] === nothing
        t2 = TG_TupleUnion((99, t))
        @test t2.data[2].data[1] == 42
    end

    @testset "TypeApp reflection" begin
        ta = Core.TypeApp(Vector, Int)
        @test ta isa Core.TypeApp
        @test ta.head === Vector
        @test ta.param === Int
        @test !ismutabletype(Core.TypeApp)
        @test fieldcount(Core.TypeApp) == 2
        @test fieldnames(Core.TypeApp) == (:head, :param)

        # Nested chain: T{P1, P2} == TypeApp(TypeApp(T, P1), P2)
        ta2 = Core.TypeApp(Core.TypeApp(Dict, String), Int)
        @test ta2.head isa Core.TypeApp
        @test ta2.head.head === Dict
        @test ta2.head.param === String
        @test ta2.param === Int

        # TypeVar head (the typegroup placeholder pattern)
        tv = TypeVar(:T)
        ta3 = Core.TypeApp(tv, Int)
        @test ta3.head === tv

        # _contains_typeapp traverses types
        @test Core._contains_typeapp(ta)
        @test !Core._contains_typeapp(Int)
        @test !Core._contains_typeapp(Vector{Int})
        @test !Core._contains_typeapp(Union{Int, String})
        # Union{Int, TypeApp} can't be constructed directly (Union requires Types),
        # but _contains_typeapp handles it if encountered during resolution.
        # Test via DataType wrapping instead:
        @test Core._contains_typeapp(Core.TypeApp(Core.TypeApp(Vector, ta), Int))

        # apply_type_or_typeapp: concrete args → real apply_type
        @test Core.apply_type_or_typeapp(Vector, Int) === Vector{Int}
        @test Core.apply_type_or_typeapp(Dict, String, Int) === Dict{String, Int}

        # apply_type_or_typeapp: TypeVar head → nested TypeApp chain
        r = Core.apply_type_or_typeapp(tv, Int, String)
        @test r isa Core.TypeApp
        @test r.head isa Core.TypeApp
        @test r.head.head === tv
        @test r.head.param === Int
        @test r.param === String

        # apply_type_or_typeapp: TypeApp head → extends the chain
        r2 = Core.apply_type_or_typeapp(ta, Float64)
        @test r2 isa Core.TypeApp
        @test r2.head === ta
        @test r2.param === Float64

        # apply_type_or_typeapp: TypeApp buried in a param → defers
        r3 = Core.apply_type_or_typeapp(Vector, ta)
        @test r3 isa Core.TypeApp
        @test r3.head === Vector
        @test r3.param === ta

        # TypeApp is not a Type — reflection that requires Type arguments errors
        @test_throws TypeError ta <: Any
        @test_throws TypeError Any <: ta
        @test_throws TypeError typeintersect(ta, Any)
        @test_throws TypeError 1 isa ta
        @test_throws TypeError fieldcount(ta)
        @test_throws MethodError fieldnames(ta)
        @test_throws MethodError fieldtypes(ta)
        @test_throws MethodError supertype(ta)
        @test_throws TypeError hasmethod(+, Tuple{ta, ta})

        # resolve_typegroup with empty inputs
        @test Core.resolve_typegroup(@__MODULE__, Core.svec(), Core.svec(), Core.svec()) === ()
    end

    # Issue #60919: accessing incomplete types during struct definition should error, not segfault
    # These tests exercise the incomplete type safety checks added for ordinary struct lowering.
    @testset "incomplete type errors (#60919)" begin
        # With typegroup lowering, struct names are TypeVars during definition.
        # Accessing incomplete types now errors safely instead of segfaulting.

        # fieldtype on incomplete type (TypeVar during definition) — gets TypeError
        @test_throws TypeError eval(:(struct TG_60919_A <: AbstractVector{fieldtype(TG_60919_A, :x)}
        end))

        # fieldtype on incomplete type where field exists but types aren't set yet
        @test_throws TypeError eval(:(struct TG_60919_B <: AbstractVector{fieldtype(TG_60919_B, :x)}
            x::Int
        end))

        # sizeof on outer incomplete type from nested struct — errors because
        # the inner struct constructor has free type variables (the outer TypeVar)
        @test_throws ArgumentError eval(:(struct TG_60919_C
            x::(struct TG_60919_C_Inner; y::TG_60919_C; end; Core.sizeof(TG_60919_C); TG_60919_C_Inner)
        end))

        # nested struct referencing incomplete outer type — errors because
        # the inner struct constructor has free type variables
        @test_throws ArgumentError eval(:(struct TG_60919_D
            x::(struct TG_60919_D_Inner; y::TG_60919_D; end; TG_60919_D_Inner)
        end))
    end

    # Constructing a typegroup type while types are still being defined should error, not crash
    @testset "method call on incomplete typegroup type" begin
        @test_throws MethodError eval(:(typegroup
            struct TG_EarlyCall_A
                x::Int
                b::Union{Nothing, TG_EarlyCall_B}
            end
            struct TG_EarlyCall_B
                a::(TG_EarlyCall_A(1, nothing); TG_EarlyCall_A)
            end
        end))
    end

    # Defining methods on incomplete types during type construction should error
    @testset "method definition on incomplete type during super expression" begin
        # Normal struct case
        @test_throws ArgumentError eval(:(struct TG_SideEffect_S <: (global _tg_se_f; _tg_se_f(::TG_SideEffect_S) = 1; Any)
            x::Int
        end))
        # Typegroup case
        @test_throws ArgumentError eval(:(typegroup
            struct TG_SideEffect_A <: (global _tg_se_g; _tg_se_g(::TG_SideEffect_A) = 1; Any)
                b::Union{Nothing, TG_SideEffect_B}
            end
            struct TG_SideEffect_B
                a::Union{Nothing, TG_SideEffect_A}
            end
        end))
        # Subtype check on incomplete type (TypeVar during definition)
        @test_throws TypeError eval(:(struct TG_SideEffect_Sub <: (TG_SideEffect_Sub <: Real ? Any : Real)
        end))
    end

    # Precompilation should fail for modules containing incomplete type errors.
    @testset "precompilation rejects incomplete types" begin
        mktempdir() do dir
            pushfirst!(LOAD_PATH, dir)
            depot = mktempdir()
            pushfirst!(DEPOT_PATH, depot)
            try
                write(joinpath(dir, "TG_PrecompIncomplete.jl"), """
                module TG_PrecompIncomplete
                # Nested struct referencing incomplete outer type
                struct Outer
                    x::(struct Inner; y::Outer; end; Inner)
                end
                end
                """)
                @test_throws Exception Base.require(Main, :TG_PrecompIncomplete)
            finally
                filter!((≠)(dir), LOAD_PATH)
                filter!((≠)(depot), DEPOT_PATH)
            end
        end
    end

    @testset "invalid supertype errors" begin
        # Cannot subtype a tuple type
        @test_throws ErrorException eval(:(typegroup
            struct TG_BadTuple <: Tuple{Int}
                x::Int
            end
        end))
        # Cannot subtype a named tuple type
        @test_throws ErrorException eval(:(typegroup
            struct TG_BadNT <: @NamedTuple{x::Int}
                x::Int
            end
        end))
        # Cannot add subtypes to Type
        @test_throws ErrorException eval(:(typegroup
            struct TG_BadType <: Type{Int}
                x::Int
            end
        end))
        # Can only subtype abstract types
        @test_throws ErrorException eval(:(typegroup
            struct TG_BadConcrete <: Int
                x::Int
            end
        end))
        # A type cannot subtype itself
        @test_throws "a type cannot subtype itself" eval(:(typegroup
            struct TG_SelfSub <: TG_SelfSub
                x::Int
            end
        end))
    end

    @testset "inner constructors" begin
        # Basic inner constructor with no arguments
        typegroup
            struct TG_InnerBasicA
                x::Int
                TG_InnerBasicA() = new(0)
            end
            struct TG_InnerBasicB
                a::TG_InnerBasicA
            end
        end
        @test TG_InnerBasicA().x == 0
        @test TG_InnerBasicB(TG_InnerBasicA()).a.x == 0

        # Inner constructor with arguments
        typegroup
            struct TG_InnerArgsA
                x::Int
                y::Float64
                TG_InnerArgsA(x::Int) = new(x, float(x))
            end
            struct TG_InnerArgsB
                a::TG_InnerArgsA
            end
        end
        @test TG_InnerArgsA(3).y == 3.0
        @test TG_InnerArgsB(TG_InnerArgsA(5)).a.x == 5

        # Inner constructor with new{T}(...) for parametric types
        typegroup
            struct TG_InnerParamA{T}
                x::T
                TG_InnerParamA{T}(x) where {T} = new{T}(x)
                TG_InnerParamA(x::T) where {T} = new{T}(x)
            end
            struct TG_InnerParamB{T}
                a::TG_InnerParamA{T}
            end
        end
        @test TG_InnerParamA{Int}(42).x == 42
        @test TG_InnerParamA(3.14).x == 3.14
        @test TG_InnerParamB{Int}(TG_InnerParamA(1)).a.x == 1

        # Inner constructor in one type referencing the other typegroup type
        typegroup
            struct TG_InnerCrossA
                x::Int
                b::Union{Nothing, TG_InnerCrossB}
                TG_InnerCrossA(x::Int) = new(x, nothing)
            end
            struct TG_InnerCrossB
                a::TG_InnerCrossA
                TG_InnerCrossB(x::Int) = new(TG_InnerCrossA(x))
            end
        end
        @test TG_InnerCrossA(1).b === nothing
        @test TG_InnerCrossB(42).a.x == 42

        # Multiple inner constructors
        typegroup
            struct TG_InnerMultiA
                x::Int
                y::Int
                TG_InnerMultiA() = new(0, 0)
                TG_InnerMultiA(x::Int) = new(x, x)
                TG_InnerMultiA(x::Int, y::Int) = new(x, y)
            end
            struct TG_InnerMultiB
                a::TG_InnerMultiA
            end
        end
        @test TG_InnerMultiA().x == 0
        @test TG_InnerMultiA(3).y == 3
        @test TG_InnerMultiA(1, 2).y == 2
    end

    @testset "docstrings on typegroup types" begin
        # Docstrings on individual types within a typegroup
        typegroup
            "TG_DocA: a documented node type"
            struct TG_DocA
                edges::Vector{TG_DocB}
            end
            "TG_DocB: a documented edge type"
            struct TG_DocB
                from::TG_DocA
                to::TG_DocA
            end
        end

        @test fieldtype(TG_DocA, :edges) == Vector{TG_DocB}
        @test fieldtype(TG_DocB, :from) == TG_DocA

        meta = Base.Docs.meta(@__MODULE__)
        bind_a = Base.Docs.Binding(@__MODULE__, :TG_DocA)
        bind_b = Base.Docs.Binding(@__MODULE__, :TG_DocB)
        @test haskey(meta, bind_a)
        @test haskey(meta, bind_b)
        @test contains(string(meta[bind_a].docs[Union{}]), "TG_DocA: a documented node type")
        @test contains(string(meta[bind_b].docs[Union{}]), "TG_DocB: a documented edge type")

        # Mix of documented and undocumented types
        typegroup
            "TG_DocC: only this one has a docstring"
            struct TG_DocC
                other::TG_DocD
            end
            struct TG_DocD
                other::TG_DocC
            end
        end

        @test fieldtype(TG_DocC, :other) == TG_DocD
        bind_c = Base.Docs.Binding(@__MODULE__, :TG_DocC)
        bind_d = Base.Docs.Binding(@__MODULE__, :TG_DocD)
        @test haskey(meta, bind_c)
        @test contains(string(meta[bind_c].docs[Union{}]), "TG_DocC: only this one has a docstring")
        @test !haskey(meta, bind_d)
    end

    @testset "group type reference inside TypeVar bounds" begin
        # The placeholder TypeVar for a group member must be substituted even
        # when it only appears inside another TypeVar's bounds (e.g. `<:` in
        # covariant position, which lowers to `Vector{S} where S<:Name`).
        struct TG_BoundSelf
            y::Vector{<:TG_BoundSelf}
            TG_BoundSelf(y) = new(y)
        end
        ft = fieldtype(TG_BoundSelf, :y)
        @test ft == Vector{<:TG_BoundSelf}
        @test !Base.has_free_typevars(ft)
        @test (ft::UnionAll).var.ub === TG_BoundSelf

        struct TG_BoundUnion
            x::Union{Nothing, Vector{<:TG_BoundUnion}}
            TG_BoundUnion(x) = new(x)
        end
        @test !Base.has_free_typevars(fieldtype(TG_BoundUnion, :x))

        struct TG_BoundWhere
            x::Ref{S} where S<:TG_BoundWhere
            TG_BoundWhere(x) = new(x)
        end
        @test !Base.has_free_typevars(fieldtype(TG_BoundWhere, :x))

        # Parametric self-reference through a bound; also checks that the
        # default constructors can be created (they reject free typevars).
        struct TG_BoundParam{T}
            x::T
            y::Dict{Symbol, <:TG_BoundParam}
        end
        @test !Base.has_free_typevars(fieldtype(TG_BoundParam, :y))
        v = TG_BoundParam{Int}(1, Dict{Symbol, TG_BoundParam{Int}}())
        @test v.x == 1

        # Sibling group member referenced from inside a bound
        typegroup
            struct TG_BoundSibA
                items::Vector{<:TG_BoundSibB}
                TG_BoundSibA(items) = new(items)
            end
            struct TG_BoundSibB
                owner::Union{Nothing, TG_BoundSibA}
            end
        end
        ftA = fieldtype(TG_BoundSibA, :items)
        @test !Base.has_free_typevars(ftA)
        @test (ftA::UnionAll).var.ub === TG_BoundSibB
        b = TG_BoundSibB(nothing)
        a = TG_BoundSibA([b])
        @test a.items[1] === b

        # Group references in a type *parameter* bound cannot be substituted
        # (the bound is baked into the wrapper UnionAll) and must be rejected
        # cleanly instead of escaping as a type with free placeholder vars.
        # On the old lowering this was an UndefVarError.
        @test_throws ErrorException @eval struct TG_BoundParamRef{T<:Union{Nothing,Vector{TG_BoundParamRef}}}
            x::T
            TG_BoundParamRef{T}(x) where T = new{T}(x)
        end
        @test !isdefined(@__MODULE__, :TG_BoundParamRef)

        # The same rebuilt TypeVar must be used consistently when the var
        # occurs in the body, so the UnionAll stays well-formed.
        struct TG_BoundBody
            x::Pair{S, Vector{S}} where S<:TG_BoundBody
            TG_BoundBody(x) = new(x)
        end
        ftB = fieldtype(TG_BoundBody, :x)
        @test !Base.has_free_typevars(ftB)
        @test ftB == Pair{S, Vector{S}} where S<:TG_BoundBody
    end

    @testset "type cache hygiene" begin
        # Count global Tuple-cache entries whose parameters reference a type
        # with the given name that is NOT the currently bound one (i.e.
        # discarded duplicates from redefinition, or types from a failed
        # definition). Field types in these tests keep the reference as a
        # direct Tuple parameter, so a shallow scan suffices.
        function stale_tuple_cache_refs(name::Symbol, keep = nothing)
            stale = 0
            for cache in (Tuple.name.cache, Tuple.name.linearcache)
                for e in cache
                    e isa DataType || continue
                    for p in e.parameters
                        if p isa DataType && p.name.name === name && p !== keep
                            stale += 1
                        end
                    end
                end
            end
            return stale
        end

        # Identical redefinition keeps the old type; the tuples instantiated
        # while constructing the discarded duplicate must not remain in the
        # global type cache.
        for _ in 1:3
            @eval struct TG_CacheRedef
                t::Tuple{TG_CacheRedef, Int}
                TG_CacheRedef(t) = new(t)
            end
        end
        @test stale_tuple_cache_refs(:TG_CacheRedef, TG_CacheRedef) == 0
        # the kept type's field tuple is published and usable
        @test fieldtype(TG_CacheRedef, :t) === Tuple{TG_CacheRedef, Int}

        # A failed group definition must not leave cache entries referencing
        # the never-published types.
        @test_throws Exception @eval typegroup
            struct TG_CacheFail
                t::Tuple{TG_CacheFail, Int}
                TG_CacheFail(t) = new(t)
            end
            struct TG_CacheFailBad
                x::1
            end
        end
        @test !isdefined(@__MODULE__, :TG_CacheFail)
        @test stale_tuple_cache_refs(:TG_CacheFail) == 0
    end

end
