// Copyright (C) 2022-2023  Luke Shumaker <lukeshu@lukeshu.com>
//
// SPDX-License-Identifier: GPL-2.0-or-later

package lowmemjson

import (
	"fmt"
	"io"
	"strings"
	"testing"

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

	"git.lukeshu.com/go/lowmemjson/internal"
)

type ReadRuneTypeResult struct {
	r rune
	s int
	t internal.RuneType
	e error
}

func (r ReadRuneTypeResult) String() string {
	return fmt.Sprintf("{%q, %d, %#v, %v}", r.r, r.s, r.t, r.e)
}

type runeTypeScannerTestcase struct {
	Input        string
	ExpRemainder string
	Exp          []ReadRuneTypeResult
}

func testRuneTypeScanner(t *testing.T, testcases map[string]runeTypeScannerTestcase, factory func(io.RuneScanner) runeTypeScanner) {
	for tcName, tc := range testcases {
		tc := tc
		t.Run(tcName, func(t *testing.T) {
			t.Parallel()
			reader := strings.NewReader(tc.Input)
			sc := factory(reader)
			var exp, act []string
			for _, iExp := range tc.Exp {
				var iAct ReadRuneTypeResult
				if iExp.s < 0 {
					iAct.s = iExp.s
					iAct.e = sc.UnreadRune()
				} else {
					iAct.r, iAct.s, iAct.t, iAct.e = sc.ReadRuneType()
				}
				exp = append(exp, iExp.String())
				act = append(act, iAct.String())
			}
			assert.Equal(t, exp, act)
			assert.Equal(t, tc.ExpRemainder, tc.Input[len(tc.Input)-reader.Len():])
		})
	}
}

