using Test, AbstractAlgebra, Groebner

supports_ordering = Groebner.monom_is_supported_ordering
monom_construct_from_vector = Groebner.monom_construct_from_vector
lex = (x, y) -> Groebner.monom_isless(x, y, Groebner.Lex())
dl = (x, y) -> Groebner.monom_isless(x, y, Groebner.DegLex())
drl = (x, y) -> Groebner.monom_isless(x, y, Groebner.DegRevLex())

implementations_to_test = [
    Groebner.ExponentVector{T} where {T},
    Groebner.PackedTuple1{T, UInt8} where {T},
    Groebner.PackedTuple2{T, UInt8} where {T},
    Groebner.PackedTuple3{T, UInt8} where {T},
    Groebner.PackedTuple4{T, UInt8} where {T},
    Groebner.NibbleMonom{16},
    Groebner.NibbleNoDeg{16},
    Groebner.FixedMonomNoDeg{16, UInt8},
    Groebner.FixedMonom{16, UInt8}
]

max_deg(::Type{<:Groebner.ExponentVector{T}}) where {T} = typemax(T)
max_deg(::Type{<:Groebner.AbstractPackedTuple{T, B}}) where {T, B} = typemax(B)
max_deg(::Type{Groebner.NibbleMonom{N}}) where {N} = 15
max_deg(::Type{Groebner.NibbleNoDeg{N}}) where {N} = 15
max_deg(::Type{Groebner.FixedMonomNoDeg{N, T}}) where {N, T} = typemax(T)
max_deg(::Type{Groebner.FixedMonom{N, T}}) where {N, T} = typemax(T)

@testset "monom orders: display" begin
    ord_display(ord) = begin
        io = IOBuffer()
        show(io, MIME("text/plain"), ord)
        String(take!(io))
    end

    ord = Groebner.WeightedOrdering(:x => 1, :y => 2, :z => 3)
    str = ord_display(ord)
    str = chopprefix(str, "WeightedOrdering(")
    str = chopsuffix(str, ")")
    @test issubset(split(str, ","), ["x=>1", "y=>2", "z=>3"])

    ord1 = Groebner.Lex(3:4)
    str1 = ord_display(ord1)
    ord2 = Groebner.DegRevLex(1:2)
    str2 = ord_display(ord2)

    ord = Groebner.ProductOrdering(Groebner.Lex(3:4), Groebner.DegRevLex(1:2))
    str = ord_display(ord)
    @test occursin(str1, str)
    @test occursin(str2, str)

    ord = Groebner.MatrixOrdering([1, 2, 3], [0 0 0; 0 1 0; 1 1 1])
    ord_display(ord)
end

