package prolog.builtins

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ValueSource
import prolog.ast.Database.Program
import prolog.ast.logic.Clause
import prolog.ast.logic.Fact
import prolog.ast.logic.Predicate
import prolog.ast.logic.Rule
import prolog.ast.terms.Atom
import prolog.ast.terms.Functor
import prolog.ast.terms.Structure
import prolog.ast.terms.Variable

class DatabaseOperatorsTests {
    @BeforeEach
    fun setup() {
        Program.reset()
    }

    abstract class AssertTestsBase<T : Structure> {
        protected abstract fun createAssert(clause: Clause): Structure

        @BeforeEach
        fun setup() {
            Program.reset()
        }

        @ParameterizedTest
        @ValueSource(classes = [AssertA::class, AssertZ::class, Assert::class])
        fun `assert(fact atom)`(assertKind: Class<*>) {
            val fact = Fact(Atom("a"))
            createAssert(fact).satisfy(emptyMap())

            assertEquals(1, Program.db.predicates.size, "Expected 1 predicate")
            assertEquals(fact, Program.db.predicates[Functor.of("a/_")]!!.clauses[0])
        }

        @Test
        fun `assert(fact structure)`() {
            val fact = Fact(Structure(Atom("a"), listOf(Atom("b"))))
            createAssert(fact).satisfy(emptyMap())

            assertEquals(1, Program.db.predicates.size, "Expected 1 predicate")
            assertEquals(fact, Program.db.predicates[Functor.of("a/1")]!!.clauses[0])
        }

        @Test
        fun `assert(rule)`() {
            val rule = Rule(
                Structure(Atom("a"), listOf(Atom("b"))),
                Atom("c")
            )
            createAssert(rule).satisfy(emptyMap())

            assertEquals(1, Program.db.predicates.size, "Expected 1 predicate")
            assertEquals(rule, Program.db.predicates[Functor.of("a/1")]!!.clauses[0])
        }
    }

    @Nested
    class AssertTests : AssertTestsBase<Assert>() {
        override fun createAssert(clause: Clause): Structure {
            return Assert(clause)
        }
    }

    @Nested
    class AssertATests : AssertTestsBase<AssertA>() {
        override fun createAssert(clause: Clause): Structure {
            return AssertA(clause)
        }

        @Test
        fun `asserta adds to the beginning`() {
            val rule1 = Rule(
                Structure(Atom("a"), listOf(Atom("b"))),
                Atom("c")
            )
            val rule2 = Rule(
                Structure(Atom("a"), listOf(Atom("d"))),
                Atom("e")
            )
            AssertA(rule1).satisfy(emptyMap())
            AssertA(rule2).satisfy(emptyMap())

            assertEquals(1, Program.db.predicates.size, "Expected 1 predicate")
            assertEquals(rule2, Program.db.predicates[Functor.of("a/1")]!!.clauses[0])
            assertEquals(rule1, Program.db.predicates[Functor.of("a/1")]!!.clauses[1])
        }
    }

    @Nested
    class AssertZTests : AssertTestsBase<AssertZ>() {
        override fun createAssert(clause: Clause): Structure {
            return AssertZ(clause)
        }

        @Test
        fun `assertz adds to the end`() {
            val rule1 = Rule(
                Structure(Atom("a"), listOf(Atom("b"))),
                Atom("c")
            )
            val rule2 = Rule(
                Structure(Atom("a"), listOf(Atom("d"))),
                Atom("e")
            )
            AssertZ(rule1).satisfy(emptyMap())
            AssertZ(rule2).satisfy(emptyMap())

            assertEquals(1, Program.db.predicates.size, "Expected 1 predicate")
            assertEquals(rule1, Program.db.predicates[Functor.of("a/1")]!!.clauses[0])
            assertEquals(rule2, Program.db.predicates[Functor.of("a/1")]!!.clauses[1])
        }
    }