func TestRuneTypeScanner(t *testing.T) {
	t.Parallel()
	testcases := map[string]runeTypeScannerTestcase{
		"basic": {`{"foo": 12.0}`, ``, []ReadRuneTypeResult{
			{'{', 1, internal.RuneTypeObjectBeg, nil},
			{'"', 1, internal.RuneTypeStringBeg, nil},
			{'f', 1, internal.RuneTypeStringChar, nil},
			{'o', 1, internal.RuneTypeStringChar, nil},
			{'o', 1, internal.RuneTypeStringChar, nil},
			{'"', 1, internal.RuneTypeStringEnd, nil},
			{':', 1, internal.RuneTypeObjectColon, nil},
			{' ', 1, internal.RuneTypeSpace, nil},
			{'1', 1, internal.RuneTypeNumberIntDig, nil},
			{'2', 1, internal.RuneTypeNumberIntDig, nil},
			{'.', 1, internal.RuneTypeNumberFracDot, nil},
			{'0', 1, internal.RuneTypeNumberFracDig, nil},
			{'}', 1, internal.RuneTypeObjectEnd, nil},
			{0, 0, internal.RuneTypeEOF, nil},
			{0, 0, internal.RuneTypeEOF, nil},
		}},
		"unread": {`{"foo": 12.0}`, ``, []ReadRuneTypeResult{
			{'{', 1, internal.RuneTypeObjectBeg, nil},
			{'"', 1, internal.RuneTypeStringBeg, nil},
			{'f', 1, internal.RuneTypeStringChar, nil},
			{'o', 1, internal.RuneTypeStringChar, nil},
			{'o', 1, internal.RuneTypeStringChar, nil},
			{'"', 1, internal.RuneTypeStringEnd, nil},
			{':', 1, internal.RuneTypeObjectColon, nil},
			{' ', 1, internal.RuneTypeSpace, nil},
			{'1', 1, internal.RuneTypeNumberIntDig, nil},
			{0, -1, 0, nil},
			{'1', 1, internal.RuneTypeNumberIntDig, nil},
			{'2', 1, internal.RuneTypeNumberIntDig, nil},
			{'.', 1, internal.RuneTypeNumberFracDot, nil},
			{'0', 1, internal.RuneTypeNumberFracDig, nil},
			{'}', 1, internal.RuneTypeObjectEnd, nil},
			{0, 0, internal.RuneTypeEOF, nil},
			{0, 0, internal.RuneTypeEOF, nil},
		}},
		"unread2": {`{"foo": 12.0}`, ``, []ReadRuneTypeResult{
			{'{', 1, internal.RuneTypeObjectBeg, nil},
			{'"', 1, internal.RuneTypeStringBeg, nil},
			{'f', 1, internal.RuneTypeStringChar, nil},
			{'o', 1, internal.RuneTypeStringChar, nil},
			{'o', 1, internal.RuneTypeStringChar, nil},
			{'"', 1, internal.RuneTypeStringEnd, nil},
			{':', 1, internal.RuneTypeObjectColon, nil},
			{' ', 1, internal.RuneTypeSpace, nil},
			{'1', 1, internal.RuneTypeNumberIntDig, nil},
			{0, -1, 0, nil},
			{0, -1, 0, ErrInvalidUnreadRune},
			{'1', 1, internal.RuneTypeNumberIntDig, nil},
			{'2', 1, internal.RuneTypeNumberIntDig, nil},
			{'.', 1, internal.RuneTypeNumberFracDot, nil},
			{'0', 1, internal.RuneTypeNumberFracDig, nil},
			{'}', 1, internal.RuneTypeObjectEnd, nil},
			{0, 0, internal.RuneTypeEOF, nil},
			{0, 0, internal.RuneTypeEOF, nil},
		}},
		"unread-eof": {`{"foo": 12.0}`, ``, []ReadRuneTypeResult{
			{'{', 1, internal.RuneTypeObjectBeg, nil},
			{'"', 1, internal.RuneTypeStringBeg, nil},
			{'f', 1, internal.RuneTypeStringChar, nil},
			{'o', 1, internal.RuneTypeStringChar, nil},
			{'o', 1, internal.RuneTypeStringChar, nil},
			{'"', 1, internal.RuneTypeStringEnd, nil},
			{':', 1, internal.RuneTypeObjectColon, nil},
			{' ', 1, internal.RuneTypeSpace, nil},
			{'1', 1, internal.RuneTypeNumberIntDig, nil},
			{'2', 1, internal.RuneTypeNumberIntDig, nil},
			{'.', 1, internal.RuneTypeNumberFracDot, nil},
			{'0', 1, internal.RuneTypeNumberFracDig, nil},
			{'}', 1, internal.RuneTypeObjectEnd, nil},
			{0, 0, internal.RuneTypeEOF, nil},
			{0, -1, 0, ErrInvalidUnreadRune},
			{0, 0, internal.RuneTypeEOF, nil},
			{0, 0, internal.RuneTypeEOF, nil},
		}},
		"syntax-error": {`[[0,]`, ``, []ReadRuneTypeResult{
			{'[', 1, internal.RuneTypeArrayBeg, nil},
			{'[', 1, internal.RuneTypeArrayBeg, nil},
			{'0', 1, internal.RuneTypeNumberIntZero, nil},
			{',', 1, internal.RuneTypeArrayComma, nil},
			{']', 1, internal.RuneTypeError, &DecodeSyntaxError{Offset: 5, Err: fmt.Errorf("invalid character %q looking for beginning of value", ']')}},
			{']', 1, internal.RuneTypeError, &DecodeSyntaxError{Offset: 5, Err: fmt.Errorf("invalid character %q looking for beginning of value", ']')}},
			{']', 1, internal.RuneTypeError, &DecodeSyntaxError{Offset: 5, Err: fmt.Errorf("invalid character %q looking for beginning of value", ']')}},
		}},
		"multi-value": {`1{}`, `}`, []ReadRuneTypeResult{
			{'1', 1, internal.RuneTypeNumberIntDig, nil},
			{'{', 1, internal.RuneTypeEOF, nil},
			{'{', 1, internal.RuneTypeEOF, nil},
			{'{', 1, internal.RuneTypeEOF, nil},
		}},
		"early-eof": {`{`, ``, []ReadRuneTypeResult{
			{'{', 1, internal.RuneTypeObjectBeg, nil},
			{0, 0, internal.RuneTypeError, &DecodeSyntaxError{Offset: 1, Err: io.ErrUnexpectedEOF}},
			{0, 0, internal.RuneTypeError, &DecodeSyntaxError{Offset: 1, Err: io.ErrUnexpectedEOF}},
			{0, 0, internal.RuneTypeError, &DecodeSyntaxError{Offset: 1, Err: io.ErrUnexpectedEOF}},
		}},
		"empty": {``, ``, []ReadRuneTypeResult{
			{0, 0, internal.RuneTypeError, &DecodeSyntaxError{Offset: 0, Err: io.EOF}},
			{0, 0, internal.RuneTypeError, &DecodeSyntaxError{Offset: 0, Err: io.EOF}},
			{0, 0, internal.RuneTypeError, &DecodeSyntaxError{Offset: 0, Err: io.EOF}},
		}},
	}
	testRuneTypeScanner(t, testcases, func(reader io.RuneScanner) runeTypeScanner {
		return &runeTypeScannerImpl{
			inner: reader,
		}
	})
}

