// Copyright 2019 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package text_test

import (
	"math"
	"strings"
	"testing"
	"unicode/utf8"

	"github.com/google/go-cmp/cmp"

	"github.com/aperturerobotics/protobuf-go-lite/internal/detrand"
	"github.com/aperturerobotics/protobuf-go-lite/internal/encoding/text"
)

// Disable detrand to enable direct comparisons on outputs.
func init() { detrand.Disable() }

func TestEncoder(t *testing.T) {
	tests := []encoderTestCase{
		{
			desc:          "no-opt",
			write:         func(e *text.Encoder) {},
			wantOut:       ``,
			wantOutIndent: ``,
		},
		{
			desc: "true",
			write: func(e *text.Encoder) {
				e.WriteName("bool")
				e.WriteBool(true)
			},
			wantOut:       `bool:true`,
			wantOutIndent: `bool: true`,
		},
		{
			desc: "false",
			write: func(e *text.Encoder) {
				e.WriteName("bool")
				e.WriteBool(false)
			},
			wantOut:       `bool:false`,
			wantOutIndent: `bool: false`,
		},
		{
			desc: "bracket name",
			write: func(e *text.Encoder) {
				e.WriteName("[extension]")
				e.WriteString("hello")
			},
			wantOut:       `[extension]:"hello"`,
			wantOutIndent: `[extension]: "hello"`,
		},
		{
			desc: "numeric name",
			write: func(e *text.Encoder) {
				e.WriteName("01234")
				e.WriteString("hello")
			},
			wantOut:       `01234:"hello"`,
			wantOutIndent: `01234: "hello"`,
		},
		{
			desc: "string",
			write: func(e *text.Encoder) {
				e.WriteName("str")
				e.WriteString("hello world")
			},
			wantOut:       `str:"hello world"`,
			wantOutIndent: `str: "hello world"`,
		},
		{
			desc: "enum",
			write: func(e *text.Encoder) {
				e.WriteName("enum")
				e.WriteLiteral("ENUM_VALUE")
			},
			wantOut:       `enum:ENUM_VALUE`,
			wantOutIndent: `enum: ENUM_VALUE`,
		},
		{
			desc: "float64",
			write: func(e *text.Encoder) {
				e.WriteName("float64")
				e.WriteFloat(1.0199999809265137, 64)
			},
			wantOut:       `float64:1.0199999809265137`,
			wantOutIndent: `float64: 1.0199999809265137`,
		},
		{
			desc: "float64 max value",
			write: func(e *text.Encoder) {
				e.WriteName("float64")
				e.WriteFloat(math.MaxFloat64, 64)
			},
			wantOut:       `float64:1.7976931348623157e+308`,
			wantOutIndent: `float64: 1.7976931348623157e+308`,
		},
		{
			desc: "float64 min value",
			write: func(e *text.Encoder) {
				e.WriteName("float64")
				e.WriteFloat(-math.MaxFloat64, 64)
			},
			wantOut:       `float64:-1.7976931348623157e+308`,
			wantOutIndent: `float64: -1.7976931348623157e+308`,
		},
		{
			desc: "float64 nan",
			write: func(e *text.Encoder) {
				e.WriteName("float64")
				e.WriteFloat(math.NaN(), 64)
			},
			wantOut:       `float64:nan`,
			wantOutIndent: `float64: nan`,
		},
		{
			desc: "float64 inf",
			write: func(e *text.Encoder) {
				e.WriteName("float64")
				e.WriteFloat(math.Inf(+1), 64)
			},
			wantOut:       `float64:inf`,
			wantOutIndent: `float64: inf`,
		},
		{
			desc: "float64 -inf",
			write: func(e *text.Encoder) {
				e.WriteName("float64")
				e.WriteFloat(math.Inf(-1), 64)
			},
			wantOut:       `float64:-inf`,
			wantOutIndent: `float64: -inf`,
		},
		{
			desc: "float64 negative zero",
			write: func(e *text.Encoder) {
				e.WriteName("float64")
				e.WriteFloat(math.Copysign(0, -1), 64)
			},
			wantOut:       `float64:-0`,
			wantOutIndent: `float64: -0`,
		},
		{
			desc: "float32",
			write: func(e *text.Encoder) {
				e.WriteName("float")
				e.WriteFloat(1.02, 32)
			},
			wantOut:       `float:1.02`,
			wantOutIndent: `float: 1.02`,
		},
		{
			desc: "float32 max value",
			write: func(e *text.Encoder) {
				e.WriteName("float32")
				e.WriteFloat(math.MaxFloat32, 32)
			},
			wantOut:       `float32:3.4028235e+38`,
			wantOutIndent: `float32: 3.4028235e+38`,
		},
		{
			desc: "float32 nan",
			write: func(e *text.Encoder) {
				e.WriteName("float32")
				e.WriteFloat(math.NaN(), 32)
			},
			wantOut:       `float32:nan`,
			wantOutIndent: `float32: nan`,
		},
		{
			desc: "float32 inf",
			write: func(e *text.Encoder) {
				e.WriteName("float32")
				e.WriteFloat(math.Inf(+1), 32)
			},
			wantOut:       `float32:inf`,
			wantOutIndent: `float32: inf`,
		},
		{
			desc: "float32 -inf",
			write: func(e *text.Encoder) {
				e.WriteName("float32")
				e.WriteFloat(math.Inf(-1), 32)
			},
			wantOut:       `float32:-inf`,
			wantOutIndent: `float32: -inf`,
		},
		{
			desc: "float32 negative zero",
			write: func(e *text.Encoder) {
				e.WriteName("float32")
				e.WriteFloat(math.Copysign(0, -1), 32)
			},
			wantOut:       `float32:-0`,
			wantOutIndent: `float32: -0`,
		},
		{
			desc: "int64 max value",
			write: func(e *text.Encoder) {
				e.WriteName("int")
				e.WriteInt(math.MaxInt64)
			},
			wantOut:       `int:9223372036854775807`,
			wantOutIndent: `int: 9223372036854775807`,
		},
		{
			desc: "int64 min value",
			write: func(e *text.Encoder) {
				e.WriteName("int")
				e.WriteInt(math.MinInt64)
			},
			wantOut:       `int:-9223372036854775808`,
			wantOutIndent: `int: -9223372036854775808`,
		},
		{
			desc: "uint",
			write: func(e *text.Encoder) {
				e.WriteName("uint")
				e.WriteUint(math.MaxUint64)
			},
			wantOut:       `uint:18446744073709551615`,
			wantOutIndent: `uint: 18446744073709551615`,
		},
		{
			desc: "empty message field",
			write: func(e *text.Encoder) {
				e.WriteName("m")
				e.StartMessage()
				e.EndMessage()
			},
			wantOut:       `m:{}`,
			wantOutIndent: `m: {}`,
		},
		{
			desc: "multiple fields",
			write: func(e *text.Encoder) {
				e.WriteName("bool")
				e.WriteBool(true)
				e.WriteName("str")
				e.WriteString("hello")
				e.WriteName("str")
				e.WriteString("world")
				e.WriteName("m")
				e.StartMessage()
				e.EndMessage()
				e.WriteName("[int]")
				e.WriteInt(49)
				e.WriteName("float64")
				e.WriteFloat(1.00023e4, 64)
				e.WriteName("101")
				e.WriteString("unknown")
			},
			wantOut: `bool:true str:"hello" str:"world" m:{} [int]:49 float64:10002.3 101:"unknown"`,
			wantOutIndent: `bool: true
str: "hello"
str: "world"
m: {}
[int]: 49
float64: 10002.3
101: "unknown"`,
		},
		{
			desc: "populated message fields",
			write: func(e *text.Encoder) {
				e.WriteName("m1")
				e.StartMessage()
				{
					e.WriteName("str")
					e.WriteString("hello")
				}
				e.EndMessage()

				e.WriteName("bool")
				e.WriteBool(true)

				e.WriteName("m2")
				e.StartMessage()
				{
					e.WriteName("str")
					e.WriteString("world")
					e.WriteName("m2-1")
					e.StartMessage()
					e.EndMessage()
					e.WriteName("m2-2")
					e.StartMessage()
					{
						e.WriteName("[int]")
						e.WriteInt(49)
					}
					e.EndMessage()
					e.WriteName("float64")
					e.WriteFloat(1.00023e4, 64)
				}
				e.EndMessage()

				e.WriteName("101")
				e.WriteString("unknown")
			},
			wantOut: `m1:{str:"hello"} bool:true m2:{str:"world" m2-1:{} m2-2:{[int]:49} float64:10002.3} 101:"unknown"`,
			wantOutIndent: `m1: {
	str: "hello"
}
bool: true
m2: {
	str: "world"
	m2-1: {}
	m2-2: {
		[int]: 49
	}
	float64: 10002.3
}
101: "unknown"`,
		},
	}

	for _, tc := range tests {
		t.Run(tc.desc, func(t *testing.T) {
			runEncoderTest(t, tc, [2]byte{})

			// Test using the angle brackets.
			// Testcases should not contain characters '{' and '}'.
			tc.wantOut = replaceDelims(tc.wantOut)
			tc.wantOutIndent = replaceDelims(tc.wantOutIndent)
			runEncoderTest(t, tc, [2]byte{'<', '>'})
		})
	}
}