    @Test
    fun `retract fails silently for unknown predicates`() {
        val retract = Retract(Atom("unknown"))
        val result = retract.satisfy(emptyMap())

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

    @Test
    fun `simple retract`() {
        val predicate = Predicate(listOf(Fact(Atom("a"))), dynamic = true)
        Program.db.load(predicate)

        assertEquals(1, Program.query(Atom("a")).count())

        val retract = Retract(Atom("a"))

        assertEquals(1, retract.satisfy(emptyMap()).toList().size, "Expected 1 result")
        assertEquals(0, predicate.clauses.size, "Expected 0 clauses")

        assertTrue(retract.satisfy(emptyMap()).none())
    }

    @Test
    fun `retract atom`() {
        val predicate = Predicate(
            listOf(
                Fact(Atom("a")),
                Fact(Atom("a")),
                Fact(Atom("a"))
            ), dynamic = true
        )
        Program.db.load(predicate)

        val control = Program.query(Atom("a")).toList()

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

        val retract = Retract(Atom("a"))

        val result = retract.satisfy(emptyMap()).iterator()

        assertEquals(3, predicate.clauses.size, "Expected 3 clauses")

        assertTrue(result.hasNext(), "Expected more results")

        val answer = result.next()

        assertTrue(answer.isSuccess, "Expected success")
        assertTrue(answer.getOrNull()!!.isEmpty(), "Expected no substitutions")

        assertEquals(2, predicate.clauses.size, "Expected 2 clauses")

        assertTrue(result.next().isSuccess)
        assertTrue(result.next().isSuccess)

        assertFalse(result.hasNext(), "Expected more results")
        assertEquals(0, predicate.clauses.size, "Expected no remaining clauses")
    }

    @Test
    fun `retract compound with variable`() {
        val predicate = Predicate(
            listOf(
                Fact(Structure(Atom("a"), listOf(Atom("b")))),
                Fact(Structure(Atom("a"), listOf(Atom("c")))),
                Fact(Structure(Atom("a"), listOf(Atom("d"))))
            ), dynamic = true
        )
        Program.db.load(predicate)

        val control = Program.query(Structure(Atom("a"), listOf(Variable("X")))).toList()

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

        val retract = Retract(Structure(Atom("a"), listOf(Variable("X"))))

        val result = retract.satisfy(emptyMap()).iterator()

        assertEquals(3, predicate.clauses.size, "Expected 3 clauses")

        assertTrue(result.hasNext(), "Expected more results")
        var answer = result.next()

        assertTrue(answer.isSuccess, "Expected success")
        var subs = answer.getOrNull()!!
        assertTrue(subs.isNotEmpty(), "Expected substitutions")
        assertTrue(Variable("X") in subs, "Expected variable X")
        assertEquals(Atom("b"), subs[Variable("X")], "Expected b")
        assertEquals(2, predicate.clauses.size, "Expected 2 clauses")

        answer = result.next()

        assertTrue(answer.isSuccess, "Expected success")
        subs = answer.getOrNull()!!
        assertTrue(subs.isNotEmpty(), "Expected substitutions")
        assertTrue(Variable("X") in subs, "Expected variable X")
        assertEquals(Atom("c"), subs[Variable("X")], "Expected c")
        assertEquals(1, predicate.clauses.size, "Expected 1 clause")

        answer = result.next()

        assertTrue(answer.isSuccess, "Expected success")
        subs = answer.getOrNull()!!
        assertTrue(subs.isNotEmpty(), "Expected substitutions")
        assertTrue(Variable("X") in subs, "Expected variable X")
        assertEquals(Atom("d"), subs[Variable("X")], "Expected d")
        assertEquals(0, predicate.clauses.size, "Expected no clauses")

        assertFalse(result.hasNext(), "Expected no more results")
    }

    @Test
    fun `custom assert example`() {
        var query = Structure(Atom("likes"), listOf(Atom("alice"), Atom("pizza")))

        var result = Program.query(query).toList()
        assertEquals(0, result.size, "Expected 0 results")

        var assert: Structure = Assert(Fact(query))
        assert.satisfy(emptyMap())

        result = Program.query(query).toList()
        assertEquals(1, result.size, "Expected 1 result")
        assertTrue(result[0].getOrNull()!!.isEmpty())

        assert = AssertZ(Fact(Structure(Atom("likes"), listOf(Atom("bob"), Atom("sushi")))))
        assert.satisfy(emptyMap())

        query = Structure(Atom("likes"), listOf(Atom("bob"), Variable("X")))

        result = Program.query(query).toList()
        assertEquals(1, result.size, "Expected 1 result")
        assertTrue(result[0].isSuccess, "Expected success")
        assertEquals(Atom("sushi"), result[0].getOrNull()!![Variable("X")], "Expected sushi")

        query = Structure(Atom("likes"), listOf(Variable("X"), Variable("Y")))

        result = Program.query(query).toList()
        assertEquals(2, result.size, "Expected 2 results")
        assertTrue(result[0].isSuccess, "Expected success")
        var result0 = result[0].getOrNull()!!
        assertEquals(Atom("alice"), result0[Variable("X")], "Expected alice")
        assertEquals(Atom("pizza"), result0[Variable("Y")], "Expected pizza")
        assertTrue(result[1].isSuccess, "Expected success")
        var result1 = result[1].getOrNull()!!
        assertEquals(Atom("bob"), result1[Variable("X")], "Expected bob")
        assertEquals(Atom("sushi"), result1[Variable("Y")], "Expected sushi")

        assert = AssertA(
            Rule(
                Structure(Atom("likes"), listOf(Variable("X"), Atom("italian"))),
                Structure(Atom("likes"), listOf(Variable("X"), Atom("pizza")))
            )
        )
        assert.satisfy(emptyMap())

        result = Program.query(query).toList()
        assertEquals(3, result.size, "Expected 3 results")
        assertTrue(result[0].isSuccess, "Expected success")
        result0 = result[0].getOrNull()!!
        assertEquals(Atom("alice"), result0[Variable("X")], "Expected alice")
        assertEquals(Atom("italian"), result0[Variable("Y")], "Expected italian")
        assertTrue(result[1].isSuccess, "Expected success")
        result1 = result[1].getOrNull()!!
        assertEquals(Atom("alice"), result1[Variable("X")], "Expected alice")
        assertEquals(Atom("pizza"), result1[Variable("Y")], "Expected pizza")
        assertTrue(result[2].isSuccess, "Expected success")
        val result2 = result[2].getOrNull()!!
        assertEquals(Atom("bob"), result2[Variable("X")], "Expected bob")
        assertEquals(Atom("sushi"), result2[Variable("Y")], "Expected sushi")
    }

    @Test
    fun `retract all`() {
        val predicate = Predicate(
            listOf(
                Fact(Structure(Atom("a"), listOf(Atom("b")))),
                Fact(Structure(Atom("a"), listOf(Atom("c")))),
                Fact(Structure(Atom("a"), listOf(Atom("d"))))
            ), dynamic = true
        )

        Program.db.load(predicate)

        val control = Program.query(Structure(Atom("a"), listOf(Variable("X")))).toList()
        assertEquals(3, control.size, "Expected 3 results")
        assertEquals(3, Program.db.predicates[Functor.of("a/1")]!!.clauses.size, "Expected 3 clauses")

        val retract = RetractAll(Structure(Atom("a"), listOf(Variable("X"))))
        val result = retract.satisfy(emptyMap()).toList()

        assertEquals(1, result.size, "Expected 1 result")
        assertTrue(result[0].isSuccess, "Expected success")
        assertEquals(0, Program.db.predicates[Functor.of("a/1")]!!.clauses.size, "Expected 0 clauses")
    }

    @Test
    fun `If Head refers to a predicate that is not defined, it is implicitly created as a dynamic predicate`() {
        val predicateName = "idonotyetexist"
        val predicateFunctor = Functor.of("$predicateName/1")

        assertFalse(predicateFunctor in Program.db.predicates, "Expected predicate to not exist before")

        val retractAll = RetractAll(Structure(Atom(predicateName), listOf(Variable("X"))))
        val result = retractAll.satisfy(emptyMap()).toList()

        assertEquals(1, result.size, "Expected 1 result")
        assertTrue(result[0].isSuccess, "Expected success")

        assertTrue(predicateFunctor in Program.db.predicates, "Expected predicate to exist after")
        assertTrue(Program.db.predicates[predicateFunctor]!!.dynamic, "Expected predicate to be dynamic")
    }
}