package prolog.builtins

import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import prolog.ast.Database.Program
import prolog.ast.arithmetic.Integer
import prolog.ast.lists.List
import prolog.ast.lists.List.Cons
import prolog.ast.lists.List.Empty
import prolog.ast.logic.Fact
import prolog.ast.logic.Rule
import prolog.ast.terms.*
import prolog.logic.equivalent

class AnalysisOperatorsTests {
    @Test
    fun `functor(foo, foo, 0)`() {
        val functor = FunctorOp(Atom("foo"), Atom("foo"), Integer(0))

        val result = functor.satisfy(emptyMap()).toList()

        assertEquals(1, result.size, "Expected 1 result")
        assertTrue(result[0].isSuccess, "Expected success")
        assertTrue(result[0].getOrNull()!!.isEmpty(), "Expected empty substitutions")
    }

    @Test
    fun `functor(foo(X), foo, Y)`() {
        val functor = FunctorOp(
            Structure(Atom("foo"), listOf(Variable("X"))),
            Atom("foo"),
            Variable("Y")
        )

        val result = functor.satisfy(emptyMap()).toList()

        assertEquals(1, result.size, "Expected 1 result")
        assertTrue(result[0].isSuccess, "Expected success")
        val subs = result[0].getOrNull()!!
        assertEquals(1, subs.size, "Expected 1 substitution")
        assertEquals(Integer(1), subs[Variable("Y")])
    }

    @Test
    fun `functor(foo, X, Y)`() {
        val atom = Atom("foo")
        val functor = FunctorOp(atom, Variable("X"), Variable("Y"))

        val result = functor.satisfy(emptyMap()).toList()

        assertEquals(1, result.size, "Expected 1 result")
        assertTrue(result[0].isSuccess, "Expected success")
        val subs = result[0].getOrNull()!!
        assertEquals(2, subs.size, "Expected 2 substitutions")
        assertEquals(atom.functor.name, subs[Variable("X")])
        assertEquals(atom.functor.arity, subs[Variable("Y")])
    }

    @Test
    fun `functor(X, foo, 1)`() {
        val functor = FunctorOp(Variable("X"), Atom("foo"), Integer(1))

        val result = functor.satisfy(emptyMap()).toList()

        assertEquals(1, result.size, "Expected 1 result")
        assertTrue(result[0].isSuccess, "Expected success")
        val subs = result[0].getOrNull()!!
        assertEquals(1, subs.size, "Expected 1 substitution")
        assertInstanceOf(Structure::class.java, subs[Variable("X")])
        val structure = subs[Variable("X")] as Structure
        assertEquals(Atom("foo"), structure.name)
        assertEquals(1, structure.arguments.size)
        assertInstanceOf(AnonymousVariable::class.java, structure.arguments[0])
    }

    @Test
    fun `functor(foo(a), foo, 0)`() {
        val functor = FunctorOp(Structure(Atom("foo"), listOf(Atom("a"))), Atom("foo"), Integer(0))

        val result = functor.satisfy(emptyMap()).toList()

        assertTrue(result.isEmpty(), "Expected no results")
    }

    @Test
    fun `functor(foo(X), foo, 0)`() {
        val functor = FunctorOp(Structure(Atom("foo"), listOf(Variable("X"))), Atom("foo"), Integer(0))

        val result = functor.satisfy(emptyMap()).toList()

        assertTrue(result.isEmpty(), "Expected no results")
    }

    @Test
    fun `functor(X, Y, 1)`() {
        val functor = FunctorOp(Variable("X"), Variable("Y"), Integer(1))

        val exception = assertThrows<Exception> {
            functor.satisfy(emptyMap()).toList()
        }
        assertEquals("Arguments are not sufficiently instantiated", exception.message)
    }

    @Test
    fun `arg without variables`() {
        val arg = Arg(
            Integer(1),
            Structure(Atom("f"), listOf(Atom("a"), Atom("b"), Atom("c"))),
            Atom("a")
        )

        val result = arg.satisfy(emptyMap()).toList()

        assertEquals(1, result.size, "Expected 1 result")
        assertTrue(result[0].isSuccess, "Expected success")
        val subs = result[0].getOrNull()!!
        assertTrue(subs.isEmpty(), "Expected no substitutions")
    }

