using Clang
using Clang.Generators
using Clang.LibClang.Clang_jll
using Clang.Generators: StructDefinition, StructMutualRef, strip_comment_markers

include("rewriter.jl")

@testset "Generators" begin
    INCLUDE_DIR = normpath(Clang_jll.artifact_dir, "include")
    CLANG_C_DIR = joinpath(INCLUDE_DIR, "clang-c")

    options = load_options(joinpath(@__DIR__, "test.toml"))
    options["general"]["output_file_path"] = joinpath(@__DIR__, "LibClang.jl")

    # add compiler flags
    args = get_default_args()
    push!(args, "-I$INCLUDE_DIR")

    # search top-level headers
    headers = detect_headers(CLANG_C_DIR, args)

    # add extra definition
    @add_def time_t AbstractJuliaSIT JuliaCtime_t Ctime_t

    # create context
    ctx = create_context(headers, args, options)

    # build without printing so we can do custom rewriting
    build!(ctx, BUILDSTAGE_NO_PRINTING)

    rewrite!(ctx.dag)

    # print
    @test_logs (:info, "Done!") match_mode=:any build!(ctx, BUILDSTAGE_PRINTING_ONLY)
end

@testset "Comments" begin
    @test strip_comment_markers("/* abc */") == ["abc "]
    @test strip_comment_markers("/** abc */") == ["abc "]
    @test strip_comment_markers("/*< abc */") == ["abc "]
    @test strip_comment_markers("/// hello") == ["hello"]
    @test strip_comment_markers("/**\n * line1\n * line2\n */") == ["line1", "line2"]
    @test strip_comment_markers("/*!\n * line1\n * line2\n */") == ["line1", "line2"]
    @test strip_comment_markers("    /// line1\n    /// line2") == ["line1", "line2"]
    @test strip_comment_markers("//! line1\n//! line2") == ["line1", "line2"]
    @test strip_comment_markers("//! line1") == ["line1"]
    @test strip_comment_markers("//< line1") == ["line1"]
end

@testset "Resolve dependency" begin
    args = get_default_args()
    headers = joinpath(@__DIR__, "include", "dependency.h")
    options = Dict("general" => Dict{String,Any}(
            "output_file_path" => joinpath(@__DIR__, "LibDependency.jl")))
    ctx = create_context(headers, args, options)
    build!(ctx)
    @test include("LibDependency.jl") isa Any
end

# See:
# - https://github.com/JuliaInterop/Clang.jl/discussions/440
# - https://github.com/JuliaInterop/Clang.jl/pull/441
@testset "Cycle detection" begin
    args = get_default_args()
    headers = joinpath(@__DIR__, "include", "cycle-detection.h")
    ctx = create_context(headers, args)
    build!(ctx)

    # In this particular case there is only one cycle in B, so only B should be
    # a StructMutualRef.
    mutual_ref_nodes = [node for node in ctx.dag.nodes if node.type == StructMutualRef()]
    @test length(mutual_ref_nodes) == 1
    @test mutual_ref_nodes[1].id == :B
end

# Check the Base.unsafe_convert() specializations that are generated by emit!()
# for TypedefMutualRef's. These use the original struct in their signatures to
# avoid method ambiguities with Base, so they must be emitted after the struct.
@testset "TypedefMutualRef method ambiguity" begin
    args = get_default_args()
    headers = joinpath(@__DIR__, "include", "method-ambiguity.h")
    ctx = create_context(headers, args)
    build!(ctx)

    # Find the StructDefinition node
    struct_idx = findfirst(x -> x.id == :foo_struct && x.type isa StructDefinition,
                           ctx.dag.nodes)
    node = ctx.dag.nodes[struct_idx]

    # There should be three expressions, one for the struct and the other two
    # for the Base.unsafe_convert() specializations.
    @test length(node.exprs) == 3
    # The first expression should be the for the struct
    @test node.exprs[1].head == :struct
    # And the others should be functions (in compact form, hence the :(=) comparison)
    @test node.exprs[2].head == :(=)
    @test node.exprs[3].head == :(=)
end

@testset "Sanity checking" begin
    ctx = create_context(joinpath(@__DIR__, "include/sanity-checks.h"), get_default_args())
    @test_logs (:warn, r"function-like macro .* foo") match_mode = :any build!(ctx)
    @test_logs (:warn, r"function .* post") match_mode = :any build!(ctx)
end

@testset "Issue 320" begin
    args = get_default_args()
    dir = joinpath(@__DIR__, "sys")
    push!(args, "-isystem$dir")
    headers = [joinpath(@__DIR__, "include", "test.h")]
    ctx = create_context(headers, args)
    @add_def stat
    @test build!(ctx, BUILDSTAGE_NO_PRINTING) isa Any
