package interpreter

import com.github.h0tk3y.betterParse.grammar.parseToEnd
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import parser.grammars.TermsGrammar
import prolog.ast.arithmetic.Integer
import prolog.ast.logic.Rule
import prolog.ast.terms.*
import prolog.builtins.*

class PreprocessorTests {
    val preprocessor = OpenPreprocessor()

    companion object {
        val preprocessor = OpenPreprocessor()

        fun test(tests: Map<Term, Term>) {
            for ((input, expected) in tests) {
                val result = preprocessor.preprocess(input)
                assertEquals(expected, result, "Expected preprocessed")
                assertEquals(expected::class, result::class, "Expected same class")
            }
        }
    }

    @Test
    fun `can preprocess anonymous variable`() {
        val input = Variable("_")

        val result = preprocessor.preprocess(input)

        assertInstanceOf(AnonymousVariable::class.java, result, "Expected anonymous variable")
        assertTrue((result as Variable).name.matches("_\\d+".toRegex()), "Expected anonymous variable name")
    }

    @Test
    fun `multiple anonymous variables should be unique`() {
        val input = CompoundTerm("foo", Variable("_"), Variable("_"))

        val result = preprocessor.preprocess(input)

        assertInstanceOf(CompoundTerm::class.java, result, "Expected compound term")
        assertEquals(2, (result as CompoundTerm).arguments.size, "Expected two terms")
        for (argument in result.arguments) {
            assertTrue(
                (argument as Variable).name.matches("_\\d+".toRegex()),
                "Expected anonymous variable name, but got ${argument.name}"
            )
        }
        val first = result.arguments[0] as Variable
        val second = result.arguments[1] as Variable
        assertNotEquals(first.name, second.name, "Expected different anonymous variable names")
    }

    @Test
    fun `can preprocess nested anonymous variables`() {
        val input = TermsGrammar().parseToEnd("name(character(Name, _, _, _))") as Term

        val result = preprocessor.preprocess(input)

        assertInstanceOf(CompoundTerm::class.java, result, "Expected compound term")
        assertEquals(1, (result as CompoundTerm).arguments.size, "Expected one term")
        assertInstanceOf(CompoundTerm::class.java, result.arguments[0], "Expected compound term")
        val inner = result.arguments[0] as CompoundTerm
        assertEquals(4, inner.arguments.size, "Expected four terms")
        for (argument in inner.arguments) {
            if ((argument as Variable).name != "Name") {
                assertTrue(
                    argument.name.matches("_\\d+".toRegex()),
                    "Expected anonymous variable name, but got ${argument.name}"
                )
            }
        }

    }

    @Nested
    class `Arithmetic operators` {
        @Test
        fun `evaluates to different`() {
            test(
                mapOf(
                    Atom("=\\=") to Atom("=\\="),
                    CompoundTerm("=\\=") to CompoundTerm("=\\="),
                    Atom("EvaluatesToDifferent") to Atom("EvaluatesToDifferent"),
                    CompoundTerm("EvaluatesToDifferent") to CompoundTerm("EvaluatesToDifferent"),
                    CompoundTerm("=\\=", Atom("a")) to CompoundTerm("=\\=", Atom("a")),
                    CompoundTerm("=\\=", Integer(1)) to CompoundTerm("=\\=", Integer(1)),
                    CompoundTerm("=\\=", Atom("=\\=")) to CompoundTerm("=\\=", Atom("=\\=")),
                    CompoundTerm("=\\=", Integer(1), Integer(2)) to EvaluatesToDifferent(Integer(1), Integer(2))
                )
            )
        }

        @Test
        fun `evaluates to`() {
            test(
                mapOf(
                    Atom("=:=") to Atom("=:="),
                    CompoundTerm("=:=") to CompoundTerm("=:="),
                    Atom("EvaluatesTo") to Atom("EvaluatesTo"),
                    CompoundTerm(Atom("EvaluatesTo"), emptyList()) to CompoundTerm(
                        Atom("EvaluatesTo"),
                        emptyList()
                    ),
                    CompoundTerm(Atom("=:="), listOf(Atom("a"))) to CompoundTerm(
                        Atom("=:="),
                        listOf(Atom("a"))
                    ),
                    CompoundTerm(Atom("=:="), listOf(Atom("=:="))) to CompoundTerm(
                        Atom("=:="),
                        listOf(Atom("=:="))
                    ),
                    CompoundTerm(Atom("=:="), listOf(Integer(1), Integer(2))) to EvaluatesTo(
                        Integer(1), Integer(2)
                    )
                )
            )
        }

