package prolog.logic

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.RepeatedTest
import org.junit.jupiter.api.Test
import prolog.Substitutions
import prolog.ast.arithmetic.Integer
import prolog.ast.arithmetic.Float
import prolog.ast.terms.Term
import prolog.ast.terms.Variable

class ArithmeticTests {
    @Test
    fun `1_between_0_and_2`() {
        val result = between(Integer(0), Integer(2), Integer(1))

        assertTrue(result.any(), "Expected 1 to be between 0 and 2")
        assertTrue(result.first().isSuccess, "Expected success")

        val subs = result.first().getOrNull()!!

        assertEquals(emptyMap<Variable, Term>(), subs, "Expected no substitutions")
    }

    @Test
    fun `3_not_between_0_and_2`() {
        val result = between(Integer(0), Integer(2), Integer(3))

        assertTrue(result.none(), "Expected 3 not to be between 0 and 2")
    }

    @Test
    fun numbers_between_0_and_2() {
        val result = between(Integer(0), Integer(2), Variable("X"))

        val expectedResults = listOf(
            mapOf(Variable("X") to Integer(0)),
            mapOf(Variable("X") to Integer(1)),
            mapOf(Variable("X") to Integer(2))
        )

        val actualResults = result.toList()
        assertEquals(expectedResults.size, actualResults.size, "Expected 3 results")
        for ((expected, actual) in expectedResults.zip(actualResults)) {
            for ((key, value) in expected) {
                assertTrue(actual.isSuccess, "Expected success")
                val actual = actual.getOrNull()!!
                assertTrue(actual.containsKey(key), "Expected key $key to be present")
                assertTrue(
                    equivalent(value, actual[key]!!, emptyMap()),
                    "Expected value $value for key $key, but got ${actual[key]}"
                )
            }
        }
    }

    @Test
    fun `succ(0) is 1`() {
        val t1 = Integer(0)

        val expected = Integer(1)

        val result = succ(t1, expected, emptyMap()).toList()

        assertEquals(1, result.size, "Expected 0 + 1 to be equal to 1")
        assertTrue(result[0].isSuccess, "Expected success")
        assertTrue(result[0].getOrNull()!!.isEmpty(), "Expected no substitutions")
    }

    @Test
    fun `succ(1) is 2`() {
        val t1 = Integer(1)

        val expected = Integer(2)

        val result = succ(t1, expected, emptyMap()).toList()

        assertEquals(1, result.size, "Expected 1 + 1 to be equal to 2")
        assertTrue(result[0].isSuccess, "Expected success")
        assertTrue(result[0].getOrNull()!!.isEmpty(), "Expected no substitutions")
    }

    @Test
    fun `succ(1) is var`() {
        val t1 = Integer(1)
        val t2 = Variable("X")

        val expected = Integer(2)

        val result = succ(t1, t2, emptyMap()).toList()

        assertEquals(1, result.size, "Expected 1 + 1 to be equal to X")
        assertTrue(result[0].isSuccess, "Expected success")
        assertEquals(expected, result[0].getOrNull()!![t2], "Expected X to be equal to 2")
    }

    @Test
    fun `succ(bound-to-1-var) is 2`() {
        val t1 = Variable("X")
        val t2 = Integer(2)

        val result = succ(t1, t2, mapOf(t1 to Integer(1))).toList()

        assertEquals(1, result.size, "Expected X + 1 to be equal to 2")
        assertTrue(result[0].isSuccess, "Expected success")
        assertTrue(result[0].getOrNull()!!.isEmpty(), "Expected no substitutions")
    }

    @Test
    fun `succ(bound-to-1-var) is var`() {
        val t1 = Variable("X")
        val t2 = Variable("Y")

        val result = succ(t1, t2, mapOf(t1 to Integer(1))).toList()

        assertEquals(1, result.size, "Expected X + 1 to be equal to Y")
        assertTrue(result[0].isSuccess, "Expected success")
        assertEquals(Integer(2), result[0].getOrNull()!![t2], "Expected Y to be equal to 2")
    }

    @Test
    fun `succ(var, 0) fails silently`() {
        val t1 = Variable("X")
        val t2 = Integer(0)

        val result = succ(t1, t2, emptyMap()).toList()

        assertTrue(result.isEmpty(), "Expected X + 1 to fail")
    }

