// SPDX-FileCopyrightText: 2022 Free Mobile
// SPDX-License-Identifier: AGPL-3.0-only

package helpers

import (
	"errors"
	"reflect"
	"strings"
	"testing"

	"github.com/gin-gonic/gin"
	"github.com/go-viper/mapstructure/v2"
)

func TestMapStructureMatchName(t *testing.T) {
	cases := []struct {
		pos       Pos
		mapKey    string
		fieldName string
		expected  bool
	}{
		{Mark(), "one", "one", true},
		{Mark(), "one", "One", true},
		{Mark(), "one-two", "OneTwo", true},
		{Mark(), "onetwo", "OneTwo", true},
		{Mark(), "One-Two", "OneTwo", true},
		{Mark(), "two", "one", false},
	}
	for _, tc := range cases {
		got := MapStructureMatchName(tc.mapKey, tc.fieldName)
		if got && !tc.expected {
			t.Errorf("%s%q == %q but expected !=", tc.pos, tc.mapKey, tc.fieldName)
		} else if !got && tc.expected {
			t.Errorf("%s%q != %q but expected ==", tc.pos, tc.mapKey, tc.fieldName)
		}
	}
}

func TestStringToSliceHookFunc(t *testing.T) {
	type Configuration struct {
		A []string
		B []int
	}
	TestConfigurationDecode(t, ConfigurationDecodeCases{
		{
			Initial: func() any { return Configuration{} },
			Configuration: func() any {
				return gin.H{
					"a": "blip,blop",
					"b": "1,2,3,4",
				}
			},
			Expected: Configuration{
				A: []string{"blip", "blop"},
				B: []int{1, 2, 3, 4},
			},
		},
	})
}

func TestProtectedDecodeHook(t *testing.T) {
	var configuration struct {
		A string
		B string
	}
	panicHook := func(from, _ reflect.Type, data any) (any, error) {
		if from.Kind() == reflect.String {
			panic(errors.New("noooo"))
		}
		return data, nil
	}
	decoder, err := mapstructure.NewDecoder(GetMapStructureDecoderConfig(&configuration, panicHook))
	if err != nil {
		t.Fatalf("NewDecoder() error:\n%+v", err)
	}
	err = decoder.Decode(gin.H{"A": "hello", "B": "bye"})
	if err == nil {
		t.Fatal("Decode() did not error")
	} else {
		got := strings.Split(err.Error(), "\n")
		expected := []string{
			`decoding failed due to the following error(s):`,
			``,
			`'A' internal error while parsing: noooo`,
			`'B' internal error while parsing: noooo`,
		}
		if diff := Diff(got, expected); diff != "" {
			t.Fatalf("Decode() error:\n%s", diff)
		}
	}
}

func TestDefaultValuesConfig(t *testing.T) {
	type InnerConfiguration struct {
		AA string
		BB string
		CC int
	}
	type OuterConfiguration struct {
		DD []InnerConfiguration
	}
	RegisterMapstructureUnmarshallerHook(DefaultValuesUnmarshallerHook(InnerConfiguration{
		BB: "hello",
		CC: 10,
	}))
	TestConfigurationDecode(t, ConfigurationDecodeCases{
		{
			Initial: func() any { return OuterConfiguration{} },
			Configuration: func() any {
				return gin.H{
					"dd": []gin.H{
						{
							"aa": "hello1",
							"bb": "hello2",
							"cc": 43,
						},
						{"cc": 44},
						{"aa": "bye"},
					},
				}
			},
			Expected: OuterConfiguration{
				DD: []InnerConfiguration{
					{
						AA: "hello1",
						BB: "hello2",
						CC: 43,
					}, {
						AA: "",
						BB: "hello",
						CC: 44,
					}, {
						AA: "bye",
						BB: "hello",
						CC: 10,
					},
				},
			},
		},
	})
}

func TestRenameConfig(t *testing.T) {
	type Configuration struct {
		UnchangedLabel string
		NewLabel       string
	}
	RegisterMapstructureUnmarshallerHook(RenameKeyUnmarshallerHook(Configuration{}, "OldLabel", "NewLabel"))
	TestConfigurationDecode(t, ConfigurationDecodeCases{
		{
			Description: "no rename needed",
			Initial:     func() any { return Configuration{} },
			Configuration: func() any {
				return gin.H{
					"unchanged-label": "hello",
					"new-label":       "bye",
				}
			},
			Expected: Configuration{
				UnchangedLabel: "hello",
				NewLabel:       "bye",
			},
		}, {
			Description: "rename needed",
			Initial:     func() any { return Configuration{} },
			Configuration: func() any {
				return gin.H{
					"unchanged-label": "hello",
					"old-label":       "bye",
				}
			},
			Expected: Configuration{
				UnchangedLabel: "hello",
				NewLabel:       "bye",
			},
		}, {
			Description: "conflicts",
			Initial:     func() any { return Configuration{} },
			Configuration: func() any {
				return gin.H{
					"unchanged-label": "hello",
					"old-label":       "bye",
					"new-label":       "whatt?",
				}
			},
			Error: true,
		},
	})
}

