package prolog.builtins

import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ValueSource
import prolog.ast.arithmetic.Float
import prolog.ast.arithmetic.Integer
import prolog.ast.terms.Atom
import prolog.ast.terms.CompoundTerm
import prolog.ast.terms.Variable
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.PrintStream

class IoOperatorsTests {
    private var outStream = ByteArrayOutputStream()

    @BeforeEach
    fun setup() {
        outStream = ByteArrayOutputStream()
        System.setOut(PrintStream(outStream))
    }

    @Test
    fun dummyTest() {
        val message = "Hello, World!"
        print(message)
        assertEquals(message, outStream.toString(), "Output should match the message")
    }

    @ParameterizedTest
    @ValueSource(strings = [
        "a",
        "hello",
        "a very special christmas",
        "1 2 3 piano"
    ])
    fun `write atoms`(name: String) {
        val write = Write(Atom(name))

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

        assertEquals(1, result.size, "Should return one result")
        assertTrue(result[0].isSuccess, "Result should be successful")
        assertEquals(name, outStream.toString(), "Output should match the atom")
    }

    @Test
    fun `write structure`() {
        val write = Write(CompoundTerm(Atom("person"), listOf(Atom("john"), Atom("doe"))))

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

        assertEquals(1, result.size, "Should return one result")
        assertTrue(result[0].isSuccess, "Result should be successful")
        assertEquals("person(john,doe)", outStream.toString().trim(), "Output should match the structure")
    }

    @Test
    fun `write arithmetic`() {
        val a = Integer(1)
        val b = Variable("B")
        val c = Float(2.0f)
        val d = Variable("D")

        val mul = Multiply(c, d)
        val sub = Subtract(b, mul)
        val expr = EvaluatesTo(a, sub)

        val expected1 = "(1 =:= (B - (2.0 * D)))"
        val expected2 = "=:=(1, -(B, *(2.0, D)))"

        val write = Write(expr)

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

        assertEquals(1, result.size, "Should return one result")
        assertTrue(result[0].isSuccess, "Result should be successful")

        val output = outStream.toString()
        assertTrue(output == expected1 || output == expected2, "Output should match the arithmetic expression")
    }

    @Test
    fun `write nl`() {
        val nl = Nl

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

        assertEquals(1, result.size, "Should return one result")
        assertTrue(result[0].isSuccess, "Result should be successful")
        assertTrue(outStream.toString().contains("\n"), "Output should contain a newline")
    }

    @Test
    fun `read term`() {
        val inputStream = ByteArrayInputStream("hello.".toByteArray())
        System.setIn(inputStream)

        val read = Read(Variable("X"))

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

        assertEquals(1, result.size, "Should return one result")
        assertTrue(result[0].isSuccess, "Result should be successful")
        val answer = result[0].getOrNull()!!
        assertTrue(answer.containsKey(Variable("X")), "Result should be successful")
        assertInstanceOf(Atom::class.java, answer[Variable("X")], "Output should be an atom")
        assertEquals(Atom("hello"), answer[Variable("X")], "Output should match the read term")
    }

    @Test
    fun `read between(1, 2, 3)`() {
        val inputStream = ByteArrayInputStream("between(1, 2, 3).".toByteArray())
        System.setIn(inputStream)

        val read = Read(Variable("X"))

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

        assertEquals(1, result.size, "Should return one result")
        assertTrue(result[0].isSuccess, "Result should be successful")
        val answer = result[0].getOrNull()!!
        assertTrue(answer.containsKey(Variable("X")), "Result should be successful")
        assertInstanceOf(CompoundTerm::class.java, answer[Variable("X")], "Output should be a compound term")
        assertEquals(
            CompoundTerm(Atom("between"), listOf(Integer(1), Integer(2), Integer(3))),
            answer[Variable("X")],
            "Output should match the read term"
        )
    }

    @Test
    fun `read foo(a, X, b, Y, c, Z)`() {
        val inputStream = ByteArrayInputStream("foo(A, x, B, y, C, z).".toByteArray())
        System.setIn(inputStream)

        val read = Read(CompoundTerm(Atom("foo"), listOf(
            Atom("a"),
            Variable("X"),
            Atom("b"),
            Variable("Y"),
            Atom("c"),
            Variable("Z")
        )))

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

        assertEquals(1, result.size, "Should return one result")
        assertTrue(result[0].isSuccess, "Result should be successful")

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

        assertTrue(answer.containsKey(Variable("X")), "Result should be successful")
        assertTrue(answer.containsKey(Variable("Y")), "Result should be successful")
        assertTrue(answer.containsKey(Variable("Z")), "Result should be successful")

        assertEquals(Atom("x"), answer[Variable("X")], "Output should match the read term")
        assertEquals(Atom("y"), answer[Variable("Y")], "Output should match the read term")
        assertEquals(Atom("z"), answer[Variable("Z")], "Output should match the read term")
    }
}