package api_test

import (
	"encoding/json"
	"math"
	"testing"

	"github.com/stretchr/testify/require"
	"go.yaml.in/yaml/v4"

	"github.com/lxc/incus/v7/shared/api"
)

func TestDevicesMap_UnmarshalJSON(t *testing.T) {
	tests := []struct {
		name   string
		input  map[string]any
		target api.DevicesMap

		assertErr require.ErrorAssertionFunc
		want      api.DevicesMap
	}{
		{
			name:  "nil",
			input: nil,

			assertErr: require.NoError,
			want:      nil,
		},
		{
			name:  "nil with defaults",
			input: nil,
			target: api.DevicesMap{
				"device": map[string]string{
					"default_string": "string",
				},
			},

			assertErr: require.NoError,
			want: api.DevicesMap{
				"device": map[string]string{
					"default_string": "string",
				},
			},
		},
		{
			name:  "empty with defaults",
			input: map[string]any{},
			target: api.DevicesMap{
				"device": map[string]string{
					"default_string": "string",
				},
			},

			assertErr: require.NoError,
			want: api.DevicesMap{
				"device": map[string]string{
					"default_string": "string",
				},
			},
		},
		{
			name: "only string",
			input: map[string]any{
				"device": map[string]any{
					"string":  "string",
					"int":     "5",
					"float64": "5.4",
					"bool":    "true",
					"empty":   "",
					"null":    "null",
				},
			},

			assertErr: require.NoError,
			want: api.DevicesMap{
				"device": map[string]string{
					"string":  "string",
					"int":     "5",
					"float64": "5.4",
					"bool":    "true",
					"empty":   "",
					"null":    "null",
				},
			},
		},
		{
			name: "mixed",
			input: map[string]any{
				"device": map[string]any{
					"string":     "string",
					"int":        5,
					"uint":       uint64(math.MaxUint64),
					"float64":    5.4,
					"float64max": math.MaxFloat64,
					"bool":       true,
					"empty":      "",
					"null":       nil,
				},
			},

			assertErr: require.NoError,
			want: api.DevicesMap{
				"device": map[string]string{
					"string":     "string",
					"int":        "5",
					"uint":       "1.8446744073709552e+19",
					"float64":    "5.4",
					"float64max": "1.7976931348623157e+308",
					"bool":       "true",
					"empty":      "",
					"null":       "",
				},
			},
		},
		{
			name: "mixed with defaults",
			input: map[string]any{
				"device": map[string]any{
					"string":     "string",
					"int":        5,
					"uint":       uint64(math.MaxUint64),
					"float64":    5.4,
					"float64max": math.MaxFloat64,
					"bool":       true,
					"empty":      "",
					"null":       nil,
				},
			},
			target: api.DevicesMap{
				"device": map[string]string{
					"default_string": "string",
				},
			},

			assertErr: require.NoError,
			want: api.DevicesMap{
				"device": map[string]string{
					"string":         "string",
					"int":            "5",
					"uint":           "1.8446744073709552e+19",
					"float64":        "5.4",
					"float64max":     "1.7976931348623157e+308",
					"bool":           "true",
					"empty":          "",
					"null":           "",
					"default_string": "string",
				},
			},
		},

		// // errors
		{
			name: "error - unsupported type object",
			input: map[string]any{
				"device": map[string]any{
					"invalid": map[string]any{ // invalid
						"inner": "value",
					},
				},
			},

			assertErr: require.Error,
			want:      nil,
		},
		{
			name: "error - unsupported type object with defaults",
			input: map[string]any{
				"device": map[string]any{
					"invalid": map[string]any{ // invalid
						"inner": "value",
					},
				},
			},
			target: api.DevicesMap{
				"device": map[string]string{
					"default_string": "string",
				},
			},

			assertErr: require.Error,
			want: api.DevicesMap{
				"device": map[string]string{
					"default_string": "string",
				},
			},
		},
		{
			name: "error - unsupported type array",
			input: map[string]any{
				"invalid": []string{ // invalid
					"inner", "value",
				},
			},

			assertErr: require.Error,
			want:      nil,
		},
		{
			name: "error - unsupported type array with defaults",
			input: map[string]any{
				"device": map[string]any{
					"invalid": []string{ // invalid
						"inner", "value",
					},
				},
			},
			target: api.DevicesMap{
				"device": map[string]string{
					"default_string": "string",
				},
			},

			assertErr: require.Error,
			want: api.DevicesMap{
				"device": map[string]string{
					"default_string": "string",
				},
			},
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			in, err := json.Marshal(tc.input)
			require.NoError(t, err)

			err = json.Unmarshal(in, &tc.target)
			tc.assertErr(t, err)

			require.Equal(t, tc.want, tc.target)
		})
	}
}