        @Test
        fun `is`() {
            test(
                mapOf(
                    Atom("is") to Atom("is"),
                    CompoundTerm(Atom("is"), emptyList()) to CompoundTerm(Atom("is"), emptyList()),
                    Atom("Is") to Atom("Is"),
                    CompoundTerm(Atom("Is"), emptyList()) to CompoundTerm(Atom("Is"), emptyList()),
                    CompoundTerm(Atom("is"), listOf(Atom("a"))) to CompoundTerm(
                        Atom("is"),
                        listOf(Atom("a"))
                    ),
                    CompoundTerm(Atom("is"), listOf(Integer(1))) to CompoundTerm(
                        Atom("is"),
                        listOf(Integer(1))
                    ),
                    CompoundTerm(Atom("is"), listOf(Atom("is"))) to CompoundTerm(
                        Atom("is"),
                        listOf(Atom("is"))
                    ),
                    CompoundTerm(Atom("is"), listOf(Integer(1), Integer(2))) to Is(
                        Integer(1), Integer(2)
                    )
                )
            )
        }

        @Test
        fun `negate and subtract`() {
            test(
                mapOf(
                    Atom("-") to Atom("-"),
                    CompoundTerm(Atom("-"), emptyList()) to CompoundTerm(Atom("-"), emptyList()),
                    Atom("Negate") to Atom("Negate"),
                    CompoundTerm(Atom("Negate"), emptyList()) to CompoundTerm(
                        Atom("Negate"),
                        emptyList()
                    ),
                    CompoundTerm(Atom("-"), listOf(Atom("a"))) to CompoundTerm(
                        Atom("-"),
                        listOf(Atom("a"))
                    ),
                    CompoundTerm(Atom("-"), listOf(Integer(1))) to Negate(Integer(1)),
                    CompoundTerm(Atom("-"), listOf(Atom("-"))) to CompoundTerm(
                        Atom("-"),
                        listOf(Atom("-"))
                    ),
                    CompoundTerm(Atom("-"), listOf(Integer(1), Integer(2))) to Subtract(
                        Integer(1), Integer(2)
                    ),
                    CompoundTerm(Atom("-"), listOf(Atom("1"), Atom("2"))) to CompoundTerm(
                        Atom("-"),
                        listOf(Atom("1"), Atom("2"))
                    ),
                    CompoundTerm(Atom("-"), listOf(Integer(1), Integer(2), Integer(3))) to CompoundTerm(
                        Atom("-"),
                        listOf(Integer(1), Integer(2), Integer(3))
                    )
                )
            )
        }

        @Test
        fun `positive and add`() {
            test(
                mapOf(
                    Atom("+") to Atom("+"),
                    CompoundTerm(Atom("+"), emptyList()) to CompoundTerm(Atom("+"), emptyList()),
                    Atom("Positive") to Atom("Positive"),
                    CompoundTerm(Atom("Positive"), emptyList()) to CompoundTerm(
                        Atom("Positive"),
                        emptyList()
                    ),
                    CompoundTerm(Atom("+"), listOf(Atom("a"))) to CompoundTerm(
                        Atom("+"),
                        listOf(Atom("a"))
                    ),
                    CompoundTerm(Atom("+"), listOf(Integer(1))) to Positive(Integer(1)),
                    CompoundTerm(Atom("+"), listOf(Atom("+"))) to CompoundTerm(
                        Atom("+"),
                        listOf(Atom("+"))
                    ),
                    CompoundTerm(Atom("+"), listOf(Integer(1), Integer(2))) to Add(
                        Integer(1), Integer(2)
                    ),
                    CompoundTerm(Atom("+"), listOf(Atom("1"), Atom("2"))) to CompoundTerm(
                        Atom("+"),
                        listOf(Atom("1"), Atom("2"))
                    ),
                    CompoundTerm(Atom("+"), listOf(Integer(1), Integer(2), Integer(3))) to CompoundTerm(
                        Atom("+"),
                        listOf(Integer(1), Integer(2), Integer(3))
                    )
                )
            )
        }

