// 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 json_test

import (
	"math"
	"strings"
	"testing"

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

	"google.golang.org/protobuf/internal/detrand"
	"google.golang.org/protobuf/internal/encoding/json"
)

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

// splitLines is a cmpopts.Option for comparing strings with line breaks.
var splitLines = cmpopts.AcyclicTransformer("SplitLines", func(s string) []string {
	return strings.Split(s, "\n")
})

func TestEncoder(t *testing.T) {
	tests := []struct {
		desc          string
		write         func(*json.Encoder)
		wantOut       string
		wantOutIndent string
	}{
		{
			desc: "null",
			write: func(e *json.Encoder) {
				e.WriteNull()
			},
			wantOut: `null`,
		},
		{
			desc: "true",
			write: func(e *json.Encoder) {
				e.WriteBool(true)
			},
			wantOut: `true`,
		},
		{
			desc: "false",
			write: func(e *json.Encoder) {
				e.WriteBool(false)
			},
			wantOut: `false`,
		},
		{
			desc: "string",
			write: func(e *json.Encoder) {
				e.WriteString("hello world")
			},
			wantOut: `"hello world"`,
		},
		{
			desc: "string contains escaped characters",
			write: func(e *json.Encoder) {
				e.WriteString("\u0000\"\\/\b\f\n\r\t")
			},
			wantOut: `"\u0000\"\\/\b\f\n\r\t"`,
		},
		{
			desc: "float64",
			write: func(e *json.Encoder) {
				e.WriteFloat(1.0199999809265137, 64)
			},
			wantOut: `1.0199999809265137`,
		},
		{
			desc: "float64 max value",
			write: func(e *json.Encoder) {
				e.WriteFloat(math.MaxFloat64, 64)
			},
			wantOut: `1.7976931348623157e+308`,
		},
		{
			desc: "float64 min value",
			write: func(e *json.Encoder) {
				e.WriteFloat(-math.MaxFloat64, 64)
			},
			wantOut: `-1.7976931348623157e+308`,
		},
		{
			desc: "float64 NaN",
			write: func(e *json.Encoder) {
				e.WriteFloat(math.NaN(), 64)
			},
			wantOut: `"NaN"`,
		},
		{
			desc: "float64 Infinity",
			write: func(e *json.Encoder) {
				e.WriteFloat(math.Inf(+1), 64)
			},
			wantOut: `"Infinity"`,
		},
		{
			desc: "float64 -Infinity",
			write: func(e *json.Encoder) {
				e.WriteFloat(math.Inf(-1), 64)
			},
			wantOut: `"-Infinity"`,
		},
		{
			desc: "float64 negative zero",
			write: func(e *json.Encoder) {
				e.WriteFloat(math.Copysign(0, -1), 64)
			},
			wantOut: `-0`,
		},
		{
			desc: "float32",
			write: func(e *json.Encoder) {
				e.WriteFloat(1.02, 32)
			},
			wantOut: `1.02`,
		},
		{
			desc: "float32 max value",
			write: func(e *json.Encoder) {
				e.WriteFloat(math.MaxFloat32, 32)
			},
			wantOut: `3.4028235e+38`,
		},
		{
			desc: "float32 min value",
			write: func(e *json.Encoder) {
				e.WriteFloat(-math.MaxFloat32, 32)
			},
			wantOut: `-3.4028235e+38`,
		},
		{
			desc: "float32 negative zero",
			write: func(e *json.Encoder) {
				e.WriteFloat(math.Copysign(0, -1), 32)
			},
			wantOut: `-0`,
		},
		{
			desc: "int",
			write: func(e *json.Encoder) {
				e.WriteInt(-math.MaxInt64)
			},
			wantOut: `-9223372036854775807`,
		},
		{
			desc: "uint",
			write: func(e *json.Encoder) {
				e.WriteUint(math.MaxUint64)
			},
			wantOut: `18446744073709551615`,
		},
		{
			desc: "empty object",
			write: func(e *json.Encoder) {
				e.StartObject()
				e.EndObject()
			},
			wantOut: `{}`,
		},
		{
			desc: "empty array",
			write: func(e *json.Encoder) {
				e.StartArray()
				e.EndArray()
			},
			wantOut: `[]`,
		},
		{
			desc: "object with one member",
			write: func(e *json.Encoder) {
				e.StartObject()
				e.WriteName("hello")
				e.WriteString("world")
				e.EndObject()
			},
			wantOut: `{"hello":"world"}`,
			wantOutIndent: `{
	"hello": "world"
}`,
		},
		{
			desc: "array with one member",
			write: func(e *json.Encoder) {
				e.StartArray()
				e.WriteNull()
				e.EndArray()
			},
			wantOut: `[null]`,
			wantOutIndent: `[
	null
]`,
		},
		{
			desc: "simple object",
			write: func(e *json.Encoder) {
				e.StartObject()
				{
					e.WriteName("null")
					e.WriteNull()
				}
				{
					e.WriteName("bool")
					e.WriteBool(true)
				}
				{
					e.WriteName("string")
					e.WriteString("hello")
				}
				{
					e.WriteName("float")
					e.WriteFloat(6.28318, 64)
				}
				{
					e.WriteName("int")
					e.WriteInt(42)
				}
				{
					e.WriteName("uint")
					e.WriteUint(47)
				}
				e.EndObject()
			},
			wantOut: `{"null":null,"bool":true,"string":"hello","float":6.28318,"int":42,"uint":47}`,
			wantOutIndent: `{
	"null": null,
	"bool": true,
	"string": "hello",
	"float": 6.28318,
	"int": 42,
	"uint": 47
}`,
		},
		{
			desc: "simple array",
			write: func(e *json.Encoder) {
				e.StartArray()
				{
					e.WriteString("hello")
					e.WriteFloat(6.28318, 32)
					e.WriteInt(42)
					e.WriteUint(47)
					e.WriteBool(true)
					e.WriteNull()
				}
				e.EndArray()
			},
			wantOut: `["hello",6.28318,42,47,true,null]`,
			wantOutIndent: `[
	"hello",
	6.28318,
	42,
	47,
	true,
	null
]`,
		},
		{
			desc: "fancy object",
			write: func(e *json.Encoder) {
				e.StartObject()
				{
					e.WriteName("object0")
					e.StartObject()
					e.EndObject()
				}
				{
					e.WriteName("array0")
					e.StartArray()
					e.EndArray()
				}
				{
					e.WriteName("object1")
					e.StartObject()
					{
						e.WriteName("null")
						e.WriteNull()
					}
					{
						e.WriteName("object1-1")
						e.StartObject()
						{
							e.WriteName("bool")
							e.WriteBool(false)
						}
						{
							e.WriteName("float")
							e.WriteFloat(3.14159, 32)
						}
						e.EndObject()
					}
					e.EndObject()
				}
				{
					e.WriteName("array1")
					e.StartArray()
					{
						e.WriteNull()
						e.StartObject()
						e.EndObject()
						e.StartObject()
						{
							e.WriteName("hello")
							e.WriteString("world")
						}
						{
							e.WriteName("hola")
							e.WriteString("mundo")
						}
						e.EndObject()
						e.StartArray()
						{
							e.WriteUint(1)
							e.WriteUint(0)
							e.WriteUint(1)
						}
						e.EndArray()
					}
					e.EndArray()
				}
				e.EndObject()
			},
			wantOutIndent: `{
	"object0": {},
	"array0": [],
	"object1": {
		"null": null,
		"object1-1": {
			"bool": false,
			"float": 3.14159
		}
	},
	"array1": [
		null,
		{},
		{
			"hello": "world",
			"hola": "mundo"
		},
		[
			1,
			0,
			1
		]
	]
}`,
		}}

	for _, tc := range tests {
		t.Run(tc.desc, func(t *testing.T) {
			if tc.wantOut != "" {
				enc, err := json.NewEncoder("")
				if err != nil {
					t.Fatalf("NewEncoder() returned error: %v", err)
				}
				tc.write(enc)
				got := string(enc.Bytes())
				if got != tc.wantOut {
					t.Errorf("%s:\n<got>:\n%v\n<want>\n%v\n", tc.desc, got, tc.wantOut)
				}
			}
			if tc.wantOutIndent != "" {
				enc, err := json.NewEncoder("\t")
				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("%s(indent):\n<got>:\n%v\n<want>\n%v\n<diff -want +got>\n%v\n",
						tc.desc, got, want, cmp.Diff(want, got, splitLines))
				}
			}
		})
	}
}

func TestWriteStringError(t *testing.T) {
	tests := []string{"abc\xff"}

	for _, in := range tests {
		t.Run(in, func(t *testing.T) {
			enc, err := json.NewEncoder("")
			if err != nil {
				t.Fatalf("NewEncoder() returned error: %v", err)
			}
			if err := enc.WriteString(in); err == nil {
				t.Errorf("WriteString(%v): got nil error, want error", in)
			}
		})
	}
}
