using Symbolics
import Symbolics: symbolic_to_float, var_from_nested_derivative, unwrap, 
                  isblock, flatten_expr!, build_expr, get_variables, get_differential_vars,
                  is_singleton, diff2term, tosymbol, lower_varname, 
                  makesubscripts, degree, coeff
using SparseArrays
using Test

@testset "get_variables" begin
    @variables t x y z(t)

    ex1 = x + y + sin(z)
    vars1 = Symbolics.get_variables(ex1)
    @test length(vars1) == 3
    @test allunique(vars1)

    sorted_vars1 = Symbolics.get_variables(ex1; sort = true)
    @test isequal(sorted_vars1, [x, y, z])

    ex2 = x - y
    vars2 = Symbolics.get_variables(ex2)
    @test length(vars2) == 2
    @test allunique(vars2)

    sorted_vars2 = Symbolics.get_variables(ex2; sort = true)
    @test isequal(sorted_vars2, [x, y])

    @variables c(..)
    ex3 = c(x) + c(t) - c(c(t) + y)
    vars3 = Symbolics.get_variables(ex3)
    @test length(vars3) == 4

    sorted_vars3 = Symbolics.get_variables(ex3; sort = true)
    @test isequal(sorted_vars3, [c.f, t, x, y])
end

@testset "symbolic_to_float" begin
    @variables x
    @test symbolic_to_float((1//2 * x)/x) isa Rational{Int}
    @test symbolic_to_float((1/2 * x)/x) isa Float64
    @test symbolic_to_float((1//2)*√(279//4)) isa Float64
    @test symbolic_to_float((big(1)//2)*√(279//4)) isa BigFloat
    @test symbolic_to_float((-1//2)*√(279//4)) isa Float64
end

@testset "var_from_nested_derivative" begin
    @variables t x(t) p(..)
    D = Differential(t)
    @test var_from_nested_derivative(x) == (x, 0)
    @test var_from_nested_derivative(D(x)) == (x, 1)
    @test var_from_nested_derivative(p) == (p, 0)
    @test var_from_nested_derivative(D(p(x))) == (p(x), 1)
end

@testset "fixpoint_sub maxiters" begin
    @variables x y
    expr = Symbolics.fixpoint_sub(x, Dict(x => y, y => x))
    @test isequal(expr, x)
    expr = Symbolics.fixpoint_sub(x, Dict(x => y, y => x); maxiters = 9)
    @test isequal(expr, y)
end

@testset "Issue#1342 substitute working on called symbolics" begin
    @variables p(..) x y
    arg = unwrap(substitute(p(x), [p => identity]))
    @test iscall(arg) && operation(arg) == identity && isequal(only(arguments(arg)), x)
    @test unwrap(substitute(p(x), [p => sqrt, x => 4.0])) ≈ 2.0
    arg = Symbolics.fixpoint_sub(p(x), [p => sqrt, x => 2y + 3, y => 1.0 + p(4)])
    @test arg ≈ 3.0
end

# Helper Functions
@variables x y z t u(x, t) v(t) w[1:2]

@testset "isblock" begin
    @test isblock([Expr(:block, :(x + y))]) == true
    @test isblock([Expr(:call, :f, x, y)]) == false
    @test isblock([Expr(:block, :(begin x; y; end))]) == true
    @test isblock([Expr(:return, :(x + y))]) == false
end

@testset "flatten_expr!" begin
    expr = Expr(:block, :(x + y))
    @test flatten_expr!(expr.args) == Any[:(x + y)]

    expr2 = Expr(:block, :(begin x + y; z; end))
    @test flatten_expr!(expr2.args) == Any[:(x + y), :z]
end

@testset "build_expr" begin
    expr = build_expr(:block, [:(x + y), :(y + z)])
    @test expr.head == :block
    @test expr.args == [:(x + y), :(y + z)]
end

@testset "is_singleton" begin
    @test is_singleton(x) == false
    @test is_singleton(sin(x)) == false
    @test is_singleton(u) == false
end

@testset "tosymbol" begin
    expr1 = sin(x)
    @test tosymbol(expr1) == Symbol("sin(x)")

    expr2 = cos(y)
    @test tosymbol(expr2) == Symbol("cos(y)")

    expr4 = u
    @test tosymbol(expr4) == Symbol("u(x, t)")
end

@testset "degree" begin
    expr1 = x^2 + y^3
    @test degree(expr1, x) == 2
    @test degree(expr1, y) == 3

    expr2 = x * y^2 + x^3
    @test degree(expr2, x) == 3
    @test degree(expr2, y) == 2

    expr3 = 1
    @test degree(expr3, x) == 0
end

@testset "coeff" begin
    expr1 = 3x + 2y
    @test coeff(expr1, x) == 3
    @test coeff(expr1, y) == 2
    @test coeff(expr1, x^2) == 0

    expr2 = x^2 + 3x + 2
    @test coeff(expr2, x) == 3
    @test coeff(expr2, x^2) == 1
end

@testset "makesubscripts" begin
    sub1 = makesubscripts(5)
    @test length(sub1) == 5
    @test typeof(sub1[1]) == SymbolicUtils.BasicSymbolic{Int64}

    sub2 = makesubscripts(10)
    @test length(sub2) == 10
end

@testset "diff2term" begin
    @variables x t u(x, t) z(t)
    Dt = Differential(t)
    Dx = Differential(x)

    test_var = x
    result = diff2term(test_var)
    @test result === x

    test_nested_derivative = Dx(Dt(Dt(u)))
    result = diff2term(Symbolics.value(test_nested_derivative))
    @test typeof(result) === Symbolics.BasicSymbolic{Real}

    @testset "staged diff2term on arrays" begin
        @variables t x(t)[1:2]
        D = Differential(t)
        xt = diff2term(unwrap(D(x[1])))
        xtt = diff2term(D(xt))
        xtt_true = diff2term(unwrap(D(D(x[1]))))
        @test isequal(xtt, xtt_true)
    end
end

@testset "get_differential_vars" begin
    @variables t x u(x, t) v(t) w[1:2]
    Dt = Differential(t)
    Dx = Differential(x)

    # Test with no differentials
    expr1 = u + v + x
    diff_vars1 = Symbolics.get_differential_vars(expr1)
    @test length(diff_vars1) == 0

    # Test with single differential
    expr2 = Dt(u) + u
    diff_vars2 = Symbolics.get_differential_vars(expr2)
    @test length(diff_vars2) == 1
    @test isequal(diff_vars2[1], Dt(u))

    # Test with multiple differentials
    expr3 = Dx(u) + Dt(u) + sin(Dt(v))
    diff_vars3 = Symbolics.get_differential_vars(expr3)
    @test length(diff_vars3) == 3
    @test any(isequal(Symbolics.value(Dt(u))), diff_vars3)
    @test any(isequal(Symbolics.value(Dx(u))), diff_vars3)
    @test any(isequal(Symbolics.value(Dt(v))), diff_vars3)

    # Test with nested differentials (finds both outer and inner)
    expr4 = Dx(Dt(u)) + u
    diff_vars4 = Symbolics.get_differential_vars(expr4)
    @test length(diff_vars4) == 2
    @test any(isequal(Symbolics.value(Dx(Dt(u)))), diff_vars4)
    @test any(isequal(Symbolics.value(Dt(u))), diff_vars4)

    # Test with duplicates (should be unique)
    expr5 = Dt(u) + 2*Dt(u) + sin(Dt(u))
    diff_vars5 = Symbolics.get_differential_vars(expr5)
    @test length(diff_vars5) == 1
    @test isequal(diff_vars5[1], Dt(u))

    # Test sorting
    expr6 = Dt(v) + Dx(u) + Dt(u)
    diff_vars6_sorted = Symbolics.get_differential_vars(expr6; sort = true)
    @test length(diff_vars6_sorted) == 3
    # Should be sorted by string representation
    strings = string.(diff_vars6_sorted)
    @test issorted(strings)

    # Test with varlist restriction  
    target_vars = [Symbolics.value(Dt(u)), Symbolics.value(Dx(u))]
    expr7 = Dt(u) + Dx(u) + Dt(v) + u
    diff_vars7 = Symbolics.get_differential_vars(expr7, target_vars)
    @test length(diff_vars7) == 2
    @test any(isequal(Symbolics.value(Dt(u))), diff_vars7)
    @test any(isequal(Symbolics.value(Dx(u))), diff_vars7)
    @test !any(isequal(Symbolics.value(Dt(v))), diff_vars7)

    # Test with Num wrapper
    expr8 = Num(Dt(u)) + Num(u)
    diff_vars8 = Symbolics.get_differential_vars(expr8)
    @test length(diff_vars8) == 1
    @test isequal(diff_vars8[1], Dt(u))

    # Test with array elements
    @variables z(t)[1:2]
    Dz1 = Dt(z[1])
    Dz2 = Dt(z[2])
    expr9 = Dz1 + Dz2 + z[1]
    diff_vars9 = Symbolics.get_differential_vars(expr9)
    @test length(diff_vars9) == 2
    @test any(isequal(Symbolics.value(Dz1)), diff_vars9)
    @test any(isequal(Symbolics.value(Dz2)), diff_vars9)

    # Test with equations
    eq = Dt(u) ~ Dx(u) + u
    diff_vars_eq = Symbolics.get_differential_vars(eq)
    @test length(diff_vars_eq) == 2
    @test any(isequal(Symbolics.value(Dt(u))), diff_vars_eq)
    @test any(isequal(Symbolics.value(Dx(u))), diff_vars_eq)
end

@testset "`fast_substitute` inside array symbolics" begin
    @variables x y z
    @register_symbolic foo(a::AbstractArray, b)
    ex = foo([x, y], z)
    ex2 = Symbolics.fixpoint_sub(ex, Dict(y => 1.0, z => 2.0))
    @test isequal(ex2, foo([x, 1.0], 2.0))
end

@testset "`fast_substitute` of subarray symbolics" begin
    @variables p[1:4] q[1:5]
    @test isequal(p[1:2], Symbolics.fast_substitute(p[1:2], Dict()))
    @test isequal(p[1:2], Symbolics.fast_substitute(p[1:2], p => p))
    @test isequal(q[1:2], Symbolics.fast_substitute(p[1:2], Dict(p => q)))
    @test isequal(q[1:2], Symbolics.fast_substitute(p[1:2], p => q))
end

@testset "`fast_substitute` folding `getindex`" begin
    @variables x[1:3]
    @test isequal(Symbolics.fast_substitute(x[1], Dict(unwrap(x) => collect(unwrap(x)))), x[1])
    @test isequal(Symbolics.fast_substitute(x[1], unwrap(x) => collect(unwrap(x))), x[1])
end

@testset "`fixpoint_sub` and `fast_substitute` on sparse arrays" begin
    @variables x y z
    mat = Num[x 0 0; 0 y 0; 0 0 z]
    mat = sparse(mat)
    mat = unwrap.(mat)
    rules = Dict(x => y, y => z, z => 1)
    res = Symbolics.fixpoint_sub(mat, rules)
    @test res isa SparseMatrixCSC
    @test res[1, 1] == res[2, 2] == res[3, 3] == 1
end

@testset "numerator and denominator" begin
    @variables x y
    num_den(x) = (numerator(x), denominator(x))
    @test num_den(x) == (x, 1)
    @test num_den(1/x) == (1, x)
    @test num_den(x/y) == (x, y)
end

@testset "factors and terms" begin
    @variables x y z

    @test Set(factors(0)) == Set([0])
    @test Set(factors(1)) == Set([1])
    @test Set(factors(x)) == Set([x])
    @test Set(factors(x*y*z)) == Set([x, y, z])

    @test Set(terms(0)) == Set([0])
    @test Set(terms(x)) == Set([x])
    @test Set(terms(x + y + z)) == Set([x, y, z])
    @test Set(terms(-x - y + z)) == Set([-x, -y, z])
end

@testset "evaluate" begin
    @variables x y
    eqn_1 = x ~ y 
    gtr = x ≳ y
    ltr = x ≲ y

    @test Symbolics.evaluate(eqn_1, Dict(x => 1, y => 1))
    @test !Symbolics.evaluate(eqn_1, Dict(x => 1, y => 2))
    @test !Symbolics.evaluate(gtr, Dict(x => 1, y => 2))
    @test Symbolics.evaluate(gtr, Dict(x => 2, y => 1))
    @test Symbolics.evaluate(ltr, Dict(x => 1, y => 2))
    @test !Symbolics.evaluate(ltr, Dict(x => 2, y => 1))
end