type encoderTestCase struct {
	desc          string
	write         func(*text.Encoder)
	wantOut       string
	wantOutIndent string
}

func runEncoderTest(t *testing.T, tc encoderTestCase, delims [2]byte) {
	t.Helper()

	if tc.wantOut != "" {
		enc, err := text.NewEncoder(nil, "", delims, false)
		if err != nil {
			t.Fatalf("NewEncoder returned error: %v", err)
		}
		tc.write(enc)
		got := string(enc.Bytes())
		if got != tc.wantOut {
			t.Errorf("(compact)\n<got>\n%v\n<want>\n%v\n", got, tc.wantOut)
		}
	}
	if tc.wantOutIndent != "" {
		enc, err := text.NewEncoder(nil, "\t", delims, false)
		if err != nil {
			t.Fatalf("NewEncoder returned error: %v", err)
		}
		tc.write(enc)
		got, want := string(enc.Bytes()), tc.wantOutIndent
		if got != want {
			t.Errorf("(multi-line)\n<got>\n%v\n<want>\n%v\n<diff -want +got>\n%v\n",
				got, want, cmp.Diff(want, got))
		}
	}
}

func replaceDelims(s string) string {
	s = strings.Replace(s, "{", "<", -1)
	return strings.Replace(s, "}", ">", -1)
}