func TestDevicesMap_UnmarshalYAML(t *testing.T) {
	tests := []struct {
		name   string
		input  map[string]any
		target api.DevicesMap

		assertErr require.ErrorAssertionFunc
		want      api.DevicesMap
	}{
		{
			name:  "nil",
			input: nil,

			assertErr: require.NoError,
			want:      api.DevicesMap{},
		},
		{
			name:  "nil with defaults",
			input: nil,
			target: api.DevicesMap{
				"device": map[string]string{
					"default_string": "string",
				},
			},

			assertErr: require.NoError,
			want: api.DevicesMap{
				"device": map[string]string{
					"default_string": "string",
				},
			},
		},
		{
			name:  "empty",
			input: map[string]any{},

			assertErr: require.NoError,
			want:      api.DevicesMap{},
		},
		{
			name:  "empty with defaults",
			input: map[string]any{},
			target: api.DevicesMap{
				"device": map[string]string{
					"default_string": "string",
				},
			},

			assertErr: require.NoError,
			want: api.DevicesMap{
				"device": map[string]string{
					"default_string": "string",
				},
			},
		},
		{
			name: "only string",
			input: map[string]any{
				"device": map[string]any{
					"string":  "string",
					"int":     "5",
					"float64": "5.4",
					"bool":    "true",
					"empty":   "",
					"null":    "null",
				},
			},

			assertErr: require.NoError,
			want: api.DevicesMap{
				"device": map[string]string{
					"string":  "string",
					"int":     "5",
					"float64": "5.4",
					"bool":    "true",
					"empty":   "",
					"null":    "null",
				},
			},
		},
		{
			name: "mixed",
			input: map[string]any{
				"device": map[string]any{
					"string":     "string",
					"int":        5,
					"uint":       uint64(math.MaxUint64),
					"float64":    5.4,
					"float64max": math.MaxFloat64,
					"bool":       true,
					"empty":      "",
					"null":       nil,
				},
			},

			assertErr: require.NoError,
			want: api.DevicesMap{
				"device": map[string]string{
					"string":     "string",
					"int":        "5",
					"uint":       "18446744073709551615",
					"float64":    "5.4",
					"float64max": "1.7976931348623157e+308",
					"bool":       "true",
					"empty":      "",
					"null":       "",
				},
			},
		},
		{
			name: "mixed with defaults",
			input: map[string]any{
				"device": map[string]any{
					"string":     "string",
					"int":        5,
					"uint":       uint64(math.MaxUint64),
					"float64":    5.4,
					"float64max": math.MaxFloat64,
					"bool":       true,
					"empty":      "",
					"null":       nil,
				},
			},
			target: api.DevicesMap{
				"device": map[string]string{
					"default_string": "string",
				},
			},

			assertErr: require.NoError,
			want: api.DevicesMap{
				"device": map[string]string{
					"string":         "string",
					"int":            "5",
					"uint":           "18446744073709551615",
					"float64":        "5.4",
					"float64max":     "1.7976931348623157e+308",
					"bool":           "true",
					"empty":          "",
					"null":           "",
					"default_string": "string",
				},
			},
		},

		// errors
		{
			name: "error - unsupported type object",
			input: map[string]any{
				"device": map[string]any{
					"invalid": map[string]any{ // invalid
						"inner": "value",
					},
				},
			},

			assertErr: require.Error,
			want:      nil,
		},
		{
			name: "error - unsupported type object with defaults",
			input: map[string]any{
				"device": map[string]any{
					"invalid": map[string]any{ // invalid
						"inner": "value",
					},
				},
			},
			target: api.DevicesMap{
				"device": map[string]string{
					"default_string": "string",
				},
			},

			assertErr: require.Error,
			want: api.DevicesMap{
				"device": map[string]string{
					"default_string": "string",
				},
			},
		},
		{
			name: "error - unsupported type array",
			input: map[string]any{
				"device": map[string]any{
					"invalid": []string{ // invalid
						"inner", "value",
					},
				},
			},

			assertErr: require.Error,
			want:      nil,
		},
		{
			name: "error - unsupported type array with defaults",
			input: map[string]any{
				"device": map[string]any{
					"invalid": []string{ // invalid
						"inner", "value",
					},
				},
			},
			target: api.DevicesMap{
				"device": map[string]string{
					"default_string": "string",
				},
			},

			assertErr: require.Error,
			want: api.DevicesMap{
				"device": map[string]string{
					"default_string": "string",
				},
			},
		},
		{
			name: "error - unsupported type int for key",
			input: map[string]any{
				"device": map[any]any{
					100: map[string]any{ // invalid key
						"inner": "value",
					},
				},
			},
			target: api.DevicesMap{
				"device": map[string]string{
					"default_string": "string",
				},
			},

			assertErr: require.Error,
			want: api.DevicesMap{
				"device": map[string]string{
					"default_string": "string",
				},
			},
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			in, err := yaml.Dump(tc.input, yaml.WithV2Defaults())
			require.NoError(t, err)

			err = yaml.Load(in, &tc.target)
			tc.assertErr(t, err)

			require.Equal(t, tc.want, tc.target)
		})
	}
}
