using BitFlags
using Markdown
using Serialization
using Test

# workaround for https://github.com/JuliaLang/julia/issues/54664
function extractdoc(doc)
    # On v1.11 without REPL loaded, the @doc macro returns a Base.Docs.DocStr object;
    # extract the stored string from the object.
    if doc isa Base.Docs.DocStr
        doc = !isnothing(doc.object) ? doc.object : first(doc.text)
    end
    # Otherwise, assume we get something like a Markdown.MD object and just turn it into
    # a string. (Strip trailing newline for consistency with above form.)
    return strip(string(doc))
end

macro macrocall(ex)
    @assert Meta.isexpr(ex, :macrocall)
    ex.head = :call
    for i in 2:length(ex.args)
        ex.args[i] = QuoteNode(ex.args[i])
    end
    insert!(ex.args, 3, __module__)
    return esc(ex)
end

#@testset "Basic operation" begin
    # Inline definition
    @bitflag Flag1 flag1a flag1b flag1c
    @test Int(flag1a) == 1
    @test Int(flag1b) == 2
    @test Int(flag1c) == 4
    @test Flag1(1) == flag1a
    @test Flag1(2) == flag1b
    @test Flag1(4) == flag1c
    @test_throws ArgumentError Flag1(0)
    @test_throws ArgumentError Flag1(8)
    @test instances(Flag1) == (flag1a, flag1b, flag1c)

    # Block definition
    @bitflag Flag2 begin
        flag2a
        flag2b
        flag2c
    end
    @test Int(flag2a) == 1
    @test Int(flag2b) == 2
    @test Int(flag2c) == 4

    # Explicit numbering, inline
    @bitflag Flag3 flag3a=1 flag3b flag3c=8
    @test Int(flag3a) == 1
    @test Int(flag3b) == 2
    @test Int(flag3c) == 8
    @test typemin(Flag3) == flag3a
    @test typemax(Flag3) == flag3c
    @test Int(typemin(Flag3)) == 1
    @test Int(typemax(Flag3)) == 8
    @test_throws ArgumentError Flag3(4)

    # Zero-valued bit flag
    @bitflag Flag4 begin
        flag4a = 0
        flag4b
        flag4c
    end
    @test Int(flag4a) == 0
    @test Int(flag4b) == 1
    @test Int(flag4c) == 2
    @test Flag4(0) == flag4a

    # Mask operations
    @test Int(Flag1(7)) == 7
    @test Flag1(7) == flag1a | flag1b | flag1c
    @test Flag1(7) & flag1a == flag1a
    @test flag1a < flag1b < flag1c
    @test flag1a | flag1b < flag1c

    # Hashing
    @test Int(flag2a) == Int(flag3a)    # same numerical value, but
    @test hash(flag2a) != hash(flag3a)  # unique hashes as BitFlag
    @test which(hash, (Flag1, UInt)).sig != Tuple{typeof(hash), Flag1, UInt}
    struct NonstandardBitFlag <: BitFlags.BitFlag{UInt8} end
    let x = NonstandardBitFlag(), h = zero(UInt)
        @test hash(x, h) == invoke(hash, Tuple{Any, UInt}, x, h)
    end

    # Broadcasting
    @test [flag1a, flag1b] .| flag1c == [flag1a | flag1c, flag1b | flag1c]

    # ccall conversion
    @bitflag CFlag1::UInt64 begin
        cflag1_small = 1
        cflag1_large = UInt64(1) << 63
    end
    flag_nonzero(x::Integer) = Cint(!iszero(x))
    cflag_nonzero_u64 = @cfunction(flag_nonzero, Cint, (UInt64,))
    cflag_nonzero_u32 = @cfunction(flag_nonzero, Cint, (UInt32,))
    cflag_nonzero_i32 = @cfunction(flag_nonzero, Cint, (Int32,))
    @test ccall(cflag_nonzero_u64, Cint, (UInt64,), cflag1_small) == 1
    @test ccall(cflag_nonzero_u32, Cint, (UInt32,), cflag1_small) == 1
    @test ccall(cflag_nonzero_i32, Cint, (Int32,), cflag1_small) == 1
    @test ccall(cflag_nonzero_u64, Cint, (UInt64,), cflag1_large) == 1
    @test_throws InexactError ccall(cflag_nonzero_u32, Cint, (UInt32,), cflag1_large)
    @test_throws InexactError ccall(cflag_nonzero_i32, Cint, (Int32,), cflag1_large)
#end