    @Test
    fun `arg with variable value`() {
        val arg = Arg(
            Integer(1),
            Structure(Atom("f"), listOf(Atom("a"), Atom("b"), Atom("c"))),
            Variable("Term")
        )

        val result = arg.satisfy(emptyMap()).toList()

        assertEquals(1, result.size, "Expected 1 result")
        assertTrue(result[0].isSuccess, "Expected success")
        val subs = result[0].getOrNull()!!
        assertEquals(1, subs.size, "Expected 1 substitution")
        assertEquals(Atom("a"), subs[Variable("Term")])
    }

    @Test
    fun `arg with variable arg`() {
        val arguments = listOf(Atom("a"), Atom("b"), Atom("c"))
        for (i in arguments.indices) {
            val arg = Arg(
                Variable("Arg"),
                Structure(Atom("f"), arguments),
                arguments[i]
            )

            val result = arg.satisfy(emptyMap()).toList()

            assertEquals(1, result.size, "Expected 1 result for arg $i")
            assertTrue(result[0].isSuccess, "Expected success for arg $i")
            val subs = result[0].getOrNull()!!
            assertEquals(1, subs.size, "Expected 1 substitution for arg $i")
            assertEquals(Integer(i + 1), subs[Variable("Arg")], "Expected arg to be $i + 1")
        }
    }

    @Test
    fun `arg with backtracking`() {
        val arg = Arg(
            Variable("Position"),
            Structure(Atom("f"), listOf(Atom("a"), Atom("b"), Atom("c"))),
            Variable("Term")
        )

        val result = arg.satisfy(emptyMap()).toList()

        assertEquals(3, result.size, "Expected 3 results")

        assertTrue(result[0].isSuccess, "Expected success")
        val subs1 = result[0].getOrNull()!!
        assertEquals(2, subs1.size, "Expected 2 substitutions")
        assertEquals(Integer(1), subs1[Variable("Position")])
        assertEquals(Atom("a"), subs1[Variable("Term")])

        assertTrue(result[1].isSuccess, "Expected success")
        val subs2 = result[1].getOrNull()!!
        assertEquals(2, subs2.size, "Expected 2 substitutions")
        assertEquals(Integer(2), subs2[Variable("Position")])
        assertEquals(Atom("b"), subs2[Variable("Term")])

        assertTrue(result[2].isSuccess, "Expected success")
        val subs3 = result[2].getOrNull()!!
        assertEquals(2, subs3.size, "Expected 2 substitutions")
        assertEquals(Integer(3), subs3[Variable("Position")])
        assertEquals(Atom("c"), subs3[Variable("Term")])
    }

    @Test
    fun `arg raises error if Arg is not compound`() {
        val arg = Arg(Integer(1), Atom("foo"), Variable("X"))

        val exception = assertThrows<IllegalArgumentException> {
            arg.satisfy(emptyMap()).toList()
        }
        assertEquals("Type error: `structure' expected, found `foo' (atom)", exception.message)
    }

    @Test
    fun `arg fails silently if arg = 0`() {
        val arg = Arg(
            Integer(0),
            Structure(Atom("f"), listOf(Atom("a"), Atom("b"), Atom("c"))),
            Variable("Term")
        )

        val result = arg.satisfy(emptyMap()).toList()

        assertTrue(result.isEmpty(), "Expected no results")
    }

    @Test
    fun `arg fails silently if arg gt arity`() {
        val arg = Arg(
            Integer(4),
            Structure(Atom("f"), listOf(Atom("a"), Atom("b"), Atom("c"))),
            Variable("Term")
        )

        val result = arg.satisfy(emptyMap()).toList()

        assertTrue(result.isEmpty(), "Expected no results")
    }

    @Test
    fun `arg raises error if arg lt 0`() {
        val arg = Arg(
            Integer(-1),
            Structure(Atom("f"), listOf(Atom("a"), Atom("b"), Atom("c"))),
            Variable("Term")
        )

        val exception = assertThrows<IllegalArgumentException> {
            arg.satisfy(emptyMap()).toList()
        }
        assertEquals("Domain error: not_less_than_zero", exception.message)
    }

    @Nested
    class `Clause operator` {
        @BeforeEach
        fun setup() {
            Program.reset()
        }

        @Test
        fun `clause fact atom without variables`() {
            Program.load(listOf(Fact(Atom("foo"))))

            val result = ClauseOp(
                Atom("foo"),
                True
            ).satisfy(emptyMap()).toList()

            assertEquals(1, result.size, "Expected 1 result")
            assertTrue(result[0].isSuccess, "Expected success")
            val subs = result[0].getOrNull()!!
            assertTrue(subs.isEmpty(), "Expected empty substitutions")
        }

