package yaml

import (
	"encoding/json"
	"fmt"
	"math"
	"reflect"
	"sort"
	"strconv"
	"testing"

	"github.com/davecgh/go-spew/spew"
	yaml "gopkg.in/yaml.v2"
)

type MarshalTest struct {
	A string
	B int64
	// Would like to test float64, but it's not supported in go-yaml.
	// (See https://github.com/go-yaml/yaml/issues/83.)
	C float32
}

func TestMarshal(t *testing.T) {
	f32String := strconv.FormatFloat(math.MaxFloat32, 'g', -1, 32)
	s := MarshalTest{"a", math.MaxInt64, math.MaxFloat32}
	e := []byte(fmt.Sprintf("A: a\nB: %d\nC: %s\n", math.MaxInt64, f32String))

	y, err := Marshal(s)
	if err != nil {
		t.Errorf("error marshaling YAML: %v", err)
	}

	if !reflect.DeepEqual(y, e) {
		t.Errorf("marshal YAML was unsuccessful, expected: %#v, got: %#v",
			string(e), string(y))
	}
}

type UnmarshalString struct {
	A    string
	True string
}

type UnmarshalStringMap struct {
	A map[string]string
}

type UnmarshalNestedString struct {
	A NestedString
}

type NestedString struct {
	A string
}

type UnmarshalSlice struct {
	A []NestedSlice
}

type NestedSlice struct {
	B string
	C *string
}

func TestUnmarshal(t *testing.T) {
	y := []byte("a: 1")
	s1 := UnmarshalString{}
	e1 := UnmarshalString{A: "1"}
	unmarshal(t, y, &s1, &e1)

	y = []byte("a: true")
	s1 = UnmarshalString{}
	e1 = UnmarshalString{A: "true"}
	unmarshal(t, y, &s1, &e1)

	y = []byte("true: 1")
	s1 = UnmarshalString{}
	e1 = UnmarshalString{True: "1"}
	unmarshal(t, y, &s1, &e1)

	y = []byte("a:\n  a: 1")
	s2 := UnmarshalNestedString{}
	e2 := UnmarshalNestedString{NestedString{"1"}}
	unmarshal(t, y, &s2, &e2)

	y = []byte("a:\n  - b: abc\n    c: def\n  - b: 123\n    c: 456\n")
	s3 := UnmarshalSlice{}
	e3 := UnmarshalSlice{[]NestedSlice{NestedSlice{"abc", strPtr("def")}, NestedSlice{"123", strPtr("456")}}}
	unmarshal(t, y, &s3, &e3)

	y = []byte("a:\n  b: 1")
	s4 := UnmarshalStringMap{}
	e4 := UnmarshalStringMap{map[string]string{"b": "1"}}
	unmarshal(t, y, &s4, &e4)

	y = []byte(`
a:
  name: TestA
b:
  name: TestB
`)
	type NamedThing struct {
		Name string `json:"name"`
	}
	s5 := map[string]*NamedThing{}
	e5 := map[string]*NamedThing{
		"a": &NamedThing{Name: "TestA"},
		"b": &NamedThing{Name: "TestB"},
	}
	unmarshal(t, y, &s5, &e5)
}

func unmarshal(t *testing.T, y []byte, s, e interface{}, opts ...JSONOpt) {
	err := Unmarshal(y, s, opts...)
	if err != nil {
		t.Errorf("error unmarshaling YAML: %v", err)
	}

	if !reflect.DeepEqual(s, e) {
		t.Errorf("unmarshal YAML was unsuccessful, expected: %+#v, got: %+#v",
			e, s)
	}
}