end

@testset "Escape anonymous name with var\"\"" begin
    args = get_default_args()
    headers = joinpath(@__DIR__, "include", "escape-with-var.h")
    options = Dict("general" => Dict{String,Any}(
            "output_file_path" => joinpath(@__DIR__, "LibEscapeWithVar.jl")))
    ctx = create_context(headers, args, options)
    build!(ctx)
    @test include("LibEscapeWithVar.jl") isa Any
end

@testset "Issue 307" begin
    args = get_default_args()
    dir = joinpath(@__DIR__, "sys")
    push!(args, "-isystem$dir")
    headers = joinpath(@__DIR__, "include", "struct-in-union.h")
    ctx = create_context(headers, args)
    @test build!(ctx, BUILDSTAGE_NO_PRINTING) isa Any

    headers = joinpath(@__DIR__, "include", "nested-struct.h")
    ctx = create_context(headers, args)
    @test build!(ctx, BUILDSTAGE_NO_PRINTING) isa Any

    headers = joinpath(@__DIR__, "include", "nested-declaration.h")
    ctx = create_context(headers, args)
    @test_broken try
        build!(ctx, BUILDSTAGE_NO_PRINTING) isa Any
        true
    catch
        false
    end
end

@testset "Issue 327" begin
    parse_header(Index(), joinpath(@__DIR__, "include/void-type.h")) do tu
        root = Clang.getTranslationUnitCursor(tu)
        func = children(root)[]
        ret_type = Clang.getCursorResultType(func)
        @test ret_type isa CLVoid
    end
end

@testset "Issue 355" begin
    parse_header(Index(), joinpath(@__DIR__, "include/return-funcptr.h")) do tu
        root = Clang.getTranslationUnitCursor(tu)
        func = children(root)[3]
        @test length(get_function_args(func)) == 1
    end
end

@testset "macros" begin
    ctx = create_context(joinpath(@__DIR__, "include/macro.h"), get_default_args())
    @test_logs (:info, "Done!") match_mode = :any build!(ctx)
end

@testset "#368" begin
    ctx = create_context(joinpath(@__DIR__, "include/union-in-struct.h"),
                         get_default_args())
    @test_logs (:info, "Done!") match_mode = :any build!(ctx)
    @test ctx.dag.nodes[end].id == :A
    @test ctx.dag.nodes[end].type isa Generators.StructLayout
end

@testset "Issue 376" begin
    ctx = create_context(joinpath(@__DIR__, "include/macro-dependency.h"), get_default_args())
    @test build!(ctx) isa Any
end

@testset "Issue 233" begin
    ctx = create_context(joinpath(@__DIR__, "include/union-in-anon-struct.h"), get_default_args())
    @test build!(ctx) isa Any
end

@testset "Issue 389" begin
    ctx = create_context(joinpath(@__DIR__, "include/macro.h"), get_default_args())
    build!(ctx)
    @test ctx.dag.nodes[ctx.dag.ids[:foo]].type isa AbstractFunctionNodeType
end

@testset "Issue 392" begin
    ctx = create_context([joinpath(@__DIR__, "include/a.h"),
                          joinpath(@__DIR__, "include/dup_a.h")], get_default_args())
    @test_logs (:info, "Done!") match_mode = :any build!(ctx)
end

@testset "Issue 412" begin
    ctx = create_context([joinpath(@__DIR__, "include/enum.h")], get_default_args())
    @test_throws Exception build!(ctx)
end

@testset "Issue 412 - no audit" begin
    options = Dict("general" => Dict{String,Any}("no_audit" => true))
    ctx = create_context([joinpath(@__DIR__, "include/enum.h")], get_default_args(), options)
    @test_logs (:info, "Done!") match_mode = :any build!(ctx)
end

@testset "PR 519 - Elaborated Enum" begin
    ctx = create_context([joinpath(@__DIR__, "include/elaborateEnum.h")], get_default_args())
    @test_logs (:info, "Done!") match_mode = :any build!(ctx)
end

@testset "PR 522 - Still skip EnumForwardDecl with attributes" begin
    ctx = create_context([joinpath(@__DIR__, "include/elaborateEnum.h")], get_default_args())

    mktemp() do path, io
        redirect_stdout(io) do
            build!(ctx)
        end
        close(io)

        output = Ref{String}("")
        open(path) do file
            output[] = read(file, String)
        end

        print(output[])
        @test contains(output[],"@cenum X::UInt32 begin") # Correctly output
        @test !contains(output[], "const X = UInt32")     # const not output
    end