        @Test
        fun `clause fact compound without variables`() {
            Program.load(
                listOf(
                    Fact(CompoundTerm(Atom("foo"), listOf(Atom("a"), Atom("b"))))
                )
            )

            val result = ClauseOp(
                CompoundTerm(Atom("foo"), listOf(Atom("a"), Atom("b"))),
                True
            ).satisfy(emptyMap()).toList()

            assertEquals(1, result.size, "Expected 1 result")
            assertTrue(result[0].isSuccess, "Expected success")
            val subs = result[0].getOrNull()!!
            assertTrue(subs.isEmpty(), "Expected empty substitutions")
        }

        @Test
        fun `clause rule without variables`() {
            Program.load(listOf(Rule(Atom("foo"), Atom("bar"))))

            val result = ClauseOp(
                Atom("foo"),
                Atom("bar")
            ).satisfy(emptyMap()).toList()

            assertEquals(1, result.size, "Expected 1 result")
            assertTrue(result[0].isSuccess, "Expected success")
            val subs = result[0].getOrNull()!!
            assertTrue(subs.isEmpty(), "Expected empty substitutions")
        }

        @Test
        fun `clause fact variable body`() {
            Program.load(listOf(Fact(Atom("foo"))))

            val variable = Variable("Term")
            val result = ClauseOp(
                Atom("foo"),
                variable
            ).satisfy(emptyMap()).toList()

            assertEquals(1, result.size, "Expected 1 result")
            assertTrue(result[0].isSuccess, "Expected success")
            val subs = result[0].getOrNull()!!
            assertEquals(1, subs.size, "Expected 1 substitution")
            assertEquals(True, subs[variable])
        }

        @Test
        fun `clause fact with variable head`() {
            Program.load(
                listOf(
                    Fact(CompoundTerm(Atom("foo"), listOf(Atom("a"))))
                )
            )

            val result = ClauseOp(
                CompoundTerm(Atom("foo"), listOf(Variable("X"))),
                True
            ).satisfy(emptyMap()).toList()

            assertEquals(1, result.size, "Expected 1 result")
            assertTrue(result[0].isSuccess, "Expected success")
            val subs = result[0].getOrNull()!!
            assertEquals(1, subs.size, "Expected 1 substitution")
            assertEquals(Atom("a"), subs[Variable("X")])
        }

        @Test
        fun `clause rule with variable body`() {
            Program.load(listOf(Rule(Atom("foo"), Atom("bar"))))

            val result = ClauseOp(
                Atom("foo"),
                Variable("Term")
            ).satisfy(emptyMap()).toList()

            assertEquals(1, result.size, "Expected 1 result")
            assertTrue(result[0].isSuccess, "Expected success")
            val subs = result[0].getOrNull()!!
            assertEquals(1, subs.size, "Expected 1 substitution")
            assertEquals(Atom("bar"), subs[Variable("Term")])
        }

        @Test
        fun `clause fact variable value with backtracking`() {
            Program.load(
                listOf(
                    Fact(CompoundTerm(Atom("bar"), listOf(Atom("d")))),
                    Fact(CompoundTerm(Atom("bar"), listOf(Atom("e")))),
                    Fact(CompoundTerm(Atom("foo"), listOf(Atom("a")))),
                    Fact(CompoundTerm(Atom("foo"), listOf(Atom("b")))),
                    Fact(CompoundTerm(Atom("foo"), listOf(Atom("c")))),
                    Rule(
                        CompoundTerm(Atom("foo"), listOf(Variable("X"))),
                        CompoundTerm(Atom("bar"), listOf(Variable("X")))
                    )
                )
            )

            val x = Variable("X")
            val term = Variable("Term")

            val result = ClauseOp(
                CompoundTerm(Atom("foo"), listOf(x)),
                term
            ).satisfy(emptyMap()).toList()

            assertEquals(4, result.size, "Expected 4 results")

            assertTrue(result[0].isSuccess, "Expected success")
            val subs1 = result[0].getOrNull()!!
            assertEquals(2, subs1.size, "Expected 2 substitutions")
            assertEquals(Atom("a"), subs1[x], "Expected a")
            assertEquals(True, subs1[term], "Expected True")

            assertTrue(result[1].isSuccess, "Expected success")
            val subs2 = result[1].getOrNull()!!
            assertEquals(2, subs2.size, "Expected 2 substitutions")
            assertEquals(Atom("b"), subs2[x], "Expected b")
            assertEquals(True, subs2[term], "Expected True")

            assertTrue(result[2].isSuccess, "Expected success")
            val subs3 = result[2].getOrNull()!!
            assertEquals(2, subs3.size, "Expected 2 substitutions")
            assertEquals(Atom("c"), subs3[x], "Expected c")
            assertEquals(True, subs3[term], "Expected True")

            assertTrue(result[3].isSuccess, "Expected success")
            val subs4 = result[3].getOrNull()!!
            assertEquals(1, subs4.size, "Expected 2 substitutions")
            assertEquals(
                CompoundTerm(Atom("bar"), listOf(Variable("X"))),
                subs4[term],
                "Expected bar(X)"
            )
        }
    }

