/*
Copyright (c) Edgeless Systems GmbH

SPDX-License-Identifier: AGPL-3.0-only
*/

package validation

import (
	"fmt"
	"reflect"
	"testing"

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

func TestErrorFormatting(t *testing.T) {
	err := &TreeError{
		path:     "path",
		err:      fmt.Errorf("error"),
		children: []*TreeError{},
	}

	assert.Equal(t, "validating path: error", err.Error())

	err.children = append(err.children, &TreeError{
		path:     "child",
		err:      fmt.Errorf("child error"),
		children: []*TreeError{},
	})

	assert.Equal(t, "validating path: error\n  validating child: child error", err.Error())

	err.children = append(err.children, &TreeError{
		path: "child2",
		err:  fmt.Errorf("child2 error"),
		children: []*TreeError{
			{
				path:     "child2child",
				err:      fmt.Errorf("child2child error"),
				children: []*TreeError{},
			},
		},
	})
	assert.Equal(t, "validating path: error\n  validating child: child error\n  validating child2: child2 error\n    validating child2child: child2child error", err.Error())
}

// Tests for primitive / shallow fields

func TestNewValidationErrorSingleField(t *testing.T) {
	st := &errorTestDoc{
		ExportedField: "abc",
		OtherField:    42,
	}

	doc, field := references(t, st, &st.OtherField, "")
	err := newTraceError(doc, field, assert.AnError)
	require.Error(t, err)
	require.Contains(t, err.Error(), fmt.Sprintf("validating errorTestDoc.otherField: %s", assert.AnError))
}

func TestNewValidationErrorSingleFieldPtr(t *testing.T) {
	st := &errorTestDoc{
		ExportedField: "abc",
		OtherField:    42,
		PointerField:  new(int),
	}

	doc, field := references(t, st, &st.PointerField, "")
	err := newTraceError(doc, field, assert.AnError)
	require.Error(t, err)
	require.Contains(t, err.Error(), fmt.Sprintf("validating errorTestDoc.pointerField: %s", assert.AnError))
}

func TestNewValidationErrorSingleFieldDoublePtr(t *testing.T) {
	intp := new(int)
	st := &errorTestDoc{
		ExportedField:      "abc",
		OtherField:         42,
		DoublePointerField: &intp,
	}

	doc, field := references(t, st, &st.DoublePointerField, "")
	err := newTraceError(doc, field, assert.AnError)
	require.Error(t, err)
	require.Contains(t, err.Error(), fmt.Sprintf("validating errorTestDoc.doublePointerField: %s", assert.AnError))
}

func TestNewValidationErrorSingleFieldInexistent(t *testing.T) {
	st := &errorTestDoc{
		ExportedField: "abc",
		OtherField:    42,
		PointerField:  new(int),
	}

	inexistentField := 123

	doc, field := references(t, st, &inexistentField, "")
	err := newTraceError(doc, field, assert.AnError)
	require.Error(t, err)
	require.Contains(t, err.Error(), "cannot find path to field: cannot traverse anymore")
}

// Tests for nested structs

func TestNewValidationErrorNestedField(t *testing.T) {
	st := &errorTestDoc{
		ExportedField: "abc",
		OtherField:    42,
		NestedField: nestederrorTestDoc{
			ExportedField: "nested",
			OtherField:    123,
		},
	}

	doc, field := references(t, st, &st.NestedField.OtherField, "")
	err := newTraceError(doc, field, assert.AnError)
	t.Log(err)
	require.Error(t, err)
	require.Contains(t, err.Error(), fmt.Sprintf("validating errorTestDoc.nestedField.otherField: %s", assert.AnError))
}

func TestNewValidationErrorPointerInNestedField(t *testing.T) {
	st := &errorTestDoc{
		ExportedField: "abc",
		OtherField:    42,
		NestedField: nestederrorTestDoc{
			ExportedField: "nested",
			OtherField:    123,
			PointerField:  new(int),
		},
	}

	doc, field := references(t, st, &st.NestedField.PointerField, "")
	err := newTraceError(doc, field, assert.AnError)
	t.Log(err)
	require.Error(t, err)
	require.Contains(t, err.Error(), fmt.Sprintf("validating errorTestDoc.nestedField.pointerField: %s", assert.AnError))
}

func TestNewValidationErrorNestedFieldPtr(t *testing.T) {
	st := &errorTestDoc{
		ExportedField: "abc",
		OtherField:    42,
		NestedField: nestederrorTestDoc{
			ExportedField: "nested",
			OtherField:    123,
		},
		NestedPointerField: &nestederrorTestDoc{
			ExportedField: "nested",
			OtherField:    123,
		},
	}

	doc, field := references(t, st, &st.NestedPointerField.OtherField, "")
	err := newTraceError(doc, field, assert.AnError)
	t.Log(err)
	require.Error(t, err)
	require.Contains(t, err.Error(), fmt.Sprintf("validating errorTestDoc.nestedPointerField.otherField: %s", assert.AnError))
}

func TestNewValidationErrorNestedNestedField(t *testing.T) {
	st := &errorTestDoc{
		ExportedField: "abc",
		OtherField:    42,
		NestedField: nestederrorTestDoc{
			ExportedField: "nested",
			OtherField:    123,
			NestedField: nestedNestederrorTestDoc{
				ExportedField: "nested",
				OtherField:    123,
			},
		},
	}

	doc, field := references(t, st, &st.NestedField.NestedField.OtherField, "")
	err := newTraceError(doc, field, assert.AnError)
	t.Log(err)
	require.Error(t, err)
	require.Contains(t, err.Error(), fmt.Sprintf("validating errorTestDoc.nestedField.nestedField.otherField: %s", assert.AnError))
}

func TestNewValidationErrorNestedNestedFieldPtr(t *testing.T) {
	st := &errorTestDoc{
		ExportedField: "abc",
		OtherField:    42,
		NestedField: nestederrorTestDoc{
			ExportedField: "nested",
			OtherField:    123,
			NestedPointerField: &nestedNestederrorTestDoc{
				ExportedField: "nested",
				OtherField:    123,
			},
		},
	}

	doc, field := references(t, st, &st.NestedField.NestedPointerField.OtherField, "")
	err := newTraceError(doc, field, assert.AnError)
	t.Log(err)
	require.Error(t, err)
	require.Contains(t, err.Error(), fmt.Sprintf("validating errorTestDoc.nestedField.nestedPointerField.otherField: %s", assert.AnError))
}

func TestNewValidationErrorNestedPtrNestedFieldPtr(t *testing.T) {
	st := &errorTestDoc{
		ExportedField: "abc",
		OtherField:    42,
		NestedPointerField: &nestederrorTestDoc{
			ExportedField: "nested",
			OtherField:    123,
			NestedPointerField: &nestedNestederrorTestDoc{
				ExportedField: "nested",
				OtherField:    123,
			},
		},
	}

	doc, field := references(t, st, &st.NestedPointerField.NestedPointerField.OtherField, "")
	err := newTraceError(doc, field, assert.AnError)
	t.Log(err)
	require.Error(t, err)
	require.Contains(t, err.Error(), fmt.Sprintf("validating errorTestDoc.nestedPointerField.nestedPointerField.otherField: %s", assert.AnError))
}

// Tests for slices / arrays

func TestNewValidationErrorPrimitiveSlice(t *testing.T) {
	st := &sliceErrorTestDoc{
		PrimitiveSlice: []string{"abc", "def"},
	}

	doc, field := references(t, st, &st.PrimitiveSlice[1], "")
	err := newTraceError(doc, field, assert.AnError)
	t.Log(err)
	require.Error(t, err)
	require.Contains(t, err.Error(), fmt.Sprintf("validating sliceErrorTestDoc.primitiveSlice[1]: %s", assert.AnError))
}

func TestNewValidationErrorPrimitiveArray(t *testing.T) {
	st := &sliceErrorTestDoc{
		PrimitiveArray: [3]int{1, 2, 3},
	}

	doc, field := references(t, st, &st.PrimitiveArray[1], "")
	err := newTraceError(doc, field, assert.AnError)
	t.Log(err)
	require.Error(t, err)
	require.Contains(t, err.Error(), fmt.Sprintf("validating sliceErrorTestDoc.primitiveArray[1]: %s", assert.AnError))
}

func TestNewValidationErrorStructSlice(t *testing.T) {
	st := &sliceErrorTestDoc{
		StructSlice: []errorTestDoc{
			{
				ExportedField: "abc",
				OtherField:    123,
			},
			{
				ExportedField: "def",
				OtherField:    456,
			},
		},
	}

	doc, field := references(t, st, &st.StructSlice[1].OtherField, "")
	err := newTraceError(doc, field, assert.AnError)
	t.Log(err)
	require.Error(t, err)
	require.Contains(t, err.Error(), fmt.Sprintf("validating sliceErrorTestDoc.structSlice[1].otherField: %s", assert.AnError))
}

func TestNewValidationErrorStructArray(t *testing.T) {
	st := &sliceErrorTestDoc{
		StructArray: [3]errorTestDoc{
			{
				ExportedField: "abc",
				OtherField:    123,
			},
			{
				ExportedField: "def",
				OtherField:    456,
			},
		},
	}

	doc, field := references(t, st, &st.StructArray[1].OtherField, "")
	err := newTraceError(doc, field, assert.AnError)
	t.Log(err)
	require.Error(t, err)
	require.Contains(t, err.Error(), fmt.Sprintf("validating sliceErrorTestDoc.structArray[1].otherField: %s", assert.AnError))
}

func TestNewValidationErrorStructPointerSlice(t *testing.T) {
	st := &sliceErrorTestDoc{
		StructPointerSlice: []*errorTestDoc{
			{
				ExportedField: "abc",
				OtherField:    123,
			},
			{
				ExportedField: "def",
				OtherField:    456,
			},
		},
	}

	doc, field := references(t, st, &st.StructPointerSlice[1].OtherField, "")
	err := newTraceError(doc, field, assert.AnError)
	t.Log(err)
	require.Error(t, err)
	require.Contains(t, err.Error(), fmt.Sprintf("validating sliceErrorTestDoc.structPointerSlice[1].otherField: %s", assert.AnError))
}

func TestNewValidationErrorStructPointerArray(t *testing.T) {
	st := &sliceErrorTestDoc{
		StructPointerArray: [3]*errorTestDoc{
			{
				ExportedField: "abc",
				OtherField:    123,
			},
			{
				ExportedField: "def",
				OtherField:    456,
			},
		},
	}

	doc, field := references(t, st, &st.StructPointerArray[1].OtherField, "")
	err := newTraceError(doc, field, assert.AnError)
	t.Log(err)
	require.Error(t, err)
	require.Contains(t, err.Error(), fmt.Sprintf("validating sliceErrorTestDoc.structPointerArray[1].otherField: %s", assert.AnError))
}

func TestNewValidationErrorPrimitiveSliceSlice(t *testing.T) {
	st := &sliceErrorTestDoc{
		PrimitiveSliceSlice: [][]string{
			{"abc", "def"},
			{"ghi", "jkl"},
		},
	}

	doc, field := references(t, st, &st.PrimitiveSliceSlice[1][1], "")
	err := newTraceError(doc, field, assert.AnError)
	t.Log(err)
	require.Error(t, err)
	require.Contains(t, err.Error(), fmt.Sprintf("validating sliceErrorTestDoc.primitiveSliceSlice[1][1]: %s", assert.AnError))
}

// Tests for maps

func TestNewValidationErrorPrimitiveMap(t *testing.T) {
	st := &mapErrorTestDoc{
		PrimitiveMap: map[string]string{
			"abc": "def",
			"ghi": "jkl",
		},
	}

	doc, field := references(t, st, &st.PrimitiveMap, "ghi")
	err := newTraceError(doc, field, assert.AnError)
	t.Log(err)
	require.Error(t, err)
	require.Contains(t, err.Error(), fmt.Sprintf("validating mapErrorTestDoc.primitiveMap[\"ghi\"]: %s", assert.AnError))
}

func TestNewValidationErrorStructPointerMap(t *testing.T) {
	st := &mapErrorTestDoc{
		StructPointerMap: map[string]*errorTestDoc{
			"abc": {
				ExportedField: "abc",
				OtherField:    123,
			},
			"ghi": {
				ExportedField: "ghi",
				OtherField:    456,
			},
		},
	}

	doc, field := references(t, st, &st.StructPointerMap["ghi"].OtherField, "")
	err := newTraceError(doc, field, assert.AnError)
	t.Log(err)
	require.Error(t, err)
	require.Contains(t, err.Error(), fmt.Sprintf("validating mapErrorTestDoc.structPointerMap[\"ghi\"].otherField: %s", assert.AnError))
}

func TestNewValidationErrorNestedPrimitiveMap(t *testing.T) {
	st := &mapErrorTestDoc{
		NestedPointerMap: map[string]*map[string]string{
			"abc": {
				"def": "ghi",
			},
			"jkl": {
				"mno": "pqr",
			},
		},
	}

	doc, field := references(t, st, st.NestedPointerMap["jkl"], "mno")
	err := newTraceError(doc, field, assert.AnError)
	t.Log(err)
	require.Error(t, err)
	require.Contains(t, err.Error(), fmt.Sprintf("validating mapErrorTestDoc.nestedPointerMap[\"jkl\"][\"mno\"]: %s", assert.AnError))
}

// Special cases

func TestNewValidationErrorTopLevelIsNeedle(t *testing.T) {
	st := &errorTestDoc{
		ExportedField: "abc",
		OtherField:    42,
	}

	doc, field := references(t, st, st, "")
	err := newTraceError(doc, field, assert.AnError)
	require.Error(t, err)
	require.Contains(t, err.Error(), fmt.Sprintf("validating errorTestDoc: %s", assert.AnError))
}

func TestNewValidationErrorUntaggedField(t *testing.T) {
	st := &errorTestDoc{
		ExportedField: "abc",
		OtherField:    42,
		NoTagField:    123,
	}

	doc, field := references(t, st, &st.NoTagField, "")
	err := newTraceError(doc, field, assert.AnError)
	require.Error(t, err)
	require.Contains(t, err.Error(), fmt.Sprintf("validating errorTestDoc.NoTagField: %s", assert.AnError))
}

func TestNewValidationErrorOnlyYamlTaggedField(t *testing.T) {
	st := &errorTestDoc{
		ExportedField: "abc",
		OtherField:    42,
		NoTagField:    123,
		OnlyYamlKey:   "abc",
	}

	doc, field := references(t, st, &st.OnlyYamlKey, "")
	err := newTraceError(doc, field, assert.AnError)
	require.Error(t, err)
	require.Contains(t, err.Error(), fmt.Sprintf("validating errorTestDoc.onlyYamlKey: %s", assert.AnError))
}

type errorTestDoc struct {
	ExportedField      string              `json:"exportedField" yaml:"exportedField"`
	OtherField         int                 `json:"otherField" yaml:"otherField"`
	PointerField       *int                `json:"pointerField" yaml:"pointerField"`
	DoublePointerField **int               `json:"doublePointerField" yaml:"doublePointerField"`
	NestedField        nestederrorTestDoc  `json:"nestedField" yaml:"nestedField"`
	NestedPointerField *nestederrorTestDoc `json:"nestedPointerField" yaml:"nestedPointerField"`
	NoTagField         int
	OnlyYamlKey        string `yaml:"onlyYamlKey"`
}

type nestederrorTestDoc struct {
	ExportedField      string                    `json:"exportedField" yaml:"exportedField"`
	OtherField         int                       `json:"otherField" yaml:"otherField"`
	PointerField       *int                      `json:"pointerField" yaml:"pointerField"`
	NestedField        nestedNestederrorTestDoc  `json:"nestedField" yaml:"nestedField"`
	NestedPointerField *nestedNestederrorTestDoc `json:"nestedPointerField" yaml:"nestedPointerField"`
}

type nestedNestederrorTestDoc struct {
	ExportedField string `json:"exportedField" yaml:"exportedField"`
	OtherField    int    `json:"otherField" yaml:"otherField"`
	PointerField  *int   `json:"pointerField" yaml:"pointerField"`
}

type sliceErrorTestDoc struct {
	PrimitiveSlice      []string         `json:"primitiveSlice" yaml:"primitiveSlice"`
	PrimitiveArray      [3]int           `json:"primitiveArray" yaml:"primitiveArray"`
	StructSlice         []errorTestDoc   `json:"structSlice" yaml:"structSlice"`
	StructArray         [3]errorTestDoc  `json:"structArray" yaml:"structArray"`
	StructPointerSlice  []*errorTestDoc  `json:"structPointerSlice" yaml:"structPointerSlice"`
	StructPointerArray  [3]*errorTestDoc `json:"structPointerArray" yaml:"structPointerArray"`
	PrimitiveSliceSlice [][]string       `json:"primitiveSliceSlice" yaml:"primitiveSliceSlice"`
}

type mapErrorTestDoc struct {
	PrimitiveMap     map[string]string             `json:"primitiveMap" yaml:"primitiveMap"`
	StructPointerMap map[string]*errorTestDoc      `json:"structPointerMap" yaml:"structPointerMap"`
	NestedPointerMap map[string]*map[string]string `json:"nestedPointerMap" yaml:"nestedPointerMap"`
}

// references returns referenceableValues for the given doc and field for testing purposes.
func references(t *testing.T, doc, field any, mapKey string) (haystack, needle referenceableValue) {
	t.Helper()
	derefedField := pointerDeref(reflect.ValueOf(field))
	fieldRef := referenceableValue{
		value:  derefedField,
		addr:   derefedField.UnsafeAddr(),
		_type:  derefedField.Type(),
		mapKey: mapKey,
	}
	derefedDoc := pointerDeref(reflect.ValueOf(doc))
	docRef := referenceableValue{
		value: derefedDoc,
		addr:  derefedDoc.UnsafeAddr(),
		_type: derefedDoc.Type(),
	}
	return docRef, fieldRef
}