end

@static if Sys.isapple()
@testset "Objective-C" begin
    args = [get_default_args(); ["-x","objective-c"]]
    options = Dict("codegen" => Dict{String,Any}("version_function" => "version_function"))

    ctx = create_context([joinpath(@__DIR__, "include/objectiveC.h")], args, options)
    mktemp() do path, io
        redirect_stdout(io) do
            build!(ctx)
        end
        close(io)

        output = Ref{String}("")
        open(path) do file
            output[] = read(file, String)
        end

        print(output[])
        @test contains(output[],"@objcwrapper immutable = true TestProtocol <: NSObject") # Protocol
        @test contains(output[],"@objcwrapper immutable = true TestProtocol2 <: TestProtocol") # Protocol subtyping Protocol
        @test contains(output[],"@objcwrapper immutable = true TestInterface <: NSObject") # Interface

        @test contains(output[],"@objcwrapper immutable = true availability = macos(v\"100.11.0\") TestAvailability <: NSObject") # Wrapper Availability
        @test contains(output[],"@autoproperty length::Int32 availability = macos(v\"101.11.0\")") # Property Availability

        # Interface Properties
        @test contains(output[],"@objcproperties TestInterfaceProperties begin")
        @test contains(output[],"@autoproperty intproperty1")
        @test contains(output[],"@autoproperty intproperty2")
        @test contains(output[],"setter = setIntproperty1")
        @test contains(output[],"getter = isintproperty2")
        @test contains(output[],"getter = isintproperty3 setter = setIntproperty3")
        @test contains(output[],"@autoproperty intproperty3")
        @test contains(output[],"@autoproperty intproperty4::id{TestInterface}")
        @test contains(output[],"@autoproperty intproperty5::id{TestProtocol}")
        @test contains(output[],"type = Vector{TestProtocol}") broken=true #XXX
        @test contains(output[],"type = Vector{TestInterface}") broken=true #XXX
    end
end
end

@testset "Issue 452 - StructMutualRef" begin
    ctx = create_context([joinpath(@__DIR__, "include/struct-mutual-ref.h")], get_default_args())
    @test_logs (:info, "Done!") match_mode = :any build!(ctx)
end

@testset "Issue 455 - skip static functions" begin
    options = Dict("general" => Dict{String,Any}("skip_static_functions" => true))
    ctx = create_context([joinpath(@__DIR__, "include/static.h")], get_default_args(), options)
    @test_logs (:info, "Done!") match_mode = :any build!(ctx)
end

# Test the documentation parser
@testset "Documentation" begin
    function doc_callback(node::ExprNode, doc::Vector{String})
        return vcat(doc, "callback")
    end

    mktemp() do path, io
        # Generate the bindings
        options = Dict("general" => Dict{String, Any}("output_file_path" => path,
                                                      "extract_c_comment_style" => "doxygen",
                                                      "callback_documentation" => doc_callback))
        ctx = create_context([joinpath(@__DIR__, "include/documentation.h")], get_default_args(), options)
        build!(ctx)

        # Load into a temporary module to avoid polluting the global namespace
        m = Module()
        Base.include(m, path)

        # Do some sanity checks on the docstring
        docstring = string(@doc m.doxygen_func)
        docstring_has = occursin(docstring)
        @test docstring_has("!!! compat \"Deprecated\"")
        @test docstring_has("# Arguments")
        @test docstring_has(" * `foo`: A parameter")
        @test docstring_has("# Returns")
        @test docstring_has("Whatever I want")
        @test docstring_has("!!! danger \"Known bug\"")
        @test docstring_has("# See also")
        @test docstring_has("quux()")
        @test docstring_has("callback")
    end
end

@testset "Issue 515 - unsigned types for large literals" begin
    args = get_default_args()
    headers = joinpath(@__DIR__, "include", "large-integer-literals.h")
    ctx = create_context(headers, args)
    build!(ctx, BUILDSTAGE_NO_PRINTING)
    extract_expr(ctx, i) = only(ctx.dag.nodes[i].exprs)
    # Clong is Int32 on Windows and Int on other platforms.
    clong_is_int32 = Sys.iswindows() || Int === Int32
    if clong_is_int32
        # We need an unsigned type to be able to hold the value on 4 bytes.
        @test extract_expr(ctx, 1) == :(const TEST = Culong(0x80000001))
        @test extract_expr(ctx, 2) == :(const TEST_2 = Culong(2147483649))
    else
        @test extract_expr(ctx, 1) == :(const TEST = Clong(0x80000001))
        @test extract_expr(ctx, 2) == :(const TEST_2 = Clong(2147483649))
    end
    @test extract_expr(ctx, 3) == :(const TEST_SIGNED = Clong(0x00000001))
    @test extract_expr(ctx, 4) == :(const TEST_SIGNED_2 = Clong(2147483646))
