package yaml

import (
	"bytes"
	"testing"

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

func TestYAMLMarshalError(t *testing.T) {
	t.Parallel()

	type yamlMarshalError struct {
		BoolValue bool   `yaml:"bool_value"`
		FuncValue func() `yaml:"func_value"`
	}

	v := yamlMarshalError{
		BoolValue: true,
		FuncValue: func() {},
	}

	require.Panics(t, func() {
		_, _ = Marshal(v)
	})
}

func TestYAML(t *testing.T) {
	t.Parallel()

	type (
		yamlKey struct {
			BoolValue   bool    `yaml:"bool_value"`
			NumberValue float64 `yaml:"number_value"`
			StringValue string  `yaml:"string_value"`
		}
		yamlObject struct {
			Array []string `yaml:"array"`
			Key   yamlKey  `yaml:"key"`
		}
		yamlReceiver struct {
			Object yamlObject `yaml:"object"`
		}

		interfaceOrObject struct {
			obj   yamlReceiver
			iface any
		}
	)

	for _, toPin := range []struct {
		Title        string
		InputYAML    []byte
		ExpectedYAML []byte            // optional: when marshaled YAML is expected to differ from raw input (e.g. on bool flags)
		Expected     interfaceOrObject // maybe either untyped interface{} or yamlObject struct with struct tags
		ExpectError  bool
		WantsStrict  bool // apply Strict mode
	}{
		{
			Title:     "happy path, untyped",
			InputYAML: testYAMLObject(),
			Expected: interfaceOrObject{
				iface: map[string]any{
					"object": map[string]any{
						"key": map[string]any{
							"string_value": "This is a doc.\nOn multiple lines.\n",
							"bool_value":   "y",
							"number_value": 10.23,
						},
						"array": []any{"x", "y"},
					},
				},
			},
		},
		{
			Title:       "happy path strict, untyped",
			InputYAML:   testYAMLObject(),
			WantsStrict: true,
			Expected: interfaceOrObject{
				iface: map[string]any{
					"object": map[string]any{
						"key": map[string]any{
							"string_value": "This is a doc.\nOn multiple lines.\n",
							"bool_value":   "y",
							"number_value": 10.23,
						},
						"array": []any{"x", "y"},
					},
				},
			},
		},
		{
			Title:        "happy path strict, explicit target",
			InputYAML:    testYAMLObject(),
			ExpectedYAML: testYAMLObjectBool(),
			WantsStrict:  true,
			Expected: interfaceOrObject{
				iface: nil,
				obj: yamlReceiver{
					Object: yamlObject{
						Key: yamlKey{
							StringValue: "This is a doc.\nOn multiple lines.\n",
							BoolValue:   true,
							NumberValue: 10.23,
						},
						Array: []string{"x", "y"},
					},
				},
			},
		},
		{
			Title:        "happy path non-strict, explicit target",
			InputYAML:    testYAMLObjectNonStrict(),
			ExpectedYAML: testYAMLObjectBool(),
			WantsStrict:  false,
			Expected: interfaceOrObject{
				iface: nil,
				obj: yamlReceiver{
					Object: yamlObject{
						Key: yamlKey{
							StringValue: "This is a doc.\nOn multiple lines.\n",
							BoolValue:   true,
							NumberValue: 10.23,
						},
						Array: []string{"x", "y"},
					},
				},
			},
		},
		{
			Title:        "happy path strict, explicit target: unknown field failure",
			InputYAML:    testYAMLObjectNonStrict(),
			ExpectedYAML: testYAMLObjectBool(),
			WantsStrict:  true,
			ExpectError:  true,
			Expected: interfaceOrObject{
				iface: nil,
				obj: yamlReceiver{
					Object: yamlObject{
						Key: yamlKey{
							StringValue: "This is a doc.\nOn multiple lines.\n",
							BoolValue:   true,
							NumberValue: 10.23,
						},
						Array: []string{"x", "y"},
					},
				},
			},
		},
	} {
		testCase := toPin

		t.Run(testCase.Title, func(t *testing.T) {
			t.Parallel()

			var (
				err               error
				b, expectedOutput []byte
			)
			iface := testCase.Expected.iface
			obj := testCase.Expected.obj

			expectedInput := toPlainYaml(testCase.InputYAML)

			if testCase.WantsStrict {
				if iface != nil {
					err = UnmarshalStrict(expectedInput, &iface)
				} else {
					err = UnmarshalStrict(expectedInput, &obj)
				}
			} else {
				if iface != nil {
					err = Unmarshal(expectedInput, &iface)
				} else {
					err = Unmarshal(expectedInput, &obj)
				}
			}

			if testCase.ExpectError {
				require.Error(t, err)

				return
			}

			require.NoError(t, err)

			if iface != nil {
				require.Equal(t, testCase.Expected.iface, iface)

				b, err = Marshal(iface)
				require.NoError(t, err)
			} else {
				require.Equal(t, testCase.Expected.obj, obj)

				b, err = Marshal(obj)
				require.NoError(t, err)
			}

			if testCase.ExpectedYAML == nil {
				expectedOutput = expectedInput
			} else {
				expectedOutput = toPlainYaml(testCase.ExpectedYAML)
			}

			require.Equal(t, expectedOutput, b)
		})
	}
}

func toPlainYaml(in []byte) []byte {
	// ensure we got legit yaml for go strings that may have been reindented using tabs, or leading new CR in source.
	return bytes.ReplaceAll(
		bytes.TrimLeft(in, "\n\r"),
		[]byte("\t"), bytes.Repeat([]byte(" "), 4),
	)
}

func testYAMLObject() []byte {
	return []byte(`
object:
  array:
    - x
    - "y"
  key:
    bool_value: "y"
    number_value: 10.23
    string_value: |
      This is a doc.
      On multiple lines.
`)
}

func testYAMLObjectBool() []byte {
	// same object, but the "y" YAML for bool has been marshaled as "true"
	return []byte(`
object:
  array:
    - x
    - "y"
  key:
    bool_value: true
    number_value: 10.23
    string_value: |
      This is a doc.
      On multiple lines.
`)
}

func testYAMLObjectNonStrict() []byte {
	// same object, but with an extra unknown value
	return []byte(`
object:
  array:
    - x
    - "y"
  key:
    bool_value: true
    number_value: 10.23
    string_value: |
      This is a doc.
      On multiple lines.
  unknown: 'wrong'
`)
}