        @Test
        fun multiply() {
            test(
                mapOf(
                    Atom("*") to Atom("*"),
                    CompoundTerm(Atom("*"), emptyList()) to CompoundTerm(Atom("*"), emptyList()),
                    Atom("Multiply") to Atom("Multiply"),
                    CompoundTerm(Atom("Multiply"), emptyList()) to CompoundTerm(
                        Atom("Multiply"),
                        emptyList()
                    ),
                    CompoundTerm(Atom("*"), listOf(Atom("a"))) to CompoundTerm(
                        Atom("*"),
                        listOf(Atom("a"))
                    ),
                    CompoundTerm(Atom("*"), listOf(Integer(1))) to CompoundTerm(Atom("*"), listOf(Integer(1))),
                    CompoundTerm(Atom("*"), listOf(Atom("*"))) to CompoundTerm(
                        Atom("*"),
                        listOf(Atom("*"))
                    ),
                    CompoundTerm(Atom("*"), listOf(Integer(1), Integer(2))) to Multiply(
                        Integer(1), Integer(2)
                    ),
                    CompoundTerm(Atom("*"), listOf(Atom("1"), Atom("2"))) to CompoundTerm(
                        Atom("*"),
                        listOf(Atom("1"), Atom("2"))
                    ),
                    CompoundTerm(Atom("*"), listOf(Integer(1), Integer(2), Integer(3))) to CompoundTerm(
                        Atom("*"),
                        listOf(Integer(1), Integer(2), Integer(3))
                    )
                )
            )
        }

        @Test
        fun divide() {
            test(
                mapOf(
                    Atom("/") to Atom("/"),
                    CompoundTerm(Atom("/"), emptyList()) to CompoundTerm(Atom("/"), emptyList()),
                    Atom("Divide") to Atom("Divide"),
                    CompoundTerm(Atom("Divide"), emptyList()) to CompoundTerm(
                        Atom("Divide"),
                        emptyList()
                    ),
                    CompoundTerm(Atom("/"), listOf(Atom("a"))) to CompoundTerm(
                        Atom("/"),
                        listOf(Atom("a"))
                    ),
                    CompoundTerm(Atom("/"), listOf(Integer(1))) to CompoundTerm(Atom("/"), listOf(Integer(1))),
                    CompoundTerm(Atom("/"), listOf(Atom("/"))) to CompoundTerm(
                        Atom("/"),
                        listOf(Atom("/"))
                    ),
                    CompoundTerm(Atom("/"), listOf(Integer(1), Integer(2))) to Divide(
                        Integer(1), Integer(2)
                    ),
                    CompoundTerm(Atom("/"), listOf(Atom("1"), Atom("2"))) to CompoundTerm(
                        Atom("/"),
                        listOf(Atom("1"), Atom("2"))
                    ),
                    CompoundTerm(Atom("/"), listOf(Integer(1), Integer(2), Integer(3))) to CompoundTerm(
                        Atom("/"),
                        listOf(Integer(1), Integer(2), Integer(3))
                    )
                )
            )
        }

        @Test
        fun between() {
            test(
                mapOf(
                    Atom("between") to Atom("between"),
                    CompoundTerm(Atom("between"), emptyList()) to CompoundTerm(
                        Atom("between"),
                        emptyList()
                    ),
                    Atom("Between") to Atom("Between"),
                    CompoundTerm(Atom("Between"), emptyList()) to CompoundTerm(
                        Atom("Between"),
                        emptyList()
                    ),
                    CompoundTerm(Atom("between"), listOf(Atom("a"))) to CompoundTerm(
                        Atom("between"),
                        listOf(Atom("a"))
                    ),
                    CompoundTerm(Atom("between"), listOf(Integer(1))) to CompoundTerm(
                        Atom("between"),
                        listOf(Integer(1))
                    ),
                    CompoundTerm(Atom("between"), listOf(Atom("between"))) to CompoundTerm(
                        Atom("between"),
                        listOf(Atom("between"))
                    ),
                    CompoundTerm(Atom("between"), listOf(Integer(1), Integer(2))) to CompoundTerm(
                        Atom("between"),
                        listOf(Integer(1), Integer(2))
                    ),
                    CompoundTerm(Atom("between"), listOf(Integer(1), Integer(2), Integer(3))) to Between(
                        Integer(1), Integer(2), Integer(3)
                    ),
                )
            )
        }