    @Nested
    class `Univ operator` {
        @Test
        fun `univ without variables`() {
            val univ = Univ(Atom("foo"), Cons(Atom("foo"), Empty))

            val result = univ.satisfy(emptyMap()).toList()

            assertEquals(1, result.size, "Expected 1 result")
            assertTrue(result[0].isSuccess, "Expected success")
            val subs = result[0].getOrNull()!!
            assertTrue(subs.isEmpty(), "Expected empty substitutions")
        }

        @Test
        fun `univ with variable list`() {
            val list = Variable("List")
            val univ = Univ(
                CompoundTerm(Atom("foo"), listOf(Atom("a"), Atom("b"))),
                list
            )
            val expected = Cons(Atom("foo"), Cons(Atom("a"), Cons(Atom("b"), Empty)))

            val result = univ.satisfy(emptyMap()).toList()

            assertEquals(1, result.size, "Expected 1 result")
            assertTrue(result[0].isSuccess, "Expected success")
            val subs = result[0].getOrNull()!!
            assertEquals(1, subs.size, "Expected 1 substitution")
            assertEquals(expected, subs[list], "Expected List to be $expected")
        }

        @Test
        fun `univ with variable in compound`() {
            val list = Variable("List")
            val univ = Univ(
                CompoundTerm(Atom("foo"), listOf(Variable("X"))),
                list
            )
            val expected = Cons(Atom("foo"), Cons(Variable("X"), Empty))

            val result = univ.satisfy(emptyMap()).toList()

            assertEquals(1, result.size, "Expected 1 result")
            assertTrue(result[0].isSuccess, "Expected success")
            val subs = result[0].getOrNull()!!
            assertEquals(1, subs.size, "Expected 1 substitution")
            assertEquals(expected, subs[list], "Expected List to be $expected")
        }

        @Test
        fun `univ with var foo`() {
            val univ = Univ(
                Variable("Term"),
                Cons(Atom("foo"), Empty)
            )

            val result = univ.satisfy(emptyMap()).toList()

            assertEquals(1, result.size, "Expected 1 result")
            assertTrue(result[0].isSuccess, "Expected success")
            val subs = result[0].getOrNull()!!
            assertEquals(1, subs.size, "Expected 1 substitution")
            assertEquals(Atom("foo"), subs[Variable("Term")], "Expected Term to be foo")
        }

        @Test
        fun `univ with var foo(a)`() {
            val univ = Univ(
                Variable("Term"),
                Cons(Atom("foo"), Cons(Atom("a"), Empty))
            )

            val result = univ.satisfy(emptyMap()).toList()

            assertEquals(1, result.size, "Expected 1 result")
            assertTrue(result[0].isSuccess, "Expected success")
            val subs = result[0].getOrNull()!!
            assertEquals(1, subs.size, "Expected 1 substitution")
            assertEquals(
                Structure(Atom("foo"), listOf(Atom("a"))),
                subs[Variable("Term")],
                "Expected Term to be foo(a)"
            )
        }

        @Test
        fun `univ with var foo(X)`() {
            val univ = Univ(
                Variable("Term"),
                Cons(Atom("foo"), Cons(Variable("X"), Empty))
            )

            val result = univ.satisfy(emptyMap()).toList()

            assertEquals(1, result.size, "Expected 1 result")
            assertTrue(result[0].isSuccess, "Expected success")
            val subs = result[0].getOrNull()!!
            assertEquals(1, subs.size, "Expected 1 substitution")
            assertEquals(
                Structure(Atom("foo"), listOf(Variable("X"))),
                subs[Variable("Term")],
                "Expected Term to be foo(X)"
            )
        }