#@testset "Documentation" begin
    # docstring literal
    """My Docstring""" @bitflag DocFlag1 begin
        """first flag"""
        docflag1a
    end
    @test extractdoc(@doc(DocFlag1)) == "My Docstring"
    @test extractdoc(@doc(docflag1a)) == "first flag"
    # docstring macro for non-string literals
    @doc raw"""Raw Docstring""" @bitflag DocFlag2 begin
        @doc md"""**second flag**"""
        docflag2a
    end
    @test extractdoc(@doc(DocFlag2)) == "Raw Docstring"
    @test extractdoc(@doc(docflag2a)) == "**second flag**"
#end

#@testset "Type properties" begin
    # Default integer typing
    @bitflag Flag5 flag5a flag5b
    @test typeof(Integer(flag5a)) == UInt32
    @test typeof(Flag5) == DataType
    @test typeof(flag5a) <: Flag5 <: BitFlag
    @test isbitstype(Flag5)
    @test isbits(flag5a)

    # Construct non-default
    @bitflag Flag6::UInt8 flag6a flag6b flag6c
    @test typeof(Integer(flag6a)) == UInt8
    @test UInt8(flag6a) === 0x01
    @test UInt16(flag6b) === 0x0002
    @test UInt128(flag6c) === 0x00000000000000000000000000000004

    # Explicit values of non-default types
    @bitflag Flag7::UInt8 flag7a=big"1" flag7b=UInt8(2)
    @test Integer(flag7a) === UInt8(1)
    @test Integer(flag7b) === UInt8(2)
#end

#@testset "Internal definitions" begin
    # Underlying integer types
    @test BitFlags.basetype(Flag5) === UInt32
    @test BitFlags.basetype(Flag6) === UInt8

    # Whether flag has an explicit zero
    @test !BitFlags.haszero(Flag3)
    @test BitFlags.haszero(Flag4)

    # Key-Value mapping
    @test BitFlags.namemap(Flag5) isa NamedTuple{(:flag5a, :flag5b), NTuple{2, UInt32}}
    @test BitFlags.namemap(Flag6) isa NamedTuple{(:flag6a, :flag6b, :flag6c), NTuple{3, UInt8}}
    # Ensure the mapping can be used in a type-inferrable way
    function isset_nt(x::B) where {T, B <: BitFlag{T}}
        nm = BitFlags.namemap(B)
        K, V = keys(nm), values(nm)
        tf = (BitFlags.haszero(B) && iszero(T(x))) ? iszero.(V) : (!iszero).(V .& T.(x))
        return NamedTuple{K}(tf)
    end
    @test @inferred isset_nt(flag3a) == (; flag3a = true, flag3b = false, flag3c = false)
    @test @inferred isset_nt(flag3b) == (; flag3a = false, flag3b = true, flag3c = false)
    @test @inferred isset_nt(flag3b | flag3c) == (; flag3a = false, flag3b = true, flag3c = true)
    @test @inferred isset_nt(flag4a) == (; flag4a = true, flag4b = false, flag4c = false)
    @test @inferred isset_nt(flag4b) == (; flag4a = false, flag4b = true, flag4c = false)
    @test @inferred isset_nt(flag4b | flag4c) == (; flag4a = false, flag4b = true, flag4c = true)
#end

#@testset "Error conditions" begin

    @test_throws ArgumentError("no arguments given for BitFlag Foo"
                              ) @macrocall(@bitflag Foo)
    @test_throws ArgumentError("invalid argument for BitFlag Foo isa UInt32: bad expression head"
                              ) @macrocall(@bitflag Foo isa UInt32)
    @test_throws ArgumentError("invalid argument for BitFlag Foo: Foo::Float64; "
                               * "base type must be a bitstype unsigned integer"
                              ) @macrocall(@bitflag Foo::Float64 x=1.)

    # Require uniqueness
    @test_throws ArgumentError("invalid argument for BitFlag Foo: y = 1; value is not unique"
                              ) @macrocall(@bitflag Foo x=1 y=1)
    @test_throws ArgumentError("invalid argument for BitFlag Foo: y = 0; value is not unique"
                              ) @macrocall(@bitflag Foo x=0 y=0)
    @test_throws ArgumentError("invalid argument for BitFlag Foo: x; name is not unique"
                              ) @macrocall(@bitflag Foo x=0 x)

    # Explicit values must be powers of two
    @test_throws ArgumentError("invalid argument for BitFlag Foo: _three = 3; "
                               * "values must be a positive power of 2"
                              ) @macrocall(@bitflag Foo _three = 3)

    # Values must be integers
    @test_throws ArgumentError("invalid argument for BitFlag Foo: _zero = \"zero\"; "
                               * "values must be unsigned integers"
                              ) @macrocall(@bitflag Foo _zero="zero")
    @test_throws ArgumentError("invalid argument for BitFlag Foo: _zero = '0'; "
                               * "values must be unsigned integers"
                              ) @macrocall(@bitflag Foo _zero='0')
    @test_throws ArgumentError("invalid argument for BitFlag Foo: _zero = 0.5; "
                               * "values must be unsigned integers"
                              ) @macrocall(@bitflag Foo _zero=0.5)

    # Names must be valid identifiers
    @test_throws ArgumentError("""invalid argument for BitFlag Foo: if x
                                      1
                                  else
                                      2
                                  end"""
                              ) @macrocall(@bitflag Foo x ? 1 : 2)
    @test_throws ArgumentError("invalid argument for BitFlag Foo: 1 = 2"
                              ) @macrocall(@bitflag Foo 1=2)
    @test_throws ArgumentError("invalid argument for BitFlag Foo: #1; "
                               * "not a valid identifier"
                              ) @eval @macrocall(@bitflag Foo $(Symbol("#1")))

    # Disallow value overflow
    @test_throws ArgumentError("overflow in value \"y\" of BitFlag Foo"
                          ) @macrocall(@bitflag Foo x=(UInt32(1) << 31) y)