func TestNoWSRuneTypeScanner(t *testing.T) {
	t.Parallel()
	testcases := map[string]runeTypeScannerTestcase{
		"basic": {`{"foo": 12.0}`, ``, []ReadRuneTypeResult{
			{'{', 1, internal.RuneTypeObjectBeg, nil},
			{'"', 1, internal.RuneTypeStringBeg, nil},
			{'f', 1, internal.RuneTypeStringChar, nil},
			{'o', 1, internal.RuneTypeStringChar, nil},
			{'o', 1, internal.RuneTypeStringChar, nil},
			{'"', 1, internal.RuneTypeStringEnd, nil},
			{':', 1, internal.RuneTypeObjectColon, nil},
			{'1', 1, internal.RuneTypeNumberIntDig, nil},
			{'2', 1, internal.RuneTypeNumberIntDig, nil},
			{'.', 1, internal.RuneTypeNumberFracDot, nil},
			{'0', 1, internal.RuneTypeNumberFracDig, nil},
			{'}', 1, internal.RuneTypeObjectEnd, nil},
			{0, 0, internal.RuneTypeEOF, nil},
			{0, 0, internal.RuneTypeEOF, nil},
		}},
		"unread": {`{"foo": 12.0}`, ``, []ReadRuneTypeResult{
			{'{', 1, internal.RuneTypeObjectBeg, nil},
			{'"', 1, internal.RuneTypeStringBeg, nil},
			{'f', 1, internal.RuneTypeStringChar, nil},
			{'o', 1, internal.RuneTypeStringChar, nil},
			{'o', 1, internal.RuneTypeStringChar, nil},
			{'"', 1, internal.RuneTypeStringEnd, nil},
			{':', 1, internal.RuneTypeObjectColon, nil},
			{'1', 1, internal.RuneTypeNumberIntDig, nil},
			{0, -1, 0, nil},
			{'1', 1, internal.RuneTypeNumberIntDig, nil},
			{'2', 1, internal.RuneTypeNumberIntDig, nil},
			{'.', 1, internal.RuneTypeNumberFracDot, nil},
			{'0', 1, internal.RuneTypeNumberFracDig, nil},
			{'}', 1, internal.RuneTypeObjectEnd, nil},
			{0, 0, internal.RuneTypeEOF, nil},
			{0, 0, internal.RuneTypeEOF, nil},
		}},
		"tail": {`{"foo": 12.0}  `, ``, []ReadRuneTypeResult{
			{'{', 1, internal.RuneTypeObjectBeg, nil},
			{'"', 1, internal.RuneTypeStringBeg, nil},
			{'f', 1, internal.RuneTypeStringChar, nil},
			{'o', 1, internal.RuneTypeStringChar, nil},
			{'o', 1, internal.RuneTypeStringChar, nil},
			{'"', 1, internal.RuneTypeStringEnd, nil},
			{':', 1, internal.RuneTypeObjectColon, nil},
			{'1', 1, internal.RuneTypeNumberIntDig, nil},
			{'2', 1, internal.RuneTypeNumberIntDig, nil},
			{'.', 1, internal.RuneTypeNumberFracDot, nil},
			{'0', 1, internal.RuneTypeNumberFracDig, nil},
			{'}', 1, internal.RuneTypeObjectEnd, nil},
			{0, 0, internal.RuneTypeEOF, nil},
			{0, 0, internal.RuneTypeEOF, nil},
		}},
		"multi-value": {`1{}`, `}`, []ReadRuneTypeResult{
			{'1', 1, internal.RuneTypeNumberIntDig, nil},
			{'{', 1, internal.RuneTypeEOF, nil},
			{'{', 1, internal.RuneTypeEOF, nil},
			{'{', 1, internal.RuneTypeEOF, nil},
		}},
		"early-eof": {` {`, ``, []ReadRuneTypeResult{
			{'{', 1, internal.RuneTypeObjectBeg, nil},
			{0, 0, internal.RuneTypeError, &DecodeSyntaxError{Offset: 2, Err: io.ErrUnexpectedEOF}},
			{0, 0, internal.RuneTypeError, &DecodeSyntaxError{Offset: 2, Err: io.ErrUnexpectedEOF}},
			{0, 0, internal.RuneTypeError, &DecodeSyntaxError{Offset: 2, Err: io.ErrUnexpectedEOF}},
		}},
	}
	testRuneTypeScanner(t, testcases, func(reader io.RuneScanner) runeTypeScanner {
		return &noWSRuneTypeScanner{
			inner: &runeTypeScannerImpl{
				inner: reader,
			},
		}
	})
}