    @Test
    fun `1_plus_2_is_3`() {
        val t1 = Integer(1)
        val t2 = Integer(2)
        val t3 = Integer(3)

        val result = plus(t1, t2, t3, emptyMap()).toList()

        assertEquals(1, result.size, "1 + 2 should be equal to 3")
        assertTrue(result[0].isSuccess, "Expected success")
        assertTrue(result[0].getOrNull()!!.isEmpty(), "1 + 2 should be equal to 3")
    }

    @Test
    fun `1,0 + 2,0 = 3,0`() {
        val t1 = Float(1.0f)
        val t2 = Float(2.0f)
        val t3 = Float(3.0f)

        val result = plus(t1, t2, t3, emptyMap()).toList()

        assertEquals(1, result.size, "There should be one solution")
        assertTrue(result[0].isSuccess, "Expected success")
        assertTrue(result[0].getOrNull()!!.isEmpty(), "1.0 + 1.0 should already be equal to 2.0")
    }

    @Test
    fun `1_plus_2_is_not_4`() {
        val t1 = Integer(1)
        val t2 = Integer(2)
        val t3 = Integer(4)

        val result = plus(t1, t2, t3, emptyMap())

        assertTrue(result.none(), "1 + 2 should not be equal to 4")
    }

    @Test
    fun `1,0 plus 2,0 is not 4,0`() {
        val t1 = Float(1.0f)
        val t2 = Float(2.0f)
        val t3 = Float(4.0f)

        val result = plus(t1, t2, t3, emptyMap())

        assertTrue(result.none(), "1.0 + 2.0 should not be equal to 4.0")
    }

    @Test
    fun `1_plus_2_is_variable`() {
        val t1 = Integer(1)
        val t2 = Integer(2)
        val t3 = Variable("X")

        val result = plus(t1, t2, t3, emptyMap()).toList()

        assertEquals(1, result.size, "1 + 2 should be equal to X")
        assertTrue(result[0].isSuccess, "Expected success")
        assertEquals(Integer(3), result[0].getOrNull()!![t3], "X should be equal to 3")
    }

    @Test
    fun `1,0 plus 2,0 is variable`() {
        val t1 = Float(1.0f)
        val t2 = Float(2.0f)
        val t3 = Variable("X")

        val result = plus(t1, t2, t3, emptyMap()).toList()

        assertEquals(1, result.size, "1.0 + 2.0 should be equal to X")
        assertTrue(result[0].isSuccess, "Expected success")

        val subs = result[0].getOrNull()!!

        assertTrue(equivalent(Float(3.0f), subs[t3]!!, emptyMap()), "X should be equal to 3.0")
    }

    @Test
    fun `1_plus_variable_is_3`() {
        val t1 = Integer(1)
        val t2 = Variable("X")
        val t3 = Integer(3)

        val result = plus(t1, t2, t3, emptyMap()).toList()

        assertEquals(1, result.size, "1 + X should be equal to 3")
        assertTrue(result[0].isSuccess, "Expected success")
        assertEquals(Integer(2), result[0].getOrNull()!![t2], "X should be equal to 2")
    }

    @Test
    fun `1,0 plus variable is 3,0`() {
        val t1 = Float(1.0f)
        val t2 = Variable("X")
        val t3 = Float(3.0f)

        val result = plus(t1, t2, t3, emptyMap()).toList()

        assertEquals(1, result.size, "1.0 + X should be equal to 3.0")
        assertTrue(result[0].isSuccess, "Expected success")

        val subs = result[0].getOrNull()!!

        assertTrue(equivalent(Float(2.0f), subs[t2]!!, emptyMap()), "X should be equal to 2.0")
    }

    @Test
    fun variable_plus_2_is_3() {
        val t1 = Variable("X")
        val t2 = Integer(2)
        val t3 = Integer(3)

        val result = plus(t1, t2, t3, emptyMap()).toList()

        assertEquals(1, result.size, "X + 2 should be equal to 3")
        assertTrue(result[0].isSuccess, "Expected success")
        assertEquals(Integer(1), result[0].getOrNull()!![t1], "X should be equal to 1")
    }

    @Test
    fun `variable plus 2,0 is 3,0`() {
        val t1 = Variable("X")
        val t2 = Float(2.0f)
        val t3 = Float(3.0f)

        val result = plus(t1, t2, t3, emptyMap()).toList()

        assertEquals(1, result.size, "X + 2.0 should be equal to 3.0")
        assertTrue(result[0].isSuccess, "Expected success")

        val subs = result[0].getOrNull()!!

        assertTrue(equivalent(Float(1.0f), subs[t1]!!, emptyMap()), "X should be equal to 1.0")
    }

