/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package mozilla.components.lib.jexl.lexer

import mozilla.components.lib.jexl.grammar.Grammar
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test

class LexerTest {
    private lateinit var lexer: Lexer

    @Before
    fun setUp() {
        lexer = Lexer(Grammar())
    }

    @Test
    fun `should count a string as one element`() {
        val expression = "\"foo\""

        assertExpressionYieldsTokens(expression, listOf(
            Token(
                Token.Type.LITERAL,
                "\"foo\"",
                "foo"
            )
        ))
    }

    @Test
    fun `should support single-quote strings`() {
        val expression = "'foo'"

        assertExpressionYieldsTokens(expression, listOf(
            Token(Token.Type.LITERAL, "'foo'", "foo")
        ))
    }

    @Test
    fun `should find multiple strings`() {
        val expression = "\"foo\" 'bar' \"baz\""

        assertExpressionYieldsTokens(expression, listOf(
            Token(
                Token.Type.LITERAL,
                "\"foo\"",
                "foo"
            ),
            Token(
                Token.Type.LITERAL,
                "'bar'",
                "bar"
            ),
            Token(
                Token.Type.LITERAL,
                "\"baz\"",
                "baz"
            )
        ))
    }

    @Test
    fun `should support escaping double-quotes`() {
        val expression = "\"f\\\"oo\""

        assertExpressionYieldsTokens(expression, listOf(
            Token(
                Token.Type.LITERAL,
                "\"f\\\"oo\"",
                "f\"oo"
            )
        ))
    }

    @Test
    fun `should support escaping single-quotes`() {
        val expression = "'f\\'oo'"

        assertExpressionYieldsTokens(expression, listOf(
            Token(
                Token.Type.LITERAL,
                "'f\\'oo'",
                "f'oo"
            )
        ))
    }

    @Test
    fun `should count an identifier as one element`() {
        val expression = "alpha12345"

        assertExpressionYieldsTokens(expression, listOf(
            Token(
                Token.Type.IDENTIFIER,
                "alpha12345",
                "alpha12345"
            )
        ))
    }

    @Test
    fun `should support boolean true`() {
        val expression = "true"

        assertExpressionYieldsTokens(expression, listOf(
            Token(Token.Type.LITERAL, "true", true)
        ))
    }

    @Test
    fun `should support boolean false`() {
        val expression = "false"

        assertExpressionYieldsTokens(expression, listOf(
            Token(Token.Type.LITERAL, "false", false)
        ))
    }

    @Test
    fun `should support comments`() {
        val expression = "# This is a comment"

        assertExpressionYieldsTokens(expression, emptyList())
    }

    @Test
    fun `should support comments after expressions`() {
        val expression = "true false #This is a comment"

        assertExpressionYieldsTokens(expression, listOf(
            Token(Token.Type.LITERAL, "true", true),
            Token(Token.Type.LITERAL, "false", false)
        ))
    }

    @Test
    fun `should support operators`() {
        val expression = "true + false - true + false++"

        assertExpressionYieldsTokens(expression, listOf(
            Token(Token.Type.LITERAL, "true", true),
            Token(Token.Type.BINARY_OP, "+", "+"),
            Token(
                Token.Type.LITERAL,
                "false",
                false
            ),
            Token(Token.Type.BINARY_OP, "-", "-"),
            Token(Token.Type.LITERAL, "true", true),
            Token(Token.Type.BINARY_OP, "+", "+"),
            Token(
                Token.Type.LITERAL,
                "false",
                false
            ),
            Token(Token.Type.BINARY_OP, "+", "+"),
            Token(Token.Type.BINARY_OP, "+", "+")
        ))
    }

    @Test
    fun `should support operators with two characters`() {
        val expression = "true == false + true != false >= true > in false"

        assertExpressionYieldsTokens(expression, listOf(
            Token(Token.Type.LITERAL, "true", true),
            Token(Token.Type.BINARY_OP, "==", "=="),
            Token(
                Token.Type.LITERAL,
                "false",
                false
            ),
            Token(Token.Type.BINARY_OP, "+", "+"),
            Token(Token.Type.LITERAL, "true", true),
            Token(Token.Type.BINARY_OP, "!=", "!="),
            Token(
                Token.Type.LITERAL,
                "false",
                false
            ),
            Token(Token.Type.BINARY_OP, ">=", ">="),
            Token(Token.Type.LITERAL, "true", true),
            Token(Token.Type.BINARY_OP, ">", ">"),
            Token(Token.Type.BINARY_OP, "in", "in"),
            Token(Token.Type.LITERAL, "false", false)
        ))
    }