end

@testset "#529" begin
    ctx = create_context(joinpath(@__DIR__, "include/typedef-union-in-struct.h"),
                         get_default_args())
    @test_logs (:info, "Done!") match_mode = :any build!(ctx)
    @test ctx.dag.nodes[end-1].id == :C_STRUCT
    @test ctx.dag.nodes[end-1].type isa Generators.StructLayout
end

@testset "#535" begin
    ctx = create_context(joinpath(@__DIR__, "include/alignment.h"),
                         get_default_args())
    @test_logs (:info, "Done!") match_mode = :any build!(ctx)
    @test occursin("##Ctag#", string(ctx.dag.nodes[6].id))
    @test ctx.dag.nodes[6].type isa Generators.UnionAnonymous
end

@testset "#536" begin
    ctx = create_context(joinpath(@__DIR__, "include/alignment.h"),
                         get_default_args())
    @test_logs (:info, "Done!") match_mode = :any build!(ctx)
    @test ctx.dag.nodes[end-1].id == :UA_FieldMetaData
    @test ctx.dag.nodes[end-1].type isa Generators.StructLayout
end

@testset "Constructors" begin
    mktemp() do path, io
        # Generate the bindings
        options = Dict("general" => Dict{String, Any}("output_file_path" => path),
        "codegen" => Dict{String, Any}("add_record_constructors" => true))
        ctx = create_context(joinpath(@__DIR__, "include/constructors.h"),
                         get_default_args(), options)
        build!(ctx)

        # Load into a temporary module to avoid polluting the global namespace
        m = Module()
        Base.include(m, path)

        pf1 = @invokelatest m.PackedFloat3(reinterpret(NTuple{12, UInt8}, (1f0, 2f0, 3f0)))
        pf2 = @invokelatest m.PackedFloat3(reinterpret(NTuple{12, UInt8}, (4f0, 5f0, 6f0)))
        ctn = @invokelatest m.ConstructorTestNormal(pf1, pf2)

        @test @invokelatest(ctn.arg1) === pf1
        @test @invokelatest(ctn.arg2) === pf2

        # XXX: Unmark broken when constructors of structs with Unions are supported
        @test_broken tpf1 = @invokelatest m.PackedFloat3(1f0, 2f0, 3f0)
        @test_broken @invokelatest(tpf1.x) === 1f0
        @test_broken @invokelatest(tpf1.y) === 2f0
        @test_broken @invokelatest(tpf1.z) === 3f0
        @test_broken @invokelatest(tpf1.elements) === (1f0, 2f0, 3f0)

        @test_broken tpf2 = @invokelatest m.PackedFloat3((1f0, 2f0, 3f0))
        @test_broken @invokelatest(tpf2.x) === 1f0
        @test_broken @invokelatest(tpf2.y) === 2f0
        @test_broken @invokelatest(tpf2.z) === 3f0
        @test_broken @invokelatest(tpf2.elements) === (1f0, 2f0, 3f0)
    end
end

@testset "#426 - Base.propertynames" begin
    mktemp() do path, io
        # Generate the bindings
        options = Dict("general" => Dict{String, Any}("output_file_path" => path))
        ctx = create_context(joinpath(@__DIR__, "include/anon-struct-in-anon-union.h"),
                         get_default_args(), options)
        build!(ctx)

        # Load into a temporary module to avoid polluting the global namespace
        m = Module()
        Base.include(m, path)

        pf = @invokelatest m.PackedFloat3(reinterpret(NTuple{12, UInt8}, (1f0, 2f0, 3f0)))

        @test @invokelatest(Base.propertynames(pf)) == (:x, :y, :z, :elements)
        @test @invokelatest(Base.propertynames(pf, true)) == (:x, :y, :z, :elements, :data)
    end
end

@testset "parse_headers()" begin
    # Mostly a test of the do-method
    mktempdir() do d
        headers = joinpath.(d, ["foo.h", "bar.h"])
        write(headers[1], "void foo();")
        write(headers[2], "void bar();")

        x = nothing
        index = Index()
        parse_headers(index, headers) do tus
            @test length(tus) == length(headers)
            GC.gc()
            # All of the TU's should still be alive
            @test all([tu.ptr != C_NULL for tu in tus])

            # Assign to an outer variable
            x = tus
        end

        # After the function call ends the TU's should have been cleaned up
        @test all([tu.ptr == C_NULL for tu in x])
    end
end