    @Test
    fun `1 plus 2 is bound-to-3-var`() {
        val t1 = Integer(1)
        val t2 = Integer(2)
        val t3 = Variable("X")

        val result = plus(t1, t2, t3, mapOf(Variable("X") to Integer(3))).toList()

        assertEquals(1, result.size, "1 + 2 should be equal to X")
        assertTrue(result[0].isSuccess, "Expected success")
        assertTrue(result[0].getOrNull()!!.isEmpty(), "t3 should not be rebound")
    }

    @Test
    fun `1,0 plus 2,0 is bound-to-3,0-var`() {
        val t1 = Float(1.0f)
        val t2 = Float(2.0f)
        val t3 = Variable("X")

        val result = plus(t1, t2, t3, mapOf(Variable("X") to Float(3.0f))).toList()

        assertEquals(1, result.size, "1.0 + 2.0 should be equal to X")
        assertTrue(result[0].isSuccess, "Expected success")
        assertTrue(result[0].getOrNull()!!.isEmpty(), "t3 should not be rebound")
    }

    @Test
    fun `1 plus 2 is bound-to-4-var`() {
        val t1 = Integer(1)
        val t2 = Integer(2)
        val t3 = Variable("X")

        val result = plus(t1, t2, t3, mapOf(t3 to Integer(4)))

        assertTrue(result.none(), "1 + 2 should not be equal to X")
    }

    @Test
    fun `1,0 plus 2,0 is bound-to-4,0-var`() {
        val t1 = Float(1.0f)
        val t2 = Float(2.0f)
        val t3 = Variable("X")

        val result = plus(t1, t2, t3, mapOf(t3 to Float(4.0f)))

        assertTrue(result.none(), "1.0 + 2.0 should not be equal to X")
    }

    @Test
    fun `1 plus bound-to-2-var is 3`() {
        val t1 = Integer(1)
        val t2 = Variable("X")
        val t3 = Integer(3)

        val result = plus(t1, t2, t3, mapOf(t2 to Integer(2))).toList()

        assertEquals(1, result.size, "1 + X should be equal to 3")
        assertTrue(result[0].isSuccess, "Expected success")
        assertTrue(result[0].getOrNull()!!.isEmpty(), "t2 should not be rebound")
    }

    @Test
    fun `1,0 plus bound-to-2,0-var is 3,0`() {
        val t1 = Float(1.0f)
        val t2 = Variable("X")
        val t3 = Float(3.0f)

        val result = plus(t1, t2, t3, mapOf(t2 to Float(2.0f))).toList()

        assertEquals(1, result.size, "1.0 + X should be equal to 3.0")
        assertTrue(result[0].isSuccess, "Expected success")
        assertTrue(result[0].getOrNull()!!.isEmpty(), "t2 should not be rebound")
    }

    @Test
    fun `1 plus bound-to-2-var is not 4`() {
        val t1 = Integer(1)
        val t2 = Variable("X")
        val t3 = Integer(4)

        val result = plus(t1, t2, t3, mapOf(t2 to Integer(2)))

        assertTrue(result.none(), "1 + X should not be equal to 4")
    }

    @Test
    fun `1,0 plus bound-to-2,0-var is not 4,0`() {
        val t1 = Float(1.0f)
        val t2 = Variable("X")
        val t3 = Float(4.0f)

        val result = plus(t1, t2, t3, mapOf(t2 to Float(2.0f)))

        assertTrue(result.none(), "1.0 + X should not be equal to 4.0")
    }

    @Test
    fun `bound-to-1-var plus 2 is 3`() {
        val t1 = Variable("X")
        val t2 = Integer(2)
        val t3 = Integer(3)

        val result = plus(t1, t2, t3, mapOf(t1 to Integer(1))).toList()

        assertEquals(1, result.size, "X + 2 should be equal to 3")
        assertTrue(result[0].isSuccess, "Expected success")
        assertTrue(result[0].getOrNull()!!.none(), "t1 should not be rebound")
    }

    @Test
    fun `bound-to-1-var plus 2,0 is 3,0`() {
        val t1 = Variable("X")
        val t2 = Float(2.0f)
        val t3 = Float(3.0f)

        val result = plus(t1, t2, t3, mapOf(t1 to Integer(1))).toList()

        assertEquals(1, result.size, "X + 2.0 should be equal to 3.0")
        assertTrue(result[0].isSuccess, "Expected success")
        assertTrue(result[0].getOrNull()!!.none(), "t1 should not be rebound")
    }