#end

#@testset "Input/Output" begin
    @bitflag Flag9::UInt64 flag9a=1 flag9b=256
    @bitflag Flag10::UInt8 flag10a=128 flag10b=2
    let io = IOBuffer()
        write(io, flag10a | flag10b)
        write(io, flag9b)
        write(io, flag10a)
        seekstart(io)
        @test read(io, Flag10) == flag10a | flag10b
        @test read(io, Flag9) == flag9b
        @test read(io, Flag10) == flag10a
    end
    let io = IOBuffer()
        serialize(io, flag9a)
        seekstart(io)
        @test deserialize(io) === flag9a
    end
#end

#@testset "String representations" begin
    @bitflag FilePerms::UInt8 NONE=0 READ=4 WRITE=2 EXEC=1
    # In submodule and not imported to test prefix printing
    module SubModule
        using ..BitFlags
        @bitflag Bits::UInt8 BIT_ONE BIT_TWO BIT_FOUR BIT_EIGHT # No-zero case
    end

    @test string(FilePerms) == "FilePerms"
    @test string(SubModule.Bits) == "Main.SubModule.Bits"
    @test string(NONE) == "NONE"
    @test string(SubModule.BIT_ONE) == "BIT_ONE"
    @test repr("text/plain", FilePerms) ==
        """BitFlag $(string(FilePerms)):
           NONE = 0x00
           EXEC = 0x01
           WRITE = 0x02
           READ = 0x04"""
    @test repr("text/plain", SubModule.Bits) ==
        """BitFlag Main.SubModule.Bits:
           BIT_ONE = 0x01
           BIT_TWO = 0x02
           BIT_FOUR = 0x04
           BIT_EIGHT = 0x08"""
    @test repr(EXEC) == "EXEC::FilePerms = 0x01"
    @test repr(SubModule.BIT_ONE) == "BIT_ONE::Main.SubModule.Bits = 0x01"
    @test repr(EXEC | READ) == "(READ | EXEC)::FilePerms = 0x05"
    @test repr(SubModule.BIT_ONE | SubModule.BIT_EIGHT) == "(BIT_EIGHT | BIT_ONE)::Main.SubModule.Bits = 0x09"
    @test repr(NONE | READ) == "READ::FilePerms = 0x04"

    let io = IOBuffer(),
        ioc = IOContext(io, :compact => true, :module => Main),
        iof = IOContext(io, :compact => false, :module => Main)
        # Explicit :compact => false required for consistency across Julia versions

        stringf(x) = (show(iof, MIME"text/plain"(), x); String(take!(io)))
        stringc(x) = (show(ioc, MIME"text/plain"(), x); String(take!(io)))

        @test stringf(BitFlag) == "BitFlag"
        @test stringc(BitFlag) == "BitFlag"

        @test stringf(Union{FilePerms, SubModule.Bits}) == "Union{FilePerms, Main.SubModule.Bits}"
        @test stringc(Union{FilePerms, SubModule.Bits}) == "Union{FilePerms, Bits}"

        @test stringc(NONE) == "NONE"
        @test stringc(SubModule.BIT_ONE) == "BIT_ONE"
        @test stringc(EXEC | READ) == "READ|EXEC"
        @test stringc(SubModule.BIT_ONE | SubModule.BIT_EIGHT) == "BIT_EIGHT|BIT_ONE"

        # Handle exceptional cases where set bits are not in the allowed set
        @test stringf(reinterpret(SubModule.Bits, 0x00)) == "reinterpret(Main.SubModule.Bits, 0x00)::Main.SubModule.Bits = 0x00"
        @test stringc(reinterpret(SubModule.Bits, 0x00)) == "0x00"

        @test stringf(reinterpret(SubModule.Bits, 0x11)) == "(BIT_ONE | reinterpret(Main.SubModule.Bits, 0x10))::Main.SubModule.Bits = 0x11"
        @test stringc(reinterpret(SubModule.Bits, 0x11)) == "BIT_ONE|0x10"

        @test stringf(reinterpret(FilePerms, 0x08)) == "reinterpret(FilePerms, 0x08)::FilePerms = 0x08"
        @test stringc(reinterpret(FilePerms, 0x08)) == "0x08"

        @test stringf(reinterpret(FilePerms, 0xff)) == "(READ | WRITE | EXEC | reinterpret(FilePerms, 0xf8))::FilePerms = 0xff"
        @test stringc(reinterpret(FilePerms, 0xff)) == "READ|WRITE|EXEC|0xf8"
    end