func TestParametrizedConfig(t *testing.T) {
	type InnerConfigurationType1 struct {
		CC string
		DD string
	}
	type InnerConfigurationType2 struct {
		CC string
		EE string
	}
	type OuterConfiguration struct {
		AA     string
		BB     string
		Config any
	}
	available := map[string](func() any){
		"type1": func() any {
			return InnerConfigurationType1{
				CC: "cc1",
				DD: "dd1",
			}
		},
		"type2": func() any {
			return InnerConfigurationType2{
				CC: "cc2",
				EE: "ee2",
			}
		},
	}
	RegisterMapstructureUnmarshallerHook(ParametrizedConfigurationUnmarshallerHook(OuterConfiguration{}, available))

	t.Run("unmarshal", func(t *testing.T) {
		TestConfigurationDecode(t, ConfigurationDecodeCases{
			{
				Description: "type1",
				Initial:     func() any { return OuterConfiguration{} },
				Configuration: func() any {
					return gin.H{
						"type": "type1",
						"aa":   "a1",
						"bb":   "b1",
						"cc":   "c1",
						"dd":   "d1",
					}
				},
				Expected: OuterConfiguration{
					AA: "a1",
					BB: "b1",
					Config: &InnerConfigurationType1{
						CC: "c1",
						DD: "d1",
					},
				},
			}, {
				Description: "type2",
				Initial:     func() any { return OuterConfiguration{} },
				Configuration: func() any {
					return gin.H{
						"type": "type2",
						"aa":   "a2",
						"bb":   "b2",
						"cc":   "c2",
						"ee":   "e2",
					}
				},
				Expected: OuterConfiguration{
					AA: "a2",
					BB: "b2",
					Config: &InnerConfigurationType2{
						CC: "c2",
						EE: "e2",
					},
				},
			}, {
				Description: "unknown type",
				Initial:     func() any { return OuterConfiguration{} },
				Configuration: func() any {
					return gin.H{
						"type": "type3",
						"aa":   "a2",
						"bb":   "b2",
						"cc":   "c2",
						"ee":   "e2",
					}
				},
				Error: true,
			},
		})
	})
	t.Run("marshal", func(t *testing.T) {
		config1 := OuterConfiguration{
			AA: "a1",
			BB: "b1",
			Config: InnerConfigurationType1{
				CC: "c1",
				DD: "d1",
			},
		}
		expected1 := gin.H{
			"type": "type1",
			"aa":   "a1",
			"bb":   "b1",
			"cc":   "c1",
			"dd":   "d1",
		}
		got1, err := ParametrizedConfigurationMarshalYAML(config1, available)
		if err != nil {
			t.Fatalf("ParametrizedConfigurationMarshalYAML() error:\n%+v", err)
		}
		if diff := Diff(got1, expected1); diff != "" {
			t.Fatalf("ParametrizedConfigurationMarshalYAML() (-got, +want):\n%s", diff)
		}

		config2 := OuterConfiguration{
			AA: "a2",
			BB: "b2",
			Config: InnerConfigurationType2{
				CC: "c2",
				EE: "e2",
			},
		}
		expected2 := gin.H{
			"type": "type2",
			"aa":   "a2",
			"bb":   "b2",
			"cc":   "c2",
			"ee":   "e2",
		}
		got2, err := ParametrizedConfigurationMarshalYAML(config2, available)
		if err != nil {
			t.Fatalf("ParametrizedConfigurationMarshalYAML() error:\n%+v", err)
		}
		if diff := Diff(got2, expected2); diff != "" {
			t.Fatalf("ParametrizedConfigurationMarshalYAML() (-got, +want):\n%s", diff)
		}

		config3 := OuterConfiguration{
			AA: "a3",
			BB: "b3",
			Config: struct {
				FF string
			}{},
		}
		if _, err := ParametrizedConfigurationMarshalYAML(config3, available); err == nil {
			t.Fatal("ParametrizedConfigurationMarshalYAML() did not error")
		}
	})
}

func TestDeprecatedFields(t *testing.T) {
	type Configuration struct {
		A string
		B string
	}
	RegisterMapstructureDeprecatedFields[Configuration]("C", "D")
	TestConfigurationDecode(t, ConfigurationDecodeCases{
		{
			Initial: func() any { return Configuration{} },
			Configuration: func() any {
				return gin.H{
					"a": "hello",
					"b": "bye",
					"c": "nooo",
					"d": "yes",
				}
			},
			Expected: Configuration{
				A: "hello",
				B: "bye",
			},
		}, {
			Initial: func() any { return Configuration{} },
			Configuration: func() any {
				return gin.H{
					"a": "hello",
					"b": "bye",
					"e": "nooo",
				}
			},
			Error: true,
		},
	})
}

func TestDeprecatedFieldsInSquashedStructure(t *testing.T) {
	type SubConfiguration struct {
		A string
		B string
	}
	type Configuration struct {
		Sub SubConfiguration `mapstructure:",squash"`
		E   string
	}
	RegisterMapstructureDeprecatedFields[SubConfiguration]("C", "D")
	TestConfigurationDecode(t, ConfigurationDecodeCases{
		{
			Initial: func() any { return Configuration{} },
			Configuration: func() any {
				return gin.H{
					"a": "hello",
					"b": "bye",
					"c": "nooo",
					"d": "yes",
					"e": "maybe",
				}
			},
			Expected: Configuration{
				Sub: SubConfiguration{
					A: "hello",
					B: "bye",
				},
				E: "maybe",
			},
		}, {
			Initial: func() any { return Configuration{} },
			Configuration: func() any {
				return gin.H{
					"a": "hello",
					"b": "bye",
					"f": "nooo",
				}
			},
			Error: true,
		},
	})
}