    @Test
    fun `bound-to-1-var plus 2 is not 4`() {
        val t1 = Variable("X")
        val t2 = Integer(2)
        val t3 = Integer(4)

        val result = plus(t1, t2, t3, mapOf(t1 to Integer(1)))

        assertTrue(result.none(), "X + 2 should not be equal to 4")
    }

    @Test
    fun `bound-to-1-var plus 2,0 is not 4,0`() {
        val t1 = Variable("X")
        val t2 = Float(2.0f)
        val t3 = Float(4.0f)

        val result = plus(t1, t2, t3, mapOf(t1 to Integer(1)))

        assertTrue(result.none(), "X + 2.0 should not be equal to 4.0")
    }

    @Test
    fun `two unbound vars plus should throw`() {
        val t1 = Variable("X")
        val t2 = Variable("Y")
        val t3 = Integer(3)

        val result = plus(t1, t2, t3, emptyMap()).toList()

        assertEquals(1, result.size, "There should be one solution")
        assertTrue(result[0].isFailure, "Expected failure")
        assertTrue(result[0].exceptionOrNull() is IllegalArgumentException, "Expected IllegalArgumentException")
    }

    @Test
    fun `bound-to-1-var plus bound-to-2-var is variable`() {
        val t1 = Variable("X")
        val t2 = Variable("Y")
        val t3 = Variable("Z")

        val map: Substitutions = mapOf(
            t1 to Integer(1),
            t2 to Integer(2),
        )

        val result = plus(t1, t2, t3, map).toList()

        assertTrue(result.isNotEmpty(), "X + Y should be equal to Z")
        assertTrue(result[0].isSuccess, "Expected success")
        assertTrue(equivalent(result[0].getOrThrow()[t3]!!, Integer(3), result[0].getOrNull()!!), "Z should be equal to 3")
    }

    @Test
    fun `bound-to-1,0-var plus bound-to-2,0-var is variable`() {
        val t1 = Variable("X")
        val t2 = Variable("Y")
        val t3 = Variable("Z")

        val map: Substitutions = mapOf(
            t1 to Float(1.0f),
            t2 to Float(2.0f),
        )

        val result = plus(t1, t2, t3, map).toList()

        assertTrue(result.isNotEmpty(), "X + Y should be equal to Z")
        assertTrue(result[0].isSuccess, "Expected success")
        assertTrue(equivalent(result[0].getOrThrow()[t3]!!, Float(3.0f), result[0].getOrNull()!!), "Z should be equal to 3.0")
    }

    @Test
    fun `int + float is float`() {
        val t1 = Integer(1)
        val t2 = Float(2.0f)
        val t3 = Variable("X")

        val result = plus(t1, t2, t3, emptyMap()).toList()

        assertEquals(1, result.size, "There should be one solution")
        assertTrue(result[0].isSuccess, "Expected success")
        assertTrue(equivalent(result[0].getOrThrow()[t3]!!, Float(3.0f), result[0].getOrNull()!!), "X should be equal to 3.0")
    }

    @Test
    fun `1 times 1 is 1`() {
        val t1 = Integer(1)
        val t2 = Integer(1)
        val t3 = Integer(1)

        val result = mul(t1, t2, t3, emptyMap()).toList()

        assertEquals(1, result.size, "There should be one solution")
        assertTrue(result[0].isSuccess, "Expected success")
        assertTrue(result[0].getOrNull()!!.isEmpty(), "1 * 1 should already be equal to 1")
    }

    @Test
    fun `1,0 times 1,0 is 1,0`() {
        val t1 = Float(1.0f)
        val t2 = Float(1.0f)
        val t3 = Float(1.0f)

        val result = mul(t1, t2, t3, emptyMap()).toList()

        assertEquals(1, result.size, "There should be one solution")
        assertTrue(result[0].isSuccess, "Expected success")
        assertTrue(result[0].getOrNull()!!.isEmpty(), "1.0 * 1.0 should already be equal to 1.0")
    }