#end

#@testset "Scoped bit flags" begin
    # Individual feature tests are less stringent since most of the generated code is
    # the same as the extensively tested unscoped variety. Therefore, only do basic
    # functionality tests, and then test for properties specific to the scoped definition.

    # Inline definition
    @bitflagx SFlag1 flag1a flag1b flag1c
    @test SFlag1.T <: BitFlags.BitFlag
    @test Int(SFlag1.flag1a) == 1
    @test flag1a !== SFlag1.flag1a  # new value is scoped and distinct from unscoped name

    # Block definition
    @bitflagx SFlag2 begin
        flag2a
        flag2b
        flag2c
    end
    @test SFlag2.T <: BitFlags.BitFlag
    @test Int(SFlag2.flag2a) == 1
    @test flag2a !== SFlag2.flag2a  # new value is scoped and distinct from unscoped name

    # Inline definition with explicit type name
    @bitflagx T=U SFlag3 S=2 T
    @test SFlag3.U <: BitFlags.BitFlag
    @test SFlag3.T isa SFlag3.U
    @test Int(SFlag3.T) == 4

    # Block definition with explicit type name
    @bitflagx T=U SFlag4 begin
        S = 2
        T
    end
    @test SFlag4.U <: BitFlags.BitFlag
    @test SFlag4.T isa SFlag4.U
    @test Int(SFlag4.T) == 4

    # Definition with explicit integer type
    @bitflagx SFlag5::UInt8 flag1
    @test typeof(Integer(SFlag5.flag1)) === UInt8

    # Definition with both explicit integer type and type name
    @bitflagx T=_T SFlag6::UInt8 flag1
    @test SFlag6._T <: BitFlags.BitFlag
    @test typeof(Integer(SFlag6.flag1)) === UInt8

    # Documentation
    """My Docstring""" @bitflagx SDocFlag1 docflag
    @test extractdoc(@doc(SDocFlag1)) == "My Docstring"
    @doc raw"""Raw Docstring""" @bitflagx SDocFlag2 docflag
    @test extractdoc(@doc(SDocFlag2)) == "Raw Docstring"

    # Error conditions
    #   Too few arguments
    @test_throws ArgumentError("bad macro call: @bitflagx A = B"
                              ) @macrocall(@bitflagx A=B)
    #   Optional argument must be `T = $somesymbol`
    @test_throws ArgumentError("bad macro call: @bitflagx A = B Foo flag"
                              ) @macrocall(@bitflagx A=B Foo flag)
    @test_throws ArgumentError("bad macro call: @bitflagx T = 1 Foo flag"
                              ) @macrocall(@bitflagx T=1 Foo flag)

    # Printing
    @bitflagx SFilePerms::UInt8 NONE=0 READ=4 WRITE=2 EXEC=1
    module ScopedSubModule
        using ..BitFlags
        @bitflagx SBits::UInt8 BIT_ONE BIT_TWO BIT_FOUR BIT_EIGHT
    end

    @test string(SFilePerms.NONE) == "NONE"
    @test string(ScopedSubModule.SBits.BIT_ONE) == "BIT_ONE"
    @test repr("text/plain", SFilePerms.T) ==
        """BitFlag Main.SFilePerms.T:
           NONE = 0x00
           EXEC = 0x01
           WRITE = 0x02
           READ = 0x04"""
    @test repr("text/plain", ScopedSubModule.SBits.T) ==
        """BitFlag Main.ScopedSubModule.SBits.T:
           BIT_ONE = 0x01
           BIT_TWO = 0x02
           BIT_FOUR = 0x04
           BIT_EIGHT = 0x08"""
    @test repr(SFilePerms.EXEC) == "EXEC::SFilePerms.T = 0x01"
    @test repr(ScopedSubModule.SBits.BIT_ONE) == "BIT_ONE::Main.ScopedSubModule.SBits.T = 0x01"
#end

#@testset "Codegen quality checks" begin
    if isdefined(Test, :detect_closure_boxes)
        @test length(Test.detect_closure_boxes(BitFlags)) == 0
    end
#end