@testset "monom orders: Lex, DegLex, DegRevLex" begin
    x = [1, 1, 1, 1, 1, 0, 1, 1, 0, 1]
    y = [1, 1, 1, 1, 1, 0, 1, 1, 0, 1]
    a = monom_construct_from_vector(Groebner.ExponentVector{UInt32}, x)
    b = monom_construct_from_vector(Groebner.ExponentVector{UInt32}, y)
    @test !lex(a, b) && !dl(a, b) && !drl(a, b)

    x = [1, 1, 1, 1, 1, 0, 1, 2, 0, 1]
    y = [1, 1, 1, 1, 1, 0, 2, 1, 0, 1]
    a = monom_construct_from_vector(Groebner.ExponentVector{UInt32}, x)
    b = monom_construct_from_vector(Groebner.ExponentVector{UInt32}, y)
    @test lex(a, b) && dl(a, b) && drl(a, b)

    n = 12
    for i in 2:n
        xx = ones(Int32, n)
        xx[i] = xx[i] + 1
        yy = ones(Int32, n)
        yy[i - 1] = yy[i - 1] + 1
        a = monom_construct_from_vector(Groebner.ExponentVector{UInt32}, xx)
        b = monom_construct_from_vector(Groebner.ExponentVector{UInt32}, yy)
        @test lex(a, b) && dl(a, b) && drl(a, b)
    end

    for T in (UInt64, UInt32, UInt16, UInt8)
        for EV in implementations_to_test
            MonomType = isconcretetype(EV) ? EV : EV{T}
            if MonomType <: Groebner.AbstractPackedTuple{T, UInt8} && T == UInt8
                continue
            end

            a = monom_construct_from_vector(MonomType, [0])
            b = monom_construct_from_vector(MonomType, [0])
            supports_ordering(MonomType, Groebner.DegRevLex()) && @test !drl(a, b)
            supports_ordering(MonomType, Groebner.Lex()) && @test !lex(a, b)
            supports_ordering(MonomType, Groebner.DegLex()) && @test !dl(a, b)

            a = monom_construct_from_vector(MonomType, [0])
            b = monom_construct_from_vector(MonomType, [1])
            supports_ordering(MonomType, Groebner.DegRevLex()) && @test drl(a, b)
            supports_ordering(MonomType, Groebner.DegRevLex()) && @test !drl(b, a)
            supports_ordering(MonomType, Groebner.Lex()) && @test lex(a, b)
            supports_ordering(MonomType, Groebner.DegLex()) && @test dl(a, b)
            supports_ordering(MonomType, Groebner.Lex()) && @test !lex(b, a)
            supports_ordering(MonomType, Groebner.DegLex()) && @test !dl(b, a)

            2 > Groebner.monom_max_vars(MonomType) && continue
            a = monom_construct_from_vector(MonomType, [1, 1])
            b = monom_construct_from_vector(MonomType, [2, 0])
            supports_ordering(MonomType, Groebner.DegRevLex()) && @test drl(a, b)
            supports_ordering(MonomType, Groebner.DegRevLex()) && @test !drl(b, a)
            supports_ordering(MonomType, Groebner.Lex()) && @test lex(a, b)
            supports_ordering(MonomType, Groebner.DegLex()) && @test dl(a, b)
            supports_ordering(MonomType, Groebner.Lex()) && @test !lex(b, a)
            supports_ordering(MonomType, Groebner.DegLex()) && @test !dl(b, a)

            a = monom_construct_from_vector(MonomType, [1, 1])
            b = monom_construct_from_vector(MonomType, [0, 2])
            supports_ordering(MonomType, Groebner.DegRevLex()) && @test drl(b, a)
            supports_ordering(MonomType, Groebner.DegRevLex()) && @test !drl(a, b)
            supports_ordering(MonomType, Groebner.Lex()) && @test !lex(a, b)
            supports_ordering(MonomType, Groebner.DegLex()) && @test !dl(a, b)
            supports_ordering(MonomType, Groebner.Lex()) && @test lex(b, a)
            supports_ordering(MonomType, Groebner.DegLex()) && @test dl(b, a)

            a = monom_construct_from_vector(MonomType, [1, 1])
            b = monom_construct_from_vector(MonomType, [1, 1])
            supports_ordering(MonomType, Groebner.DegRevLex()) && @test !drl(a, b)
            supports_ordering(MonomType, Groebner.Lex()) && @test !lex(a, b)
            supports_ordering(MonomType, Groebner.DegLex()) && @test !dl(a, b)

            a = monom_construct_from_vector(MonomType, [1, 1])
            b = monom_construct_from_vector(MonomType, [2, 2])
            supports_ordering(MonomType, Groebner.DegRevLex()) && @test !drl(b, a)
            supports_ordering(MonomType, Groebner.DegRevLex()) && @test drl(a, b)
            supports_ordering(MonomType, Groebner.Lex()) && @test lex(a, b)
            supports_ordering(MonomType, Groebner.DegLex()) && @test dl(a, b)
            supports_ordering(MonomType, Groebner.Lex()) && @test !lex(b, a)
            supports_ordering(MonomType, Groebner.DegLex()) && @test !dl(b, a)

            3 > Groebner.monom_max_vars(MonomType) && continue
            a = monom_construct_from_vector(MonomType, [1, 0, 2])
            b = monom_construct_from_vector(MonomType, [2, 0, 1])
            supports_ordering(MonomType, Groebner.DegRevLex()) && @test drl(a, b)
            supports_ordering(MonomType, Groebner.Lex()) && @test lex(a, b)
            supports_ordering(MonomType, Groebner.DegLex()) && @test dl(a, b)

            a = monom_construct_from_vector(MonomType, [1, 5, 0])
            b = monom_construct_from_vector(MonomType, [1, 0, 1])
            @test !drl(a, b)

            25 > Groebner.monom_max_vars(MonomType) && continue
            a = monom_construct_from_vector(MonomType, ones(UInt, 25))
            b = monom_construct_from_vector(MonomType, ones(UInt, 25))
            supports_ordering(MonomType, Groebner.DegRevLex()) && @test !drl(a, b)
            supports_ordering(MonomType, Groebner.Lex()) && @test !lex(a, b)
            supports_ordering(MonomType, Groebner.DegLex()) && @test !dl(a, b)

            30 > Groebner.monom_max_vars(MonomType) && continue
            a = monom_construct_from_vector(MonomType, ones(UInt, 30))
            b = monom_construct_from_vector(MonomType, ones(UInt, 30))
            supports_ordering(MonomType, Groebner.DegRevLex()) && @test !drl(a, b)
            supports_ordering(MonomType, Groebner.Lex()) && @test !lex(a, b)
            supports_ordering(MonomType, Groebner.DegLex()) && @test !dl(a, b)
        end

        # test that different implementations agree
        for n in 1:10
            k = rand(1:60)

            implementations_to_test_local =
                map(EV -> isconcretetype(EV) ? EV : EV{T}, implementations_to_test)
            implementations_to_test_local =
                filter(MT -> Groebner.monom_max_vars(MT) >= k, implementations_to_test_local)

            t = div(typemax(UInt8), 2k) - 1
            t = min(t, div(minimum(max_deg.(implementations_to_test_local)), 2))
            x, y = rand(0:t, k), rand(0:t, k)
            as = [
                monom_construct_from_vector(MonomType, x) for
                MonomType in implementations_to_test_local
            ]
            bs = [
                monom_construct_from_vector(MonomType, y) for
                MonomType in implementations_to_test_local
            ]

            if all(supports_ordering.(implementations_to_test_local, Ref(Groebner.DegRevLex())))
                @test length(unique(map(drl, as, bs))) == 1
            end
            if all(supports_ordering.(implementations_to_test_local, Ref(Groebner.Lex())))
                @test length(unique(map(lex, as, bs))) == 1
            end
            if all(supports_ordering.(implementations_to_test_local, Ref(Groebner.DegLex())))
                @test length(unique(map(dl, as, bs))) == 1
            end

            # test that a < b && b < a does not happen
            for (a, b) in zip(as, bs)
                k > Groebner.monom_max_vars(a) && continue
                if supports_ordering(typeof(a), Groebner.Lex()) && lex(a, b)
                    @test !lex(b, a)
                end
                if supports_ordering(typeof(a), Groebner.DegLex()) && dl(a, b)
                    @test !dl(b, a)
                end
                if supports_ordering(typeof(a), Groebner.DegRevLex()) && drl(a, b)
                    @test !drl(b, a)
                end
            end
        end
    end