        @Test
        fun `univ with var foo(a, b(X), c)`() {
            val univ = Univ(
                Variable("Term"),
                Cons(
                    Atom("foo"),
                    Cons(Atom("a"), Cons(CompoundTerm(Atom("b"), listOf(Variable("X"))), Cons(Atom("c"), Empty)))
                )
            )
            val expected = CompoundTerm(
                Atom("foo"),
                listOf(Atom("a"), CompoundTerm(Atom("b"), listOf(Variable("X"))), Atom("c"))
            )

            val result = univ.satisfy(emptyMap()).toList()

            assertEquals(1, result.size, "Expected 1 result")
            assertTrue(result[0].isSuccess, "Expected success")
            val subs = result[0].getOrNull()!!
            assertEquals(1, subs.size, "Expected 1 substitution")
            assertEquals(expected, subs[Variable("Term")], "Expected Term to be $expected")
        }

        @Test
        fun `univ with unified var should return substitution`() {
            val univ = Univ(
                CompoundTerm(
                    Atom("foo"), listOf(
                        Atom("a"),
                        CompoundTerm(Atom("bar"), listOf(Variable("X"))),
                        Atom("c")
                    )
                ),
                Cons(
                    Atom("foo"),
                    Cons(Atom("a"), Cons(CompoundTerm(Atom("bar"), listOf(Atom("b"))), Cons(Atom("c"), Empty)))
                )
            )

            val result = univ.satisfy(emptyMap()).toList()

            assertEquals(1, result.size, "Expected 1 result")
            assertTrue(result[0].isSuccess, "Expected success")
            val subs = result[0].getOrNull()!!
            assertEquals(1, subs.size, "Expected 1 substitution")
            assertEquals(Atom("b"), subs[Variable("X")], "Expected X to be b")
        }

        @Test
        fun `univ of incompatible structures should fail`() {
            val univ = Univ(
                CompoundTerm(Atom("foo"), listOf(Atom("a"))),
                Cons(Atom("bar"), Empty)
            )

            val result = univ.satisfy(emptyMap()).toList()

            assertEquals(0, result.size, "Expected 0 results")
        }

        @Test
        fun `univ with two vars should throw`() {
            val univ = Univ(Variable("X"), Variable("Y"))

            val exception = assertThrows<Exception> {
                univ.satisfy(emptyMap()).toList()
            }
            assertEquals("Arguments are not sufficiently instantiated", exception.message)
        }

        class OpenUniv(term: Term, list: Term) : Univ(term, list) {
            public override fun listToTerm(list: List): Term = super.listToTerm(list)
            public override fun termToList(term: Term): List = super.termToList(term)
        }

        @Test
        fun `a to term`() {
            val univ = OpenUniv(Atom(""), Empty)

            val originalList = Cons(Atom("a"), Empty)
            val originalTerm = Atom("a")

            val term = univ.listToTerm(originalList)
            assertEquals(originalTerm, term, "Expected term to be a")

            val list = univ.termToList(term)
            assertEquals(originalList, list, "Expected list to be [a]")
        }

        @Test
        fun `foo, bar to compound`() {
            val univ = OpenUniv(Atom(""), Empty)

            val originalList = Cons(Atom("foo"), Cons(Atom("bar"), Empty))
            val originalTerm = CompoundTerm(Atom("foo"), listOf(Atom("bar")))

            val term = univ.listToTerm(originalList)
            assertEquals(originalTerm, term)

            val list = univ.termToList(term)
            assertEquals(originalList, list)
        }

        @Test
        fun `foo, bar, baz to compound`() {
            val univ = OpenUniv(Atom(""), Empty)

            val originalList = Cons(Atom("foo"), Cons(Atom("bar"), Cons(Atom("baz"), Empty)))
            val originalTerm = CompoundTerm(Atom("foo"), listOf(Atom("bar"), Atom("baz")))

            val term = univ.listToTerm(originalList)
            assertEquals(originalTerm, term)

            val list = univ.termToList(term)
            assertEquals(originalList, list)
        }

        @Test
        fun `foo, bar, (baz, bar) to compound with list`() {
            val univ = OpenUniv(Atom(""), Empty)

            val originalList =
                Cons(Atom("foo"), Cons(Atom("bar"), Cons(Cons(Atom("baz"), Cons(Atom("bar"), Empty)), Empty)))
            val originalTerm = CompoundTerm(
                Atom("foo"),
                listOf(Atom("bar"), Cons(Atom("baz"), Cons(Atom("bar"), Empty)))
            )

            val term = univ.listToTerm(originalList)
            assertTrue(equivalent(originalTerm, term, emptyMap()), "Expected term to be foo(bar, [baz, bar]))")

            val list = univ.termToList(term)
            assertTrue(equivalent(originalList, list, emptyMap()), "Expected list to be [foo, bar, [baz, bar]]")
        }
    }
}