    @Test
    fun `should support numerics`() {
        val expression = "1234 == 782"

        assertExpressionYieldsTokens(expression, listOf(
            Token(Token.Type.LITERAL, "1234", 1234),
            Token(Token.Type.BINARY_OP, "==", "=="),
            Token(Token.Type.LITERAL, "782", 782)
        ))
    }

    @Test
    fun `should support negative numerics`() {
        val expression = "-7.6 + (-20 * -1)"

        assertExpressionYieldsTokens(expression, listOf(
            Token(Token.Type.LITERAL, "-7.6", -7.6),
            Token(Token.Type.BINARY_OP, "+", "+"),
            Token(Token.Type.OPEN_PAREN, "(", "("),
            Token(Token.Type.LITERAL, "-20", -20),
            Token(Token.Type.BINARY_OP, "*", "*"),
            Token(Token.Type.LITERAL, "-1", -1),
            Token(Token.Type.CLOSE_PAREN, ")", ")")
        ))
    }

    @Test
    fun `should support floating point numerics`() {
        val expression = "1.337 != 2.42"

        assertExpressionYieldsTokens(expression, listOf(
            Token(
                Token.Type.LITERAL,
                "1.337",
                1.337
            ),
            Token(Token.Type.BINARY_OP, "!=", "!="),
            Token(Token.Type.LITERAL, "2.42", 2.42)
        ))
    }

    @Test
    fun `should support identifiers, numerics and operators`() {
        val expression = "person.age == 12 && (person.hasJob == true || person.onVacation != false)"

        assertExpressionYieldsTokens(expression, listOf(
            Token(
                Token.Type.IDENTIFIER,
                "person",
                "person"
            ),
            Token(Token.Type.DOT, ".", "."),
            Token(
                Token.Type.IDENTIFIER,
                "age",
                "age"
            ),
            Token(Token.Type.BINARY_OP, "==", "=="),
            Token(Token.Type.LITERAL, "12", 12),
            Token(Token.Type.BINARY_OP, "&&", "&&"),
            Token(Token.Type.OPEN_PAREN, "(", "("),
            Token(
                Token.Type.IDENTIFIER,
                "person",
                "person"
            ),
            Token(Token.Type.DOT, ".", "."),
            Token(
                Token.Type.IDENTIFIER,
                "hasJob",
                "hasJob"
            ),
            Token(Token.Type.BINARY_OP, "==", "=="),
            Token(Token.Type.LITERAL, "true", true),
            Token(Token.Type.BINARY_OP, "||", "||"),
            Token(
                Token.Type.IDENTIFIER,
                "person",
                "person"
            ),
            Token(Token.Type.DOT, ".", "."),
            Token(
                Token.Type.IDENTIFIER,
                "onVacation",
                "onVacation"
            ),
            Token(Token.Type.BINARY_OP, "!=", "!="),
            Token(
                Token.Type.LITERAL,
                "false",
                false
            ),
            Token(Token.Type.CLOSE_PAREN, ")", ")")
        ))
    }

    @Test
    fun `tokenize math expression`() {
        val expression = "age * (3 - 1)"

        assertExpressionYieldsTokens(expression, listOf(
            Token(
                Token.Type.IDENTIFIER,
                "age",
                "age"
            ),
            Token(Token.Type.BINARY_OP, "*", "*"),
            Token(Token.Type.OPEN_PAREN, "(", "("),
            Token(Token.Type.LITERAL, "3", 3),
            Token(Token.Type.BINARY_OP, "-", "-"),
            Token(Token.Type.LITERAL, "1", 1),
            Token(Token.Type.CLOSE_PAREN, ")", ")")
        ))
    }