end

function test_circular_shift(a, b, n, Ord, answers)
    R, x = AbstractAlgebra.QQ[["x$i" for i in 1:n]...]
    vars_to_index = Dict(x .=> 1:n)
    orders = map(i -> Groebner.ordering_transform(Ord(circshift(x, -i)), vars_to_index), 0:(n - 1))
    cmps = map(ord -> ((x, y) -> Groebner.monom_isless(x, y, ord)), orders)

    for (cmp, ord, answer) in zip(cmps, orders, answers)
        if supports_ordering(typeof(a), ord)
            @test answer == cmp(a, b) && !answer == cmp(b, a)
        end
    end
end

@testset "monoms, variable permutation" begin
    for T in (UInt64, UInt32, UInt16)
        for EV in implementations_to_test
            MonomType = isconcretetype(EV) ? EV : EV{T}

            n = 3
            n >= Groebner.monom_max_vars(MonomType) && continue

            a = monom_construct_from_vector(MonomType, [5, 5, 3])
            b = monom_construct_from_vector(MonomType, [1, 1, 10])

            test_circular_shift(a, b, n, Groebner.Lex, [false, false, true])
            test_circular_shift(a, b, n, Groebner.DegLex, [false, false, false])
            test_circular_shift(a, b, n, Groebner.DegRevLex, [false, false, false])

            n = 5
            n >= Groebner.monom_max_vars(MonomType) && continue

            a = monom_construct_from_vector(MonomType, [1, 2, 3, 4, 5])
            b = monom_construct_from_vector(MonomType, [4, 3, 2, 1, 5])

            test_circular_shift(a, b, n, Groebner.Lex, [true, true, false, false, true])
            test_circular_shift(a, b, n, Groebner.DegLex, [true, true, false, false, true])
            test_circular_shift(a, b, n, Groebner.DegRevLex, [true, false, false, true, true])
        end
    end