// Test for UTF-8 and ASCII outputs.
func TestEncodeStrings(t *testing.T) {
	tests := []struct {
		in           string
		wantOut      string
		wantOutASCII string
	}{
		{
			in:      `"`,
			wantOut: `"\""`,
		},
		{
			in:      `'`,
			wantOut: `"'"`,
		},
		{
			in:           "hello\u1234world",
			wantOut:      "\"hello\u1234world\"",
			wantOutASCII: `"hello\u1234world"`,
		},
		{
			// String that has as few escaped characters as possible.
			in: func() string {
				var b []byte
				for i := rune(0); i <= 0x00a0; i++ {
					switch i {
					case 0, '\\', '\n', '\'': // these must be escaped, so ignore them
					default:
						var r [utf8.UTFMax]byte
						n := utf8.EncodeRune(r[:], i)
						b = append(b, r[:n]...)
					}
				}
				return string(b)
			}(),
			wantOut:      `"\x01\x02\x03\x04\x05\x06\x07\x08\t\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !\"#$%&()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_` + "`" + `abcdefghijklmnopqrstuvwxyz{|}~\x7f\u0080\u0081\u0082\u0083\u0084\u0085\u0086\u0087\u0088\u0089\u008a\u008b\u008c\u008d\u008e\u008f\u0090\u0091\u0092\u0093\u0094\u0095\u0096\u0097\u0098\u0099\u009a\u009b\u009c\u009d\u009e\u009f` + "\u00a0" + `"`,
			wantOutASCII: `"\x01\x02\x03\x04\x05\x06\x07\x08\t\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !\"#$%&()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_` + "`" + `abcdefghijklmnopqrstuvwxyz{|}~\x7f\u0080\u0081\u0082\u0083\u0084\u0085\u0086\u0087\u0088\u0089\u008a\u008b\u008c\u008d\u008e\u008f\u0090\u0091\u0092\u0093\u0094\u0095\u0096\u0097\u0098\u0099\u009a\u009b\u009c\u009d\u009e\u009f\u00a0"`,
		},
		{
			// Valid UTF-8 wire encoding of the RuneError rune.
			in:           string(utf8.RuneError),
			wantOut:      `"` + string(utf8.RuneError) + `"`,
			wantOutASCII: `"\ufffd"`,
		},
		{
			in:           "\"'\\?\a\b\n\r\t\v\f\x01\nS\n\xab\x12\uab8f\U0010ffff",
			wantOut:      `"\"'\\?\x07\x08\n\r\t\x0b\x0c\x01\nS\n\xab\x12` + "\uab8f\U0010ffff" + `"`,
			wantOutASCII: `"\"'\\?\x07\x08\n\r\t\x0b\x0c\x01\nS\n\xab\x12\uab8f\U0010ffff"`,
		},
		{
			in:           "\001x",
			wantOut:      `"\x01x"`,
			wantOutASCII: `"\x01x"`,
		},
		{
			in:           "\012x",
			wantOut:      `"\nx"`,
			wantOutASCII: `"\nx"`,
		},
		{
			in:           "\123x",
			wantOut:      `"Sx"`,
			wantOutASCII: `"Sx"`,
		},
		{
			in:           "\1234x",
			wantOut:      `"S4x"`,
			wantOutASCII: `"S4x"`,
		},
		{
			in:           "\001",
			wantOut:      `"\x01"`,
			wantOutASCII: `"\x01"`,
		},
		{
			in:           "\012",
			wantOut:      `"\n"`,
			wantOutASCII: `"\n"`,
		},
		{
			in:           "\123",
			wantOut:      `"S"`,
			wantOutASCII: `"S"`,
		},
		{
			in:           "\1234",
			wantOut:      `"S4"`,
			wantOutASCII: `"S4"`,
		},
		{
			in:           "\377",
			wantOut:      `"\xff"`,
			wantOutASCII: `"\xff"`,
		},
		{
			in:           "\x0fx",
			wantOut:      `"\x0fx"`,
			wantOutASCII: `"\x0fx"`,
		},
		{
			in:           "\xffx",
			wantOut:      `"\xffx"`,
			wantOutASCII: `"\xffx"`,
		},
		{
			in:           "\xfffx",
			wantOut:      `"\xfffx"`,
			wantOutASCII: `"\xfffx"`,
		},
		{
			in:           "\x0f",
			wantOut:      `"\x0f"`,
			wantOutASCII: `"\x0f"`,
		},
		{
			in:           "\x7f",
			wantOut:      `"\x7f"`,
			wantOutASCII: `"\x7f"`,
		},
		{
			in:           "\xff",
			wantOut:      `"\xff"`,
			wantOutASCII: `"\xff"`,
		},
		{
			in:           "\xfff",
			wantOut:      `"\xfff"`,
			wantOutASCII: `"\xfff"`,
		},
	}
	for _, tc := range tests {
		t.Run("", func(t *testing.T) {
			if tc.wantOut != "" {
				runEncodeStringsTest(t, tc.in, tc.wantOut, false)
			}
			if tc.wantOutASCII != "" {
				runEncodeStringsTest(t, tc.in, tc.wantOutASCII, true)
			}
		})
	}
}

