using Tricks
using InteractiveUtils
using Test

struct Iterable end; struct NonIterable end;
iterableness_static(::Type{T}) where T = static_hasmethod(iterate, Tuple{T}) ? Iterable() : NonIterable()

struct Foo end

# if code has no calls then it must be fully static
has_no_calls(ir) = all(stmt->!Meta.isexpr(stmt, :call), ir)

VERSION >= v"1.3" && @testset "static_hasmethod" begin
    @testset "positive: $(typeof(data))" for data in (
        "abc", [1,2,3], (2,3), ones(4,10,2), 'a',  1:100
    )
        T = typeof(data)
        @test iterableness_static(T) === Iterable()
        code_typed = (@code_typed iterableness_static(T))

        @test code_typed[2] === Iterable  # return type
        @test has_no_calls(code_typed[1].code)
    end

    @testset "negative: $(typeof(data))" for data in (
        :a, rand, Int
    )
        T = typeof(data)
        @test iterableness_static(T) === NonIterable()
        code_typed = (@code_typed iterableness_static(T))

        @test code_typed[2] === NonIterable  # return type
        @test has_no_calls(code_typed[1].code)
    end

    @testset "add method" begin
        @test iterableness_static(Foo) === NonIterable()

        Base.iterate(::Foo) = ("Foo", nothing);
        Base.iterate(::Foo, ::Nothing) = nothing;
        Base.length(::Foo) = 1;
        @test collect(Foo()) == ["Foo"]

        @test iterableness_static(Foo) === Iterable()
    end

    @testset "delete method" begin
        @test iterableness_static(Foo) === Iterable()
        meth = first(methods(iterate, Tuple{Foo}))
        Base.delete_method(meth)
        @test_throws MethodError collect(Foo())

        @test iterableness_static(Foo) === NonIterable()
    end


    @testset "abstract type args" begin
        # https://github.com/oxinabox/Tricks.jl/issues/14

        goo(x::Integer) = 1
        @assert !hasmethod(goo, Tuple{Real})  # the behaviour we want to match
        @test !static_hasmethod(goo, Tuple{Real})

        goo(x::Number) = 2
        @assert hasmethod(goo, Tuple{Real})   # Now it _is_ covered.
        @test static_hasmethod(goo, Tuple{Real})   # Now it _is_ covered.
    end
end

@testset "compat_hasmethod" begin
    @static if VERSION < v"1.3"
        @test compat_hasmethod == hasmethod
    else
        @test compat_hasmethod == static_hasmethod
    end
end

module Bar
h(::Int) = 1

end
using .Bar

VERSION >= v"1.3" && @testset "static_methods" begin
    # behavour
    f(x) = x + 1
    @test (length ∘ collect ∘ static_methods)(f) == 1
    f(::Int) = 1
    @test (length ∘ collect ∘ static_methods)(f) == 2

    g(::Int) = 1
    @test (length ∘ collect ∘ static_methods)(g) == 1
    g(x) = x+1
    @test (length ∘ collect ∘ static_methods)(g) == 2

    @test (length ∘ collect ∘ static_methods)(Bar.h) == 1
    Bar.h(x) = x
    @test (length ∘ collect ∘ static_methods)(Bar.h) == 2

    # Code Generation
    code_typed = (@code_typed static_methods(f))
    @test code_typed[2] === Base.MethodList  # return type
    @test has_no_calls(code_typed[1].code)

    @testset "delete method" begin
        i(::Int) = 1
        @test (length ∘ collect ∘ static_methods)(i) == 1
        i(x) = x+1
        @test (length ∘ collect ∘ static_methods)(i) == 2

        Base.delete_method((first ∘ methods)(i))
        @test (length ∘ collect ∘ static_methods)(i) == 1

        Base.delete_method((first ∘ methods)(i))
        @test (length ∘ collect ∘ static_methods)(i) == 0

        i(x) = x+1
        @test (length ∘ collect ∘ static_methods)(i) == 1
    end