end

function test_orderings(n, v1, v2, ords_to_test)
    R, x = AbstractAlgebra.QQ[["x$i" for i in 1:n]...]
    var_to_index = Dict(x .=> 1:n)
    for wo in ords_to_test
        ord = wo.ord
        ans = wo.ans
        internal_ord = Groebner.ordering_transform(ord, var_to_index)
        @test Groebner.monom_isless(v1, v2, internal_ord) == ans[1]
        @test Groebner.monom_isless(v2, v1, internal_ord) == ans[2]
        @test Groebner.monom_isless(v2, v2, internal_ord) == false
        @test Groebner.monom_isless(v1, v1, internal_ord) == false
    end
end

@testset "monom orders: WeightedOrdering" begin
    R, x = AbstractAlgebra.QQ[["x$i" for i in 1:3]...]

    pv = Groebner.ExponentVector{T} where {T}

    v1 = Groebner.monom_construct_from_vector(pv{UInt64}, [1, 2, 3])
    v2 = Groebner.monom_construct_from_vector(pv{UInt64}, [3, 2, 1])

    @test_throws DomainError Groebner.WeightedOrdering(Dict(x .=> [-1, 0, 0]))

    ords_to_test = [
        (ord=Groebner.WeightedOrdering(Dict(x .=> [1, 1, 1])), ans=[true, false]),
        (ord=Groebner.WeightedOrdering(Dict(x .=> [0, 0, 1])), ans=[false, true]),
        (ord=Groebner.WeightedOrdering(Dict(x .=> [0, 1, 0])), ans=[true, false]),
        (ord=Groebner.WeightedOrdering(Dict(x .=> [1, 0, 0])), ans=[true, false]),
        (ord=Groebner.WeightedOrdering(Dict(x .=> [1, 1, 5])), ans=[false, true]),
        (ord=Groebner.WeightedOrdering(Dict(x .=> [1, 0, 0])), ans=[true, false])
    ]

    test_orderings(3, v1, v2, ords_to_test)

    v1 = Groebner.monom_construct_from_vector(pv{UInt64}, [1, 2, 3])
    v2 = Groebner.monom_construct_from_vector(pv{UInt64}, [1, 2, 3])

    ords_to_test = [
        (ord=Groebner.WeightedOrdering(Dict(x .=> [1, 1, 1])), ans=[false, false]),
        (ord=Groebner.WeightedOrdering(Dict(x .=> [0, 0, 1])), ans=[false, false]),
        (ord=Groebner.WeightedOrdering(Dict(x .=> [0, 1, 0])), ans=[false, false]),
        (ord=Groebner.WeightedOrdering(Dict(x .=> [1, 0, 0])), ans=[false, false]),
        (ord=Groebner.WeightedOrdering(Dict(x .=> [1, 1, 5])), ans=[false, false]),
        (ord=Groebner.WeightedOrdering(Dict(x .=> [1, 0, 0])), ans=[false, false])
    ]

    test_orderings(3, v1, v2, ords_to_test)

    R, x = AbstractAlgebra.QQ[["x$i" for i in 1:6]...]

    v1 = Groebner.monom_construct_from_vector(pv{UInt64}, [1, 2, 3, 0, 0, 7])
    v2 = Groebner.monom_construct_from_vector(pv{UInt64}, [4, 5, 0, 0, 1, 4])

    ords_to_test = [
        (ord=Groebner.WeightedOrdering(Dict(x .=> [1, 1, 1, 1, 1, 1])), ans=[true, false]),
        (ord=Groebner.WeightedOrdering(Dict(x .=> [0, 0, 0, 0, 0, 4])), ans=[false, true]),
        (ord=Groebner.WeightedOrdering(Dict(x .=> [0, 2, 5, 0, 0, 0])), ans=[false, true]),
        (ord=Groebner.WeightedOrdering(Dict(x .=> [0, 2, 2, 0, 0, 0])), ans=[true, false])
    ]

    test_orderings(6, v1, v2, ords_to_test)