func TestElemRuneTypeScanner(t *testing.T) {
	t.Parallel()
	toplevelTestcases := map[string]runeTypeScannerTestcase{
		"basic": {`1`, ``, []ReadRuneTypeResult{
			{'1', 1, internal.RuneTypeNumberIntDig, nil},
			{0, 0, internal.RuneTypeEOF, nil},
			{0, 0, internal.RuneTypeEOF, nil},
			{0, 0, internal.RuneTypeEOF, nil},
		}},
		"syntax-error": {`[[0,]`, ``, []ReadRuneTypeResult{
			{'[', 1, internal.RuneTypeArrayBeg, nil},
			{'[', 1, internal.RuneTypeArrayBeg, nil},
			{'0', 1, internal.RuneTypeNumberIntZero, nil},
			{',', 1, internal.RuneTypeArrayComma, nil},
			{']', 1, internal.RuneTypeError, &DecodeSyntaxError{Offset: 5, Err: fmt.Errorf("invalid character %q looking for beginning of value", ']')}},
			{']', 1, internal.RuneTypeError, &DecodeSyntaxError{Offset: 5, Err: fmt.Errorf("invalid character %q looking for beginning of value", ']')}},
			{']', 1, internal.RuneTypeError, &DecodeSyntaxError{Offset: 5, Err: fmt.Errorf("invalid character %q looking for beginning of value", ']')}},
		}},
		"multi-value": {`1{}`, `{}`, []ReadRuneTypeResult{
			{'1', 1, internal.RuneTypeNumberIntDig, nil},
			{0, 0, internal.RuneTypeEOF, nil},
			{0, 0, internal.RuneTypeEOF, nil},
			{0, 0, internal.RuneTypeEOF, nil},
		}},
		"fragment": {`1,`, `,`, []ReadRuneTypeResult{
			{'1', 1, internal.RuneTypeNumberIntDig, nil},
			{0, 0, internal.RuneTypeEOF, nil},
			{0, 0, internal.RuneTypeEOF, nil},
			{0, 0, internal.RuneTypeEOF, nil},
		}},
		"early-eof": {`{`, ``, []ReadRuneTypeResult{
			{'{', 1, internal.RuneTypeObjectBeg, nil},
			{0, 0, internal.RuneTypeError, &DecodeSyntaxError{Offset: 1, Err: io.ErrUnexpectedEOF}},
			{0, 0, internal.RuneTypeError, &DecodeSyntaxError{Offset: 1, Err: io.ErrUnexpectedEOF}},
			{0, 0, internal.RuneTypeError, &DecodeSyntaxError{Offset: 1, Err: io.ErrUnexpectedEOF}},
		}},
	}

	childTestcases := make(map[string]runeTypeScannerTestcase, len(toplevelTestcases))
	for tcName, tc := range toplevelTestcases {
		tc.Input = `[` + tc.Input
		tc.Exp = append([]ReadRuneTypeResult(nil), tc.Exp...) //  copy
		for i, res := range tc.Exp {
			if se, ok := res.e.(*DecodeSyntaxError); ok {
				seCopy := *se
				seCopy.Offset++
				tc.Exp[i].e = &seCopy
			}
		}
		childTestcases[tcName] = tc
	}

	t.Run("top-level", func(t *testing.T) {
		t.Parallel()
		testRuneTypeScanner(t, toplevelTestcases, func(reader io.RuneScanner) runeTypeScanner {
			return &elemRuneTypeScanner{
				inner: &noWSRuneTypeScanner{
					inner: &runeTypeScannerImpl{
						inner: reader,
					},
				},
			}
		})
	})
	t.Run("child", func(t *testing.T) {
		t.Parallel()
		testRuneTypeScanner(t, childTestcases, func(reader io.RuneScanner) runeTypeScanner {
			inner := &noWSRuneTypeScanner{
				inner: &runeTypeScannerImpl{
					inner: reader,
				},
			}
			var res ReadRuneTypeResult
			res.r, res.s, res.t, res.e = inner.ReadRuneType()
			require.Equal(t,
				ReadRuneTypeResult{'[', 1, internal.RuneTypeArrayBeg, nil}.String(),
				res.String())

			return &elemRuneTypeScanner{
				inner: inner,
			}
		})
	})
}