func runEncodeStringsTest(t *testing.T, in string, want string, outputASCII bool) {
	t.Helper()

	charType := "UTF-8"
	if outputASCII {
		charType = "ASCII"
	}

	enc, err := text.NewEncoder(nil, "", [2]byte{}, outputASCII)
	if err != nil {
		t.Fatalf("[%s] NewEncoder returned error: %v", charType, err)
	}
	enc.WriteString(in)
	got := string(enc.Bytes())
	if got != want {
		t.Errorf("[%s] WriteString(%q)\n<got>\n%v\n<want>\n%v\n", charType, in, got, want)
	}
}

func TestReset(t *testing.T) {
	enc, err := text.NewEncoder(nil, "\t", [2]byte{}, false)
	if err != nil {
		t.Fatalf("NewEncoder returned error: %v", err)
	}

	enc.WriteName("foo")
	pos := enc.Snapshot()

	// Attempt to write a message value.
	enc.StartMessage()
	enc.WriteName("bar")
	enc.WriteUint(10)

	// Reset the value and decided to write a string value instead.
	enc.Reset(pos)
	enc.WriteString("0123456789")

	got := string(enc.Bytes())
	want := `foo: "0123456789"`
	if got != want {
		t.Errorf("Reset did not restore given position:\n<got>\n%v\n<want>\n%v\n", got, want)
	}
}