end

@testset "monom orders: ProductOrdering" begin
    pv = Groebner.ExponentVector{T} where {T}

    R, x = AbstractAlgebra.QQ[["x$i" for i in 1:3]...]
    v1 = Groebner.monom_construct_from_vector(pv{UInt64}, [1, 2, 3])
    v2 = Groebner.monom_construct_from_vector(pv{UInt64}, [3, 2, 1])

    ords_to_test = [
        (
            ord=Groebner.ProductOrdering(Groebner.Lex(x[1]), Groebner.DegLex(x[2], x[3])),
            ans=[true, false]
        ),
        (
            ord=Groebner.ProductOrdering(Groebner.DegRevLex(x[1:2]), Groebner.Lex(x[3])),
            ans=[true, false]
        )
    ]

    test_orderings(3, v1, v2, ords_to_test)

    R, x = AbstractAlgebra.QQ[["x$i" for i in 1:6]...]
    v1 = Groebner.monom_construct_from_vector(pv{UInt64}, [4, 1, 7, 0, 9, 8])
    v2 = Groebner.monom_construct_from_vector(pv{UInt64}, [1, 6, 3, 5, 9, 100])

    ords_to_test = [
        (
            ord=Groebner.ProductOrdering(Groebner.DegLex(x[1]), Groebner.DegLex(x[2:6])),
            ans=[false, true]
        ),
        (
            ord=Groebner.ProductOrdering(Groebner.DegLex(x[1:2]), Groebner.DegLex(x[3:6])),
            ans=[true, false]
        ),
        (
            ord=Groebner.ProductOrdering(Groebner.DegLex(x[1:3]), Groebner.DegLex(x[4:6])),
            ans=[false, true]
        ),
        (
            ord=Groebner.ProductOrdering(Groebner.DegLex(x[1:4]), Groebner.DegLex(x[5:6])),
            ans=[true, false]
        ),
        (
            ord=Groebner.ProductOrdering(
                Groebner.ProductOrdering(Groebner.DegLex(x[1:2]), Groebner.DegLex(x[3:4])),
                Groebner.DegLex(x[5:6])
            ),
            ans=[true, false]
        ),
        (
            ord=Groebner.ProductOrdering(
                Groebner.ProductOrdering(Groebner.DegLex(x[1:3]), Groebner.DegLex(x[4])),
                Groebner.DegLex(x[5:6])
            ),
            ans=[false, true]
        ),
        (
            ord=Groebner.DegLex(x[1:3]) * Groebner.DegLex(x[4]) * Groebner.DegLex(x[5:6]),
            ans=[false, true]
        )
    ]

    test_orderings(6, v1, v2, ords_to_test)
end

@testset "monom orders: MatrixOrdering" begin
    R, x = AbstractAlgebra.QQ[["x$i" for i in 1:3]...]

    pv = Groebner.ExponentVector{T} where {T}

    v1 = Groebner.monom_construct_from_vector(pv{UInt64}, [1, 2, 3])
    v2 = Groebner.monom_construct_from_vector(pv{UInt64}, [3, 2, 1])

    ord1 = Groebner.MatrixOrdering(
        x,
        [
            1 0 0
            0 1 0
            0 0 1
        ]
    )
    ord2 = Groebner.MatrixOrdering(
        x,
        [
            1 0 2;
        ]
    )
    ord3 = Groebner.MatrixOrdering(
        x,
        [
            0 0 0
            0 1 0
            1 1 1
        ]
    )
    @test_throws DomainError Groebner.MatrixOrdering(
        x,
        [
            1 0;
        ]
    )

    mo_to_test = [
        (ord=ord1, ans=[true, false]),
        (ord=ord2, ans=[false, true]),
        (ord=ord3, ans=[false, false])
    ]
    test_orderings(3, v1, v2, mo_to_test)
end