func TestUnmarshalStrict(t *testing.T) {
	y := []byte("a: 1")
	s1 := UnmarshalString{}
	e1 := UnmarshalString{A: "1"}
	unmarshalStrict(t, y, &s1, &e1)

	y = []byte("a: true")
	s1 = UnmarshalString{}
	e1 = UnmarshalString{A: "true"}
	unmarshalStrict(t, y, &s1, &e1)

	y = []byte("true: 1")
	s1 = UnmarshalString{}
	e1 = UnmarshalString{True: "1"}
	unmarshalStrict(t, y, &s1, &e1)

	y = []byte("a:\n  a: 1")
	s2 := UnmarshalNestedString{}
	e2 := UnmarshalNestedString{NestedString{"1"}}
	unmarshalStrict(t, y, &s2, &e2)

	y = []byte("a:\n  - b: abc\n    c: def\n  - b: 123\n    c: 456\n")
	s3 := UnmarshalSlice{}
	e3 := UnmarshalSlice{[]NestedSlice{NestedSlice{"abc", strPtr("def")}, NestedSlice{"123", strPtr("456")}}}
	unmarshalStrict(t, y, &s3, &e3)

	y = []byte("a:\n  b: 1")
	s4 := UnmarshalStringMap{}
	e4 := UnmarshalStringMap{map[string]string{"b": "1"}}
	unmarshalStrict(t, y, &s4, &e4)

	y = []byte(`
a:
  name: TestA
b:
  name: TestB
`)
	type NamedThing struct {
		Name string `json:"name"`
	}
	s5 := map[string]*NamedThing{}
	e5 := map[string]*NamedThing{
		"a": &NamedThing{Name: "TestA"},
		"b": &NamedThing{Name: "TestB"},
	}
	unmarshal(t, y, &s5, &e5)

	// When using not-so-strict unmarshal, we should
	// be picking up the ID-1 as the value in the "id" field
	y = []byte(`
a:
  name: TestA
  id: ID-A
  id: ID-1
`)
	type NamedThing2 struct {
		Name string `json:"name"`
		ID   string `json:"id"`
	}
	s6 := map[string]*NamedThing2{}
	e6 := map[string]*NamedThing2{
		"a": {Name: "TestA", ID: "ID-1"},
	}
	unmarshal(t, y, &s6, &e6)
}

func TestUnmarshalStrictFails(t *testing.T) {
	y := []byte("a: true\na: false")
	s1 := UnmarshalString{}
	unmarshalStrictFail(t, y, &s1)

	y = []byte("a:\n  - b: abc\n    c: 32\n      b: 123")
	s2 := UnmarshalSlice{}
	unmarshalStrictFail(t, y, &s2)

	y = []byte("a:\n  b: 1\n    c: 3")
	s3 := UnmarshalStringMap{}
	unmarshalStrictFail(t, y, &s3)

	type NamedThing struct {
		Name string `json:"name"`
		ID   string `json:"id"`
	}
	// When using strict unmarshal, we should see
	// the unmarshal fail if there are multiple keys
	y = []byte(`
a:
  name: TestA
  id: ID-A
  id: ID-1
`)
	s4 := NamedThing{}
	unmarshalStrictFail(t, y, &s4)

	// Strict unmarshal should fail for unknown fields
	y = []byte(`
name: TestB
id: ID-B
unknown: Some-Value
`)
	s5 := NamedThing{}
	unmarshalStrictFail(t, y, &s5)
}

func unmarshalStrict(t *testing.T, y []byte, s, e interface{}, opts ...JSONOpt) {
	err := UnmarshalStrict(y, s, opts...)
	if err != nil {
		t.Errorf("error unmarshaling YAML: %v", err)
	}

	if !reflect.DeepEqual(s, e) {
		t.Errorf("unmarshal YAML was unsuccessful, expected: %+#v, got: %+#v",
			e, s)
	}
}

func unmarshalStrictFail(t *testing.T, y []byte, s interface{}, opts ...JSONOpt) {
	err := UnmarshalStrict(y, s, opts...)
	if err == nil {
		t.Errorf("error unmarshaling YAML: %v", err)
	}
}

type Case struct {
	input  string
	output string
	// By default we test that reversing the output == input. But if there is a
	// difference in the reversed output, you can optionally specify it here.
	reverse *string
}

type RunType int

const (
	RunTypeJSONToYAML RunType = iota
	RunTypeYAMLToJSON
)

func TestJSONToYAML(t *testing.T) {
	cases := []Case{
		{
			`{"t":"a"}`,
			"t: a\n",
			nil,
		}, {
			`{"t":null}`,
			"t: null\n",
			nil,
		}, {
			`{"t":"this is very long line with spaces and it must be longer than 80 so we will repeat that it must be longer that 80"}`,
			"t: this is very long line with spaces and it must be longer than 80 so we will repeat\n  that it must be longer that 80\n",
			nil,
		},
	}

	runCases(t, RunTypeJSONToYAML, cases)
}