        @Test
        fun `fun combinations`() {
            /*
             * [X - 1] is [(1 + 2) * ((12 / 3) - 0)]
             * should return
             * X = 13
             */
            val sum_ = CompoundTerm(Atom("+"), listOf(Integer(1), Integer(2)))
            val sum = Add(Integer(1), Integer(2))

            val div_ = CompoundTerm(Atom("/"), listOf(Integer(12), Integer(3)))
            val div = Divide(Integer(12), Integer(3))

            val sub_ = CompoundTerm(Atom("-"), listOf(div_, Integer(0)))
            val sub = Subtract(div, Integer(0))

            val right_ = CompoundTerm(Atom("*"), listOf(sum_, sub_))
            val right = Multiply(sum, sub)

            val left_ = CompoundTerm(Atom("-"), listOf(Variable("X"), Integer(1)))
            val left = Subtract(Variable("X"), Integer(1))

            val expr_ = CompoundTerm(Atom("is"), listOf(left_, right_))
            val expr = Is(left, right)

            val result = OpenPreprocessor().preprocess(expr_)

            assertEquals(expr, result)
            assertEquals(Is::class, result::class)
            val `is` = result as Is

            assertEquals(left, `is`.number)
            assertEquals(Subtract::class, `is`.number::class)

            assertEquals(right, `is`.expr)
            assertEquals(Multiply::class, `is`.expr::class)
            val multiply = `is`.expr as Multiply

            assertEquals(sum, multiply.expr1)
            assertEquals(Add::class, multiply.expr1::class)
        }
    }

    @Nested
    class `Control operators` {
        private var preprocessor = OpenPreprocessor()

        @Test
        fun fail() {
            test(
                mapOf(
                    Atom("fail") to Fail,
                    CompoundTerm(Atom("fail"), emptyList()) to Fail,
                    Atom("Fail") to Atom("Fail"),
                    CompoundTerm(Atom("Fail"), emptyList()) to CompoundTerm(Atom("Fail"), emptyList()),
                    CompoundTerm(Atom("fail"), listOf(Atom("a"))) to CompoundTerm(Atom("fail"), listOf(Atom("a"))),
                    CompoundTerm(Atom("fail"), listOf(Atom("fail"))) to CompoundTerm(Atom("fail"), listOf(Fail))
                )
            )
        }

        @Test
        fun `true`() {
            test(
                mapOf(
                    Atom("true") to True,
                    CompoundTerm(Atom("true"), emptyList()) to True,
                    Atom("True") to Atom("True"),
                    CompoundTerm(Atom("True"), emptyList()) to CompoundTerm(Atom("True"), emptyList()),
                    CompoundTerm(Atom("true"), listOf(Atom("a"))) to CompoundTerm(Atom("true"), listOf(Atom("a"))),
                    CompoundTerm(Atom("true"), listOf(Atom("true"))) to CompoundTerm(Atom("true"), listOf(True))
                )
            )
        }

        @Test
        fun cut() {
            test(
                mapOf(
                    Atom("!") to Cut(),
                    CompoundTerm(Atom("!"), emptyList()) to Cut(),
                    CompoundTerm(Atom("!"), listOf(Atom("a"))) to CompoundTerm(Atom("!"), listOf(Atom("a"))),
                    CompoundTerm(Atom("!"), listOf(Atom("!"))) to CompoundTerm(Atom("!"), listOf(Cut()))
                )
            )
        }

        @Test
        fun conjunction() {
            test(
                mapOf(
                    CompoundTerm(Atom(","), listOf(Atom("a"), Atom("b"))) to Conjunction(Atom("a"), Atom("b")),
                    CompoundTerm(Atom(","), listOf(Atom("a"), Atom("b"), Atom("c"))) to CompoundTerm(
                        Atom(","),
                        listOf(Atom("a"), Atom("b"), Atom("c"))
                    ),
                    // Nested conjunctions
                    CompoundTerm(
                        Atom(","),
                        listOf(Atom("a"), CompoundTerm(Atom(","), listOf(Atom("b"), Atom("c"))))
                    ) to Conjunction(Atom("a"), Conjunction(Atom("b"), Atom("c"))),
                )
            )
        }