end
@testset "type signatures" begin
    j(x) = x
    j(x, y, z...) = x + y + length(y)
    @test (length ∘ collect ∘ static_methods)(j) == 2
    @test static_method_count(j) == 2

    @test (length ∘ collect ∘ static_methods)(j, Tuple{Int}) == 1
    @test static_method_count(j, Tuple{Int,Int}) == 1
    @test (length ∘ collect ∘ static_methods)(j, Tuple{Int,Int}) == 1
    @test static_method_count(j, Tuple{Int,Int}) == 1
    @test (length ∘ collect ∘ static_methods)(j, Tuple{Int,Int,Int}) == 1
    @test static_method_count(j, Tuple{Int,Int,Int}) == 1

    # Varargs
    @test (length ∘ collect ∘ static_methods)(j, Tuple{Int, Int, Vararg{Int}}) == 1
    @test static_method_count(j, Tuple{Int, Int, Vararg{Int}}) == 1

    @test (length ∘ collect ∘ static_methods)(j, Tuple{Vararg{Int}}) == 2
    @test static_method_count(j, Tuple{Vararg{Int}}) == 2

    @test (length ∘ collect ∘ static_methods)(j, Tuple{Any, Vararg{Int}}) == 2
    @test static_method_count(j, Tuple{Any, Vararg{Int}}) == 2

    j(x, y::Int, z...) = x + y + length(y)

    @test (length ∘ collect ∘ static_methods)(j, Tuple{Int, Int, Vararg{Int}}) == 1
    @test static_method_count(j, Tuple{Int, Int, Vararg{Int}}) == 1
    @test (length ∘ collect ∘ static_methods)(j, Tuple{Int, Any, Vararg{Int}}) == 2
    @test static_method_count(j, Tuple{Int, Any, Vararg{Int}}) == 2

    @test (length ∘ collect ∘ static_methods)(j, Tuple{Vararg{Any}}) == 3
    @test static_method_count(j, Tuple{Vararg{Any}}) == 3

    @test (length ∘ collect ∘ static_methods)(j, Tuple{Any, Vararg{Any}}) == 3
    @test static_method_count(j, Tuple{Any, Vararg{Any}}) == 3

    @testset "delete method" begin

        while length(collect(methods(j))) > 0
            Base.delete_method((first ∘ methods)(j))

            T = Tuple{Any, Vararg{Any}}
            @test (length ∘ collect ∘ static_methods)(j, T) == length(collect(methods(j)))
            @test static_method_count(j, T) == length(collect(methods(j)))
        end
    end
end

# Redefine module Bar
module Bar
h(::Int) = 1
end
VERSION >= v"1.3" && @testset "static_method_count" begin
    # behavour
    f(x) = x + 1
    @test static_method_count(f) == 1
    f(::Int) = 1
    @test static_method_count(f) == 2

    g(::Int) = 1
    @test static_method_count(g) == 1
    g(x) = x+1
    @test static_method_count(g) == 2

    @test static_method_count(Bar.h) == 1
    Bar.h(x) = x
    @test static_method_count(Bar.h) == 2

    # Code Generation
    code_typed = (@code_typed static_method_count(f))
    @test code_typed[2] === Int  # return type
    @test has_no_calls(code_typed[1].code)

    @testset "delete method" begin
        i(::Int) = 1
        @test static_method_count(i) == 1
        i(x) = x+1
        @test static_method_count(i) == 2

        Base.delete_method((first ∘ methods)(i))
        @test static_method_count(i) == 1

        Base.delete_method((first ∘ methods)(i))
        @test static_method_count(i) == 0

        i(x) = x+1
        @test static_method_count(i) == 1
    end
end

VERSION >= v"1.3" && @testset "closures" begin
    make_add_n(n) = x->x+n
    func = make_add_n(2)

    # Add a 0-arg method:
    (::typeof(func))() = func(0)

    @assert func(1) == 3
    @assert func() == 2

    @testset "static_hasmethod" begin
        @assert hasmethod(func, Tuple{Int}) == true
        @test static_hasmethod(func, Tuple{Int}) == true

        @assert hasmethod(func, Tuple{}) == true
        @test static_hasmethod(func, Tuple{}) == true
    end

    @testset "static_methods" begin
        @test collect(methods(func, Tuple{Int})) ==
                collect(static_methods(func, Tuple{Int}))
        @test length(static_methods(func, Tuple{Int})) == 1

        @test collect(methods(func, Tuple{})) ==
                collect(static_methods(func, Tuple{}))
        @test length(static_methods(func, Tuple{})) == 1

        @test collect(methods(func, Tuple{Int,Int,Int})) ==
                collect(static_methods(func, Tuple{Int,Int,Int}))
        @test length(static_methods(func, Tuple{Int,Int,Int})) == 0
    end

    @testset "static_method_count" begin
        @test length(collect(methods(func, Tuple{Int}))) ==
                static_method_count(func, Tuple{Int}) == 1

        @test length(collect(methods(func, Tuple{}))) ==
                static_method_count(func, Tuple{}) == 1

        @test length(collect(methods(func, Tuple{Int,Int,Int}))) ==
                static_method_count(func, Tuple{Int,Int,Int}) == 0
    end
end

@testset "static_field____" begin
    function foo(data)
        names = static_fieldnames(typeof(data))
        map(name -> getproperty(data, name), names)
    end
    @test (@inferred foo(:a => 1)) == (:a, 1)

    bar(::Type{T}) where {T} = Val{static_fieldcount(T)}()
    @test @inferred(bar(Complex{Int})) == Val(2)

    baz(::Type{T}) where {T} = static_fieldtypes(T)
    @test @inferred(baz(Complex{Int})) == (Int, Int)
end