    @Test
    fun `should not split grammar elements out of transforms`() {
        val expression = "inString"

        assertExpressionYieldsTokens(expression, listOf(
            Token(
                Token.Type.IDENTIFIER,
                "inString",
                "inString"
            )
        ))
    }

    @Test
    fun `should handle a complex mix of comments in single, multiline and value contexts`() {
        val expression = """
            6+x -  -17.55*y #end comment
            <= !foo.bar["baz\"foz"] # with space
            && b=="not a #comment" # is a comment
            # comment # 2nd comment
        """.trimIndent()

        assertExpressionYieldsTokens(expression, listOf(
            Token(Token.Type.LITERAL, "6", 6),
            Token(Token.Type.BINARY_OP, "+", "+"),
            Token(Token.Type.IDENTIFIER, "x", "x"),
            Token(Token.Type.BINARY_OP, "-", "-"),
            Token(
                Token.Type.LITERAL,
                "-17.55",
                -17.55
            ),
            Token(Token.Type.BINARY_OP, "*", "*"),
            Token(Token.Type.IDENTIFIER, "y", "y"),
            Token(Token.Type.BINARY_OP, "<=", "<="),
            Token(Token.Type.UNARY_OP, "!", "!"),
            Token(
                Token.Type.IDENTIFIER,
                "foo",
                "foo"
            ),
            Token(Token.Type.DOT, ".", "."),
            Token(
                Token.Type.IDENTIFIER,
                "bar",
                "bar"
            ),
            Token(Token.Type.OPEN_BRACKET, "[", "["),
            Token(
                Token.Type.LITERAL,
                "\"baz\\\"foz\"",
                "baz\"foz"
            ),
            Token(
                Token.Type.CLOSE_BRACKET,
                "]",
                "]"
            ),
            Token(Token.Type.BINARY_OP, "&&", "&&"),
            Token(Token.Type.IDENTIFIER, "b", "b"),
            Token(Token.Type.BINARY_OP, "==", "=="),
            Token(
                Token.Type.LITERAL,
                "\"not a #comment\"",
                "not a #comment"
            )
        ))
    }

    @Test
    fun `should tokenize a full expression`() {
        val expression = """6+x -  -17.55*y<= !foo.bar["baz\\"foz"]"""

        assertExpressionYieldsTokens(expression, listOf(
            Token(Token.Type.LITERAL, "6", 6),
            Token(Token.Type.BINARY_OP, "+", "+"),
            Token(Token.Type.IDENTIFIER, "x", "x"),
            Token(Token.Type.BINARY_OP, "-", "-"),
            Token(Token.Type.LITERAL, "-17.55", -17.55),
            Token(Token.Type.BINARY_OP, "*", "*"),
            Token(Token.Type.IDENTIFIER, "y", "y"),
            Token(Token.Type.BINARY_OP, "<=", "<="),
            Token(Token.Type.UNARY_OP, "!", "!"),
            Token(Token.Type.IDENTIFIER, "foo", "foo"),
            Token(Token.Type.DOT, ".", "."),
            Token(Token.Type.IDENTIFIER, "bar", "bar"),
            Token(Token.Type.OPEN_BRACKET, "[", "["),
            Token(Token.Type.LITERAL, """"baz\\"foz"""", """baz\"foz"""),
            Token(Token.Type.CLOSE_BRACKET, "]", "]")
        ))
    }

    @Test
    fun `should consider minus to be negative appropriately`() {
        val expression = "-1?-2:-3"

        assertExpressionYieldsTokens(expression, listOf(
            Token(Token.Type.LITERAL, "-1", -1),
            Token(Token.Type.QUESTION, "?", "?"),
            Token(Token.Type.LITERAL, "-2", -2),
            Token(Token.Type.COLON, ":", ":"),
            Token(Token.Type.LITERAL, "-3", -3)
        ))
    }

    private fun assertExpressionYieldsTokens(expression: String, tokens: List<Token>) {
        val actual = lexer.tokenize(expression)

        println(actual)

        assertEquals(tokens.size, actual.size)

        for (i in 0 until tokens.size) {
            assertEquals(tokens[i], actual[i])
        }
    }
}
