package proptools

import (
	"strings"
	"testing"
)

func mustHash(t *testing.T, data interface{}) uint64 {
	t.Helper()
	result, err := CalculateHash(data)
	if err != nil {
		t.Fatal(err)
	}
	return result
}

func TestHashingMapGetsSameResults(t *testing.T) {
	data := map[string]string{"foo": "bar", "baz": "qux"}
	first := mustHash(t, data)
	second := mustHash(t, data)
	third := mustHash(t, data)
	fourth := mustHash(t, data)
	if first != second || second != third || third != fourth {
		t.Fatal("Did not get the same result every time for a map")
	}
}

func TestHashingNonSerializableTypesFails(t *testing.T) {
	testCases := []struct {
		name string
		data interface{}
	}{
		{
			name: "function pointer",
			data: []func(){nil},
		},
		{
			name: "channel",
			data: []chan int{make(chan int)},
		},
		{
			name: "list with non-serializable type",
			data: []interface{}{"foo", make(chan int)},
		},
	}
	for _, testCase := range testCases {
		t.Run(testCase.name, func(t *testing.T) {
			_, err := CalculateHash(testCase)
			if err == nil {
				t.Fatal("Expected hashing error but didn't get one")
			}
			expected := "data may only contain primitives, strings, arrays, slices, structs, maps, and pointers"
			if !strings.Contains(err.Error(), expected) {
				t.Fatalf("Expected %q, got %q", expected, err.Error())
			}
		})
	}
}

var hashTestCases = []struct {
	name string
	data interface{}
}{
	{
		name: "int",
		data: 5,
	},
	{
		name: "string",
		data: "foo",
	},
	{
		name: "*string",
		data: StringPtr("foo"),
	},
	{
		name: "array",
		data: [3]string{"foo", "bar", "baz"},
	},
	{
		name: "slice",
		data: []string{"foo", "bar", "baz"},
	},
	{
		name: "struct",
		data: struct {
			foo string
			bar int
		}{
			foo: "foo",
			bar: 3,
		},
	},
	{
		name: "map",
		data: map[string]int{
			"foo": 3,
			"bar": 4,
		},
	},
	{
		name: "list of interfaces with different types",
		data: []interface{}{"foo", 3, []string{"bar", "baz"}},
	},
	{
		name: "nested maps",
		data: map[string]map[string]map[string]map[string]map[string]int{
			"foo": {"foo": {"foo": {"foo": {"foo": 5}}}},
		},
	},
	{
		name: "multiple maps",
		data: struct {
			foo  map[string]int
			bar  map[string]int
			baz  map[string]int
			qux  map[string]int
			quux map[string]int
		}{
			foo:  map[string]int{"foo": 1, "bar": 2},
			bar:  map[string]int{"bar": 2},
			baz:  map[string]int{"baz": 3, "foo": 1},
			qux:  map[string]int{"qux": 4},
			quux: map[string]int{"quux": 5},
		},
	},
	{
		name: "nested structs",
		data: nestableStruct{
			foo: nestableStruct{
				foo: nestableStruct{
					foo: nestableStruct{
						foo: nestableStruct{
							foo: "foo",
						},
					},
				},
			},
		},
	},
}

func TestHashSuccessful(t *testing.T) {
	for _, testCase := range hashTestCases {
		t.Run(testCase.name, func(t *testing.T) {
			mustHash(t, testCase.data)
		})
	}
}

func TestHashingDereferencePointers(t *testing.T) {
	str1 := "this is a hash test for pointers"
	str2 := "this is a hash test for pointers"
	data := []struct {
		content *string
	}{
		{content: &str1},
		{content: &str2},
	}
	first := mustHash(t, data[0])
	second := mustHash(t, data[1])
	if first != second {
		t.Fatal("Got different results for the same string")
	}
}

type nestableStruct struct {
	foo interface{}
}

func TestContainsConfigurable(t *testing.T) {
	testCases := []struct {
		name   string
		value  any
		result bool
	}{
		{
			name: "struct without configurable",
			value: struct {
				S string
			}{},
			result: false,
		},
		{
			name: "struct with configurable",
			value: struct {
				S Configurable[string]
			}{},
			result: true,
		},
		{
			name: "struct with allowed configurable",
			value: struct {
				S Configurable[string] `blueprint:"allow_configurable_in_provider"`
			}{},
			result: false,
		},
	}

	for _, testCase := range testCases {
		t.Run(testCase.name, func(t *testing.T) {
			got := ContainsConfigurable(testCase.value)
			if got != testCase.result {
				t.Errorf("expected %v, got %v", testCase.value, got)
			}
		})
	}
}

func BenchmarkCalculateHash(b *testing.B) {
	for _, testCase := range hashTestCases {
		b.Run(testCase.name, func(b *testing.B) {
			b.ReportAllocs()
			for i := 0; i < b.N; i++ {
				_, err := CalculateHash(testCase.data)
				if err != nil {
					panic(err)
				}
			}
		})
	}
}