    @Test
    fun `1 times 2 is 2`() {
        val t1 = Integer(1)
        val t2 = Integer(2)
        val t3 = Integer(2)

        val result = mul(t1, t2, t3, emptyMap()).toList()

        assertEquals(1, result.size, "There should be one solution")
        assertTrue(result[0].isSuccess, "Expected success")
        assertTrue(result[0].getOrNull()!!.isEmpty(), "1 * 2 should already be equal to 2")
    }

    @Test
    fun `1,0 times 2,0 is 2,0`() {
        val t1 = Float(1.0f)
        val t2 = Float(2.0f)
        val t3 = Float(2.0f)

        val result = mul(t1, t2, t3, emptyMap()).toList()

        assertEquals(1, result.size, "There should be one solution")
        assertTrue(result[0].isSuccess, "Expected success")
        assertTrue(result[0].getOrNull()!!.isEmpty(), "1.0 * 2.0 should already be equal to 2.0")
    }

    @Test
    fun `2 times 3 is 6`() {
        val t1 = Integer(2)
        val t2 = Integer(3)
        val t3 = Integer(6)

        val result = mul(t1, t2, t3, emptyMap()).toList()

        assertEquals(1, result.size, "There should be one solution")
        assertTrue(result[0].isSuccess, "Expected success")
        assertTrue(result[0].getOrNull()!!.isEmpty(), "2 * 3 should already be equal to 6")
    }

    @Test
    fun `2,0 times 3,0 is 6,0`() {
        val t1 = Float(2.0f)
        val t2 = Float(3.0f)
        val t3 = Float(6.0f)

        val result = mul(t1, t2, t3, emptyMap()).toList()

        assertEquals(1, result.size, "There should be one solution")
        assertTrue(result[0].isSuccess, "Expected success")
        assertTrue(result[0].getOrNull()!!.isEmpty(), "2.0 * 3.0 should already be equal to 6.0")
    }

    @Test
    fun `2 times 3 is not 4`() {
        val t1 = Integer(2)
        val t2 = Integer(3)
        val t3 = Integer(4)

        val result = mul(t1, t2, t3, emptyMap())

        assertTrue(result.none(), "2 * 3 should not be equal to 4")
    }

    @Test
    fun `2,0 times 3,0 is not 4,0`() {
        val t1 = Float(2.0f)
        val t2 = Float(3.0f)
        val t3 = Float(4.0f)

        val result = mul(t1, t2, t3, emptyMap())

        assertTrue(result.none(), "2.0 * 3.0 should not be equal to 4.0")
    }

    @Test
    fun `int times float is float`() {
        val t1 = Integer(2)
        val t2 = Float(3.0f)
        val t3 = Variable("X")

        val result = mul(t1, t2, t3, emptyMap()).toList()

        assertEquals(1, result.size, "There should be one solution")
        assertTrue(result[0].isSuccess, "Expected success")
        assertTrue(equivalent(result[0].getOrThrow()[t3]!!, Float(6.0f), result[0].getOrNull()!!), "X should be equal to 6.0")
    }

    @Test
    fun `addition with negative`() {
        val t1 = Integer(1)
        val t2 = Integer(-1)
        val t3 = Integer(0)

        val result = plus(t1, t2, t3, emptyMap()).toList()

        assertEquals(1, result.size, "There should be one solution")
        assertTrue(result[0].isSuccess, "Expected success")
        assertTrue(result[0].getOrNull()!!.isEmpty(), "1 + -1 should already be equal to 0")
    }

    @RepeatedTest(100)
    fun `random test for mul`() {
        val t1 = Integer((-1000..1000).random())
        val t2 = Integer((-1000..1000).random())
        val t3 = Variable("X")

        val result = mul(t1, t2, t3, emptyMap()).toList()

        assertEquals(1, result.size, "There should be one solution")
        assertTrue(result[0].isSuccess, "Expected success")
        assertEquals(Integer(t1.value * t2.value), result[0].getOrNull()!![t3], "X should be equal to ${t1.value * t2.value}")
    }

    @RepeatedTest(100)
    fun `random test for mul with floats`() {
        val t1 = Float((0..1000).random().toFloat())
        val t2 = Float((0..1000).random().toFloat())
        val t3 = Variable("X")

        val result = mul(t1, t2, t3, emptyMap()).toList()

        assertEquals(1, result.size, "There should be one solution")
        assertTrue(result[0].isSuccess, "Expected success")
        val subs = result[0].getOrNull()!!
        assertTrue(equivalent(subs[t3]!!, Float(t1.value * t2.value), subs), "X should be equal to ${t1.value * t2.value}")
    }
}