        @Test
        fun disjunction() {
            test(
                mapOf(
                    CompoundTerm(Atom(";"), listOf(Atom("a"), Atom("b"))) to Disjunction(Atom("a"), Atom("b")),
                    CompoundTerm(Atom(";"), listOf(Atom("a"), Atom("b"), Atom("c"))) to CompoundTerm(
                        Atom(";"),
                        listOf(Atom("a"), Atom("b"), Atom("c"))
                    ),
                    // Nested disjunctions
                    CompoundTerm(
                        Atom(";"),
                        listOf(Atom("a"), CompoundTerm(Atom(";"), listOf(Atom("b"), Atom("c"))))
                    ) to Disjunction(Atom("a"), Disjunction(Atom("b"), Atom("c"))),
                )
            )
        }

        @Test
        fun not() {
            test(
                mapOf(
                    CompoundTerm(Atom("\\+"), listOf(Atom("a"))) to Not(Atom("a")),
                    CompoundTerm(Atom("\\+"), listOf(Atom("a"), Atom("b"))) to CompoundTerm(
                        Atom("\\+"),
                        listOf(Atom("a"), Atom("b"))
                    ),
                    // Nested not
                    CompoundTerm(
                        Atom("foo"),
                        listOf(
                            Atom("bar"),
                            CompoundTerm(Atom("\\+"), listOf(CompoundTerm(Atom("\\+"), listOf(Atom("baz")))))
                        )
                    ) to CompoundTerm(Atom("foo"), listOf(Atom("bar"), Not(Not(Atom("baz"))))),
                )
            )
        }

        @Test
        fun `is`() {
            test(
                mapOf(
                    CompoundTerm(Atom("is"), listOf(Variable("T"), Integer(1))) to Is(Variable("T"), Integer(1)),
                    CompoundTerm(Atom("is"), listOf(Variable("T"), Add(Variable("HP"), Integer(5)))) to Is(
                        Variable("T"),
                        Add(Variable("HP"), Integer(5))
                    ),
                    CompoundTerm(Atom("is"), listOf(Variable("T"), Subtract(Variable("HP"), Integer(5)))) to Is(
                        Variable("T"),
                        Subtract(Variable("HP"), Integer(5))
                    ),
                )
            )
        }
    }

    @Nested
    class `Database operators` {
        private val preprocessor = OpenPreprocessor()

        @Test
        fun `assert(fact)`() {
            val input = Structure(
                Atom("assert"), listOf(
                    Structure(
                        Atom(":-"), listOf(
                            Atom("a"),
                            Atom("b")
                        )
                    )
                )
            )
            val expected = Assert(
                Rule(
                    Atom("a"),
                    Atom("b")
                )
            )

            val result = preprocessor.preprocess(input)

            assertEquals(expected, result)
        }

        @Test
        fun `asserta(fact)`() {
            val input = Structure(
                Atom("asserta"), listOf(
                    Structure(
                        Atom(":-"), listOf(
                            Atom("a"),
                            Atom("b")
                        )
                    )
                )
            )
            val expected = AssertA(
                Rule(
                    Atom("a"),
                    Atom("b")
                )
            )

            val result = preprocessor.preprocess(input)

            assertEquals(expected, result)
        }

        @Test
        fun `assertz(fact)`() {
            val input = Structure(
                Atom("assertz"), listOf(
                    Structure(
                        Atom(":-"), listOf(
                            Atom("a"),
                            Atom("b")
                        )
                    )
                )
            )
            val expected = AssertZ(
                Rule(
                    Atom("a"),
                    Atom("b")
                )
            )

            val result = preprocessor.preprocess(input)

            assertEquals(expected, result)
        }

        @Test
        fun `retract(atom)`() {
            val input = Structure(
                Atom("retract"), listOf(
                    Atom("a")
                )
            )
            val expected = Retract(Atom("a"))

            val result = preprocessor.preprocess(input)

            assertEquals(expected, result)
        }

        @Test
        fun `retract(compund with variable)`() {
            val input = Structure(
                Atom("retract"), listOf(
                    CompoundTerm(Atom("a"), listOf(Variable("X")))
                )
            )
            val expected = Retract(CompoundTerm(Atom("a"), listOf(Variable("X"))))

            val result = preprocessor.preprocess(input)

            assertEquals(expected, result)
        }

        @Test
        fun `dynamic declaration`() {
            val input = Structure(
                Atom("dynamic"), listOf(
                    Atom("declaration/1")
                )
            )
            val expected = Dynamic(Functor.of("declaration/1"))

            val result = preprocessor.preprocess(input)

            assertEquals(expected, result)
        }
    }
}