func TestElemRuneTypeScanner2(t *testing.T) {
	t.Parallel()
	parent := &noWSRuneTypeScanner{
		inner: &runeTypeScannerImpl{
			inner: strings.NewReader(` { "foo" : 12.0 } `),
		},
	}
	exp := []ReadRuneTypeResult{
		{'{', 1, internal.RuneTypeObjectBeg, nil},
		{'"', 1, internal.RuneTypeStringBeg, nil},
		{'f', 1, internal.RuneTypeStringChar, nil},
		{'o', 1, internal.RuneTypeStringChar, nil},
		{'o', 1, internal.RuneTypeStringChar, nil},
		{'"', 1, internal.RuneTypeStringEnd, nil},
		{':', 1, internal.RuneTypeObjectColon, nil},
	}
	expStr := make([]string, 0, len(exp))
	actStr := make([]string, 0, len(exp))
	for _, iExp := range exp {
		var iAct ReadRuneTypeResult
		iAct.r, iAct.s, iAct.t, iAct.e = parent.ReadRuneType()
		expStr = append(expStr, iExp.String())
		actStr = append(actStr, iAct.String())
		require.Equal(t, expStr, actStr)
	}

	child := &elemRuneTypeScanner{
		inner: parent,
	}
	exp = []ReadRuneTypeResult{
		{'1', 1, internal.RuneTypeNumberIntDig, nil},
		{'2', 1, internal.RuneTypeNumberIntDig, nil},
		{'.', 1, internal.RuneTypeNumberFracDot, nil},
		{'0', 1, internal.RuneTypeNumberFracDig, nil},
		{0, 0, internal.RuneTypeEOF, nil},
		{0, 0, internal.RuneTypeEOF, nil},
	}
	expStr, actStr = nil, nil
	for _, iExp := range exp {
		var iAct ReadRuneTypeResult
		iAct.r, iAct.s, iAct.t, iAct.e = child.ReadRuneType()
		expStr = append(expStr, iExp.String())
		actStr = append(actStr, iAct.String())
		require.Equal(t, expStr, actStr)
	}

	exp = []ReadRuneTypeResult{
		{'}', 1, internal.RuneTypeObjectEnd, nil},
		{0, 0, internal.RuneTypeEOF, nil},
		{0, 0, internal.RuneTypeEOF, nil},
	}
	expStr, actStr = nil, nil
	for _, iExp := range exp {
		var iAct ReadRuneTypeResult
		iAct.r, iAct.s, iAct.t, iAct.e = parent.ReadRuneType()
		expStr = append(expStr, iExp.String())
		actStr = append(actStr, iAct.String())
		require.Equal(t, expStr, actStr)
	}
}