func TestYAMLToJSON(t *testing.T) {
	cases := []Case{
		{
			"t: a\n",
			`{"t":"a"}`,
			nil,
		}, {
			"t: \n",
			`{"t":null}`,
			strPtr("t: null\n"),
		}, {
			"t: null\n",
			`{"t":null}`,
			nil,
		}, {
			"1: a\n",
			`{"1":"a"}`,
			strPtr("\"1\": a\n"),
		}, {
			"1000000000000000000000000000000000000: a\n",
			`{"1e+36":"a"}`,
			strPtr("\"1e+36\": a\n"),
		}, {
			"1e+36: a\n",
			`{"1e+36":"a"}`,
			strPtr("\"1e+36\": a\n"),
		}, {
			"\"1e+36\": a\n",
			`{"1e+36":"a"}`,
			nil,
		}, {
			"\"1.2\": a\n",
			`{"1.2":"a"}`,
			nil,
		}, {
			"- t: a\n",
			`[{"t":"a"}]`,
			nil,
		}, {
			"- t: a\n" +
				"- t:\n" +
				"    b: 1\n" +
				"    c: 2\n",
			`[{"t":"a"},{"t":{"b":1,"c":2}}]`,
			nil,
		}, {
			`[{t: a}, {t: {b: 1, c: 2}}]`,
			`[{"t":"a"},{"t":{"b":1,"c":2}}]`,
			strPtr("- t: a\n" +
				"- t:\n" +
				"    b: 1\n" +
				"    c: 2\n"),
		}, {
			"- t: \n",
			`[{"t":null}]`,
			strPtr("- t: null\n"),
		}, {
			"- t: null\n",
			`[{"t":null}]`,
			nil,
		},
	}

	// Cases that should produce errors.
	_ = []Case{
		{
			"~: a",
			`{"null":"a"}`,
			nil,
		}, {
			"a: !!binary gIGC\n",
			"{\"a\":\"\x80\x81\x82\"}",
			nil,
		},
	}

	runCases(t, RunTypeYAMLToJSON, cases)
}

func runCases(t *testing.T, runType RunType, cases []Case) {
	var f func([]byte) ([]byte, error)
	var invF func([]byte) ([]byte, error)
	var msg string
	var invMsg string
	if runType == RunTypeJSONToYAML {
		f = JSONToYAML
		invF = YAMLToJSON
		msg = "JSON to YAML"
		invMsg = "YAML back to JSON"
	} else {
		f = YAMLToJSON
		invF = JSONToYAML
		msg = "YAML to JSON"
		invMsg = "JSON back to YAML"
	}

	for _, c := range cases {
		// Convert the string.
		t.Logf("converting %s\n", c.input)
		output, err := f([]byte(c.input))
		if err != nil {
			t.Errorf("Failed to convert %s, input: `%s`, err: %v", msg, c.input, err)
		}

		// Check it against the expected output.
		if string(output) != c.output {
			t.Errorf("Failed to convert %s, input: `%s`, expected `%s`, got `%s`",
				msg, c.input, c.output, string(output))
		}

		// Set the string that we will compare the reversed output to.
		reverse := c.input
		// If a special reverse string was specified, use that instead.
		if c.reverse != nil {
			reverse = *c.reverse
		}

		// Reverse the output.
		input, err := invF(output)
		if err != nil {
			t.Errorf("Failed to convert %s, input: `%s`, err: %v", invMsg, string(output), err)
		}

		// Check the reverse is equal to the input (or to *c.reverse).
		if string(input) != reverse {
			t.Errorf("Failed to convert %s, input: `%s`, expected `%s`, got `%s`",
				invMsg, string(output), reverse, string(input))
		}
	}

}

// To be able to easily fill in the *Case.reverse string above.
func strPtr(s string) *string {
	return &s
}

func TestYAMLToJSONStrict(t *testing.T) {
	const data = `
foo: bar
foo: baz
`
	if _, err := YAMLToJSON([]byte(data)); err != nil {
		t.Error("expected YAMLtoJSON to pass on duplicate field names")
	}
	if _, err := YAMLToJSONStrict([]byte(data)); err == nil {
		t.Error("expected YAMLtoJSONStrict to fail on duplicate field names")
	}
}

