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

package json

import (
	"bytes"
	"reflect"
	"strings"
	"testing"

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

func TestCompatHTMLEscape(t *testing.T) {
	t.Parallel()
	type testcase struct {
		In  string
		Out string
	}
	testcases := map[string]testcase{
		"invalid":   {In: `x`, Out: `x`},
		"hex-lower": {In: `"\uabcd"`, Out: `"\uabcd"`},
		"hex-upper": {In: `"\uABCD"`, Out: `"\uABCD"`},
		"hex-mixed": {In: `"\uAbCd"`, Out: `"\uAbCd"`},
	}
	for tcName, tc := range testcases {
		tc := tc
		t.Run(tcName, func(t *testing.T) {
			t.Parallel()
			t.Logf("in=%q", tc.In)
			var dst bytes.Buffer
			HTMLEscape(&dst, []byte(tc.In))
			assert.Equal(t, tc.Out, dst.String())
		})
	}
}

func TestCompatValid(t *testing.T) {
	t.Parallel()
	type testcase struct {
		In  string
		Exp bool
	}
	testcases := map[string]testcase{
		"empty":     {In: ``, Exp: false},
		"num":       {In: `1`, Exp: true},
		"trunc":     {In: `{`, Exp: false},
		"object":    {In: `{}`, Exp: true},
		"non-utf8":  {In: "\"\x85\xcd\"", Exp: false}, // https://github.com/golang/go/issues/58517
		"hex-lower": {In: `"\uabcd"`, Exp: true},
		"hex-upper": {In: `"\uABCD"`, Exp: true},
		"hex-mixed": {In: `"\uAbCd"`, Exp: true},
	}
	for tcName, tc := range testcases {
		tc := tc
		t.Run(tcName, func(t *testing.T) {
			t.Parallel()
			t.Logf("in=%q", tc.In)
			act := Valid([]byte(tc.In))
			assert.Equal(t, tc.Exp, act)
		})
	}
}

func TestCompatCompact(t *testing.T) {
	t.Parallel()
	type testcase struct {
		In  string
		Out string
		Err string
	}
	testcases := map[string]testcase{
		"trunc":     {In: `{`, Out: ``, Err: `unexpected end of JSON input`},
		"object":    {In: `{}`, Out: `{}`},
		"non-utf8":  {In: "\"\x85\xcd\"", Out: "\"\x85\xcd\""},
		"float":     {In: `1.200e003`, Out: `1.200e003`},
		"hex-lower": {In: `"\uabcd"`, Out: `"\uabcd"`},
		"hex-upper": {In: `"\uABCD"`, Out: `"\uABCD"`},
		"hex-mixed": {In: `"\uAbCd"`, Out: `"\uAbCd"`},
	}
	for tcName, tc := range testcases {
		tc := tc
		t.Run(tcName, func(t *testing.T) {
			t.Parallel()
			t.Logf("in=%q", tc.In)
			var out bytes.Buffer
			err := Compact(&out, []byte(tc.In))
			assert.Equal(t, tc.Out, out.String())
			if tc.Err == "" {
				assert.NoError(t, err)
			} else {
				assert.EqualError(t, err, tc.Err)
			}
		})
	}
}

func TestCompatIndent(t *testing.T) {
	t.Parallel()
	type testcase struct {
		In  string
		Out string
		Err string
	}
	testcases := map[string]testcase{
		"trunc":     {In: `{`, Out: ``, Err: `unexpected end of JSON input`},
		"object":    {In: `{}`, Out: `{}`},
		"non-utf8":  {In: "\"\x85\xcd\"", Out: "\"\x85\xcd\""},
		"float":     {In: `1.200e003`, Out: `1.200e003`},
		"tailws0":   {In: `0`, Out: `0`},
		"tailws1":   {In: `0 `, Out: `0 `},
		"tailws2":   {In: `0  `, Out: `0  `},
		"tailws3":   {In: "0\n", Out: "0\n"},
		"headws1":   {In: ` 0`, Out: `0`},
		"objws1":    {In: `{"a"  :  1}`, Out: "{\n>.\"a\": 1\n>}"},
		"objws2":    {In: "{\"a\"\n:\n1}", Out: "{\n>.\"a\": 1\n>}"},
		"hex-lower": {In: `"\uabcd"`, Out: `"\uabcd"`},
		"hex-upper": {In: `"\uABCD"`, Out: `"\uABCD"`},
		"hex-mixed": {In: `"\uAbCd"`, Out: `"\uAbCd"`},
	}
	for tcName, tc := range testcases {
		tc := tc
		t.Run(tcName, func(t *testing.T) {
			t.Parallel()
			t.Logf("in=%q", tc.In)
			var out bytes.Buffer
			err := Indent(&out, []byte(tc.In), ">", ".")
			assert.Equal(t, tc.Out, out.String())
			if tc.Err == "" {
				assert.NoError(t, err)
			} else {
				assert.EqualError(t, err, tc.Err)
			}
		})
	}
}

func TestCompatMarshal(t *testing.T) {
	t.Parallel()
	type testcase struct {
		In  any
		Out string
		Err string
	}
	testcases := map[string]testcase{
		"non-utf8": {In: "\x85\xcd", Out: "\"\\ufffd\\ufffd\""},
		"urc":      {In: "\ufffd", Out: "\"\ufffd\""},
		"float":    {In: 1.2e3, Out: `1200`},
	}
	for tcName, tc := range testcases {
		tc := tc
		t.Run(tcName, func(t *testing.T) {
			t.Parallel()
			out, err := Marshal(tc.In)
			assert.Equal(t, tc.Out, string(out))
			if tc.Err == "" {
				assert.NoError(t, err)
			} else {
				assert.EqualError(t, err, tc.Err)
			}
		})
	}
}

func TestCompatUnmarshal(t *testing.T) {
	t.Parallel()
	type testcase struct {
		In     string
		InPtr  any
		ExpOut any
		ExpErr string
	}
	testcases := map[string]testcase{
		"empty-obj":            {In: `{}`, ExpOut: map[string]any{}},
		"partial-obj":          {In: `{"foo":"bar",`, ExpOut: nil, ExpErr: `unexpected end of JSON input`},
		"existing-obj":         {In: `{"baz":"quz"}`, InPtr: &map[string]string{"foo": "bar"}, ExpOut: map[string]string{"foo": "bar", "baz": "quz"}},
		"existing-obj-partial": {In: `{"baz":"quz"`, InPtr: &map[string]string{"foo": "bar"}, ExpOut: map[string]string{"foo": "bar"}, ExpErr: "unexpected end of JSON input"},
		"empty-ary":            {In: `[]`, ExpOut: []any{}},
		"two-objs":             {In: `{} {}`, ExpOut: nil, ExpErr: `invalid character '{' after top-level value`},
		"two-numbers1":         {In: `00`, ExpOut: nil, ExpErr: `invalid character '0' after top-level value`},
		"two-numbers2":         {In: `1 2`, ExpOut: nil, ExpErr: `invalid character '2' after top-level value`},
		// 2e308 is slightly more than math.MaxFloat64 (~1.79e308)
		"obj-overflow":      {In: `{"foo":"bar", "baz":2e308, "qux": "orb"}`, ExpOut: map[string]any{"foo": "bar", "baz": nil, "qux": "orb"}, ExpErr: `json: cannot unmarshal number 2e308 into Go value of type float64`},
		"ary-overflow":      {In: `["foo",2e308,"bar",3e308]`, ExpOut: []any{"foo", nil, "bar", nil}, ExpErr: `json: cannot unmarshal number 2e308 into Go value of type float64`},
		"existing-overflow": {In: `2e308`, InPtr: func() any { x := 4; return &x }(), ExpOut: 4, ExpErr: `json: cannot unmarshal number 2e308 into Go value of type int`},
	}
	for tcName, tc := range testcases {
		tc := tc
		t.Run(tcName, func(t *testing.T) {
			t.Parallel()
			ptr := tc.InPtr
			if ptr == nil {
				var out any
				ptr = &out
			}
			err := Unmarshal([]byte(tc.In), ptr)
			assert.Equal(t, tc.ExpOut, reflect.ValueOf(ptr).Elem().Interface())
			if tc.ExpErr == "" {
				assert.NoError(t, err)
			} else {
				assert.EqualError(t, err, tc.ExpErr)
			}
		})
	}
}

func TestCompatDecode(t *testing.T) {
	t.Parallel()
	type testcase struct {
		In     string
		InPtr  any
		ExpOut any
		ExpErr string
	}
	testcases := map[string]testcase{
		"empty-obj":            {In: `{}`, ExpOut: map[string]any{}},
		"partial-obj":          {In: `{"foo":"bar",`, ExpOut: nil, ExpErr: `unexpected EOF`},
		"existing-obj":         {In: `{"baz":"quz"}`, InPtr: &map[string]string{"foo": "bar"}, ExpOut: map[string]string{"foo": "bar", "baz": "quz"}},
		"existing-obj-partial": {In: `{"baz":"quz"`, InPtr: &map[string]string{"foo": "bar"}, ExpOut: map[string]string{"foo": "bar"}, ExpErr: "unexpected EOF"},
		"empty-ary":            {In: `[]`, ExpOut: []any{}},
		"two-objs":             {In: `{} {}`, ExpOut: map[string]any{}},
		"two-numbers1":         {In: `00`, ExpOut: float64(0)},
		"two-numbers2":         {In: `1 2`, ExpOut: float64(1)},
		// 2e308 is slightly more than math.MaxFloat64 (~1.79e308)
		"obj-overflow":      {In: `{"foo":"bar", "baz":2e308, "qux": "orb"}`, ExpOut: map[string]any{"foo": "bar", "baz": nil, "qux": "orb"}, ExpErr: `json: cannot unmarshal number 2e308 into Go value of type float64`},
		"ary-overflow":      {In: `["foo",2e308,"bar",3e308]`, ExpOut: []any{"foo", nil, "bar", nil}, ExpErr: `json: cannot unmarshal number 2e308 into Go value of type float64`},
		"existing-overflow": {In: `2e308`, InPtr: func() any { x := 4; return &x }(), ExpOut: 4, ExpErr: `json: cannot unmarshal number 2e308 into Go value of type int`},
	}
	for tcName, tc := range testcases {
		tc := tc
		t.Run(tcName, func(t *testing.T) {
			t.Parallel()
			ptr := tc.InPtr
			if ptr == nil {
				var out any
				ptr = &out
			}
			err := NewDecoder(strings.NewReader(tc.In)).Decode(ptr)
			assert.Equal(t, tc.ExpOut, reflect.ValueOf(ptr).Elem().Interface())
			if tc.ExpErr == "" {
				assert.NoError(t, err)
			} else {
				assert.EqualError(t, err, tc.ExpErr)
			}
		})
	}
}