package syntax

import (
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestLexer(t *testing.T) {
	t.Run("returns false on end of input", func(t *testing.T) {
		input := ""
		l := lex(input)

		assert.Equal(t, tokenKind_EOI, l.Next().Kind())
	})

	t.Run("lexes whitespace properly", func(t *testing.T) {
		input := " \t\r\n"
		l := lex(input)

		token := l.Next()
		assert.Equal(t, token.Kind(), tokenKind_WS)
		assert.Equal(t, input, input[token.Start():token.End()])
	})

	t.Run("lexes number properly", func(t *testing.T) {
		input := "12345 54321"
		l := lex(input)

		token := l.Next()
		assert.Equal(t, token.Kind(), tokenKind_Number)
		assert.Equal(t, "12345", input[token.Start():token.End()])

		// Skip whitespace token
		l.Next()
		token = l.Next()
		assert.Equal(t, token.Kind(), tokenKind_Number)
		assert.Equal(t, "54321", input[token.Start():token.End()])
	})

	t.Run("lexes symbols correctly", func(t *testing.T) {
		input := "{}#:,="
		l := lex(input)
		kinds := []struct {
			symbol string
			kind   tokenKind
		}{
			{"{", tokenKind_OpenBrace},
			{"}", tokenKind_CloseBrace},
			{"#", tokenKind_Hash},
			{":", tokenKind_Colon},
			{",", tokenKind_Comma},
			{"=", tokenKind_Equal},
		}

		for _, s := range kinds {
			token := l.Next()

			assert.Equal(t, s.kind, token.Kind())
			assert.Equal(t, s.symbol, input[token.Start():token.End()])
		}
	})

	t.Run("lexes escaped symbols correctly", func(t *testing.T) {
		input := "'{'#'<"
		l := lex(input)
		kinds := []struct {
			symbol string
			kind   tokenKind
		}{
			{"{", tokenKind_Text},
			{"#", tokenKind_Text},
			{"<", tokenKind_Text},
		}

		for _, s := range kinds {
			token := l.Next()

			assert.Equal(t, s.kind, token.Kind())
			assert.Equal(t, s.symbol, input[token.Start():token.End()])
		}
	})

	t.Run("lexes ' correctly", func(t *testing.T) {
		input := "v'v"
		l := lex(input)

		token := l.Next()
		assert.Equal(t, tokenKind_Text, token.Kind())
		assert.Equal(t, "v", input[token.Start():token.End()])

		token = l.Next()
		assert.Equal(t, tokenKind_Text, token.Kind())
		assert.Equal(t, "'", input[token.Start():token.End()])

		token = l.Next()
		assert.Equal(t, tokenKind_Text, token.Kind())
		assert.Equal(t, "v", input[token.Start():token.End()])
	})

	t.Run("lexes text correctly", func(t *testing.T) {
		input := "this_is_test_text"
		l := lex(input)
		token := l.Next()

		assert.Equal(t, tokenKind_Text, token.Kind())
		assert.Equal(t, input, input[token.Start():token.End()])
	})

	t.Run("lexes multi-byte text correctly", func(t *testing.T) {
		input := "🥶❤️👻"
		l := lex(input)
		token := l.Next()

		assert.Equal(t, tokenKind_Text, token.Kind())
		assert.Equal(t, input, input[token.Start():token.End()])
	})

	t.Run("lexes open tag", func(t *testing.T) {
		input := "<em>"
		l := lex(input)

		assert.Equal(t, tokenKind_ElementOpenStart, l.Next().Kind())
		token := l.Next()
		assert.Equal(t, tokenKind_ElementName, token.Kind())
		assert.Equal(t, "em", input[token.Start():token.End()])
		assert.Equal(t, tokenKind_ElementEnd, l.Next().Kind())
	})

	t.Run("lexes element with space in name", func(t *testing.T) {
		input := "<em f>"
		l := lex(input)

		assert.Equal(t, tokenKind_ElementOpenStart, l.Next().Kind())
		assert.Equal(t, tokenKind_ElementName, l.Next().Kind())
		assert.Equal(t, tokenKind_WS, l.Next().Kind())
		assert.Equal(t, tokenKind_ElementName, l.Next().Kind())
		assert.Equal(t, tokenKind_ElementEnd, l.Next().Kind())
	})

	t.Run("lexes close tag", func(t *testing.T) {
		input := "</em>"
		l := lex(input)

		assert.Equal(t, tokenKind_ElementCloseStart, l.Next().Kind())
		assert.Equal(t, tokenKind_ElementName, l.Next().Kind())
		assert.Equal(t, tokenKind_ElementEnd, l.Next().Kind())
	})

	t.Run("lexes tag pair", func(t *testing.T) {
		input := "<em></em>"
		l := lex(input)

		assert.Equal(t, tokenKind_ElementOpenStart, l.Next().Kind())
		assert.Equal(t, tokenKind_ElementName, l.Next().Kind())
		assert.Equal(t, tokenKind_ElementEnd, l.Next().Kind())

		assert.Equal(t, tokenKind_ElementCloseStart, l.Next().Kind())
		assert.Equal(t, tokenKind_ElementName, l.Next().Kind())
		assert.Equal(t, tokenKind_ElementEnd, l.Next().Kind())
	})

	t.Run("lexes tag pair with text in middle", func(t *testing.T) {
		input := "<em>test</em>"
		l := lex(input)

		assert.Equal(t, tokenKind_ElementOpenStart, l.Next().Kind())
		assert.Equal(t, tokenKind_ElementName, l.Next().Kind())
		assert.Equal(t, tokenKind_ElementEnd, l.Next().Kind())

		assert.Equal(t, tokenKind_Text, l.Next().Kind())

		assert.Equal(t, tokenKind_ElementCloseStart, l.Next().Kind())
		assert.Equal(t, tokenKind_ElementName, l.Next().Kind())
		assert.Equal(t, tokenKind_ElementEnd, l.Next().Kind())
	})

	t.Run("lexes close tag after text", func(t *testing.T) {
		input := "text</em>"
		l := lex(input)

		token := l.Next()
		assert.Equal(t, tokenKind_Text, token.Kind())
		assert.Equal(t, "text", input[token.Start():token.End()])

		assert.Equal(t, tokenKind_ElementCloseStart, l.Next().Kind())
		assert.Equal(t, tokenKind_ElementName, l.Next().Kind())
		assert.Equal(t, tokenKind_ElementEnd, l.Next().Kind())
	})

	t.Run("lexes keywords correctly", func(t *testing.T) {
		input := "plural select offset zero one two few many other number time date integer currency percent em "
		l := lex(input)
		kinds := []struct {
			symbol string
			kind   tokenKind
		}{
			{"plural", tokenKind_Plural},
			{"select", tokenKind_Select},
			{"offset", tokenKind_Offset},
			{"zero", tokenKind_Zero},
			{"one", tokenKind_One},
			{"two", tokenKind_Two},
			{"few", tokenKind_Few},
			{"many", tokenKind_Many},
			{"other", tokenKind_Other},
			{"number", tokenKind_FormatNumber},
			{"time", tokenKind_FormatTime},
			{"date", tokenKind_FormateDate},
			{"integer", tokenKind_NumberFormatInteger},
			{"currency", tokenKind_NumberFormatCurrency},
			{"percent", tokenKind_NumberFormatPercent},
		}

		for _, s := range kinds {
			token := l.Next()

			assert.Equal(t, s.kind, token.Kind())
			assert.Equal(t, s.symbol, input[token.Start():token.End()])

			// Skip whitespace
			assert.Equal(t, tokenKind_WS, l.Next().Kind())
		}
	})

	t.Run("lexes unescaped <", func(t *testing.T) {
		input := "<"
		l := lex(input)

		assert.Equal(t, tokenKind_ElementOpenStart, l.Next().Kind())
	})

	t.Run("returns EOI for NULL terminator in string", func(t *testing.T) {
		input := "\x00 test"
		l := lex(input)

		assert.Equal(t, tokenKind_EOI, l.Next().Kind())
	})
}