func TestJSONObjectToYAMLObject(t *testing.T) {
	const bigUint64 = ((uint64(1) << 63) + 500) / 1000 * 1000
	intOrInt64 := func(i64 int64) interface{} {
		if i := int(i64); i64 == int64(i) {
			return i
		}
		return i64
	}

	tests := []struct {
		name     string
		input    map[string]interface{}
		expected yaml.MapSlice
	}{
		{name: "nil", expected: yaml.MapSlice(nil)},
		{name: "empty", input: map[string]interface{}{}, expected: yaml.MapSlice(nil)},
		{
			name: "values",
			input: map[string]interface{}{
				"nil slice":          []interface{}(nil),
				"nil map":            map[string]interface{}(nil),
				"empty slice":        []interface{}{},
				"empty map":          map[string]interface{}{},
				"bool":               true,
				"float64":            float64(42.1),
				"fractionless":       float64(42),
				"int":                int(42),
				"int64":              int64(42),
				"int64 big":          float64(math.Pow(2, 62)),
				"negative int64 big": -float64(math.Pow(2, 62)),
				"map":                map[string]interface{}{"foo": "bar"},
				"slice":              []interface{}{"foo", "bar"},
				"string":             string("foo"),
				"uint64 big":         bigUint64,
			},
			expected: yaml.MapSlice{
				{Key: "nil slice"},
				{Key: "nil map"},
				{Key: "empty slice", Value: []interface{}{}},
				{Key: "empty map", Value: yaml.MapSlice(nil)},
				{Key: "bool", Value: true},
				{Key: "float64", Value: float64(42.1)},
				{Key: "fractionless", Value: int(42)},
				{Key: "int", Value: int(42)},
				{Key: "int64", Value: int(42)},
				{Key: "int64 big", Value: intOrInt64(int64(1) << 62)},
				{Key: "negative int64 big", Value: intOrInt64(-(1 << 62))},
				{Key: "map", Value: yaml.MapSlice{{Key: "foo", Value: "bar"}}},
				{Key: "slice", Value: []interface{}{"foo", "bar"}},
				{Key: "string", Value: string("foo")},
				{Key: "uint64 big", Value: bigUint64},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := JSONObjectToYAMLObject(tt.input)
			sortMapSlicesInPlace(tt.expected)
			sortMapSlicesInPlace(got)
			if !reflect.DeepEqual(got, tt.expected) {
				t.Errorf("jsonToYAML() = %v, want %v", spew.Sdump(got), spew.Sdump(tt.expected))
			}

			jsonBytes, err := json.Marshal(tt.input)
			if err != nil {
				t.Fatalf("unexpected json.Marshal error: %v", err)
			}
			var gotByRoundtrip yaml.MapSlice
			if err := yaml.Unmarshal(jsonBytes, &gotByRoundtrip); err != nil {
				t.Fatalf("unexpected yaml.Unmarshal error: %v", err)
			}

			// yaml.Unmarshal loses precision, it's rounding to the 4th last digit.
			// Replicate this here in the test, but don't change the type.
			for i := range got {
				switch got[i].Key {
				case "int64 big", "uint64 big", "negative int64 big":
					switch v := got[i].Value.(type) {
					case int64:
						d := int64(500)
						if v < 0 {
							d = -500
						}
						got[i].Value = int64((v+d)/1000) * 1000
					case uint64:
						got[i].Value = uint64((v+500)/1000) * 1000
					case int:
						d := int(500)
						if v < 0 {
							d = -500
						}
						got[i].Value = int((v+d)/1000) * 1000
					default:
						t.Fatalf("unexpected type for key %s: %v:%T", got[i].Key, v, v)
					}
				}
			}

			if !reflect.DeepEqual(got, gotByRoundtrip) {
				t.Errorf("yaml.Unmarshal(json.Marshal(tt.input)) = %v, want %v\njson: %s", spew.Sdump(gotByRoundtrip), spew.Sdump(got), string(jsonBytes))
			}
		})
	}
}

func sortMapSlicesInPlace(x interface{}) {
	switch x := x.(type) {
	case []interface{}:
		for i := range x {
			sortMapSlicesInPlace(x[i])
		}
	case yaml.MapSlice:
		sort.Slice(x, func(a, b int) bool {
			return x[a].Key.(string) < x[b].Key.(string)
		})
	}
}
