// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package jsonprovider

import (
	"encoding/json"
	"testing"

	"github.com/google/go-cmp/cmp"
	"github.com/opentofu/opentofu/internal/providers"
	"github.com/zclconf/go-cty/cty"
)

func TestMarshalReturnType(t *testing.T) {
	type testcase struct {
		Arg      cty.Type
		Expected any
	}

	tests := map[string]testcase{
		"string": {
			Arg:      cty.String,
			Expected: "string",
		},
		"number": {
			Arg:      cty.Number,
			Expected: "number",
		},
		"bool": {
			Arg:      cty.Bool,
			Expected: "bool",
		},
		"object": {
			Arg: cty.Object(map[string]cty.Type{"number_type": cty.Number}),
			Expected: []any{
				string("object"),
				map[string]cty.Type{"number_type": cty.Number},
			},
		},
		"map": {
			Arg: cty.Map(cty.String),
			Expected: []any{
				string("map"),
				cty.String,
			},
		},
		"list": {
			Arg: cty.List(cty.Bool),
			Expected: []any{
				string("list"),
				cty.Bool,
			},
		},
		"set": {
			Arg: cty.Set(cty.Number),
			Expected: []any{
				string("set"),
				cty.Number,
			},
		},
		"tuple": {
			Arg: cty.Tuple([]cty.Type{cty.String}),
			Expected: []any{
				string("tuple"),
				[]any{cty.String},
			},
		},
	}

	for tn, tc := range tests {
		t.Run(tn, func(t *testing.T) {
			actual := marshalReturnType(tc.Arg)

			// to avoid the nightmare of comparing cty primitve types we can marshal them to json and compare that
			actualJSON, _ := json.Marshal(actual)
			expectedJSON, _ := json.Marshal(tc.Expected)
			if !cmp.Equal(actualJSON, expectedJSON) {
				t.Fatalf("values don't match:\n %v\n", cmp.Diff(string(actualJSON), string(expectedJSON)))
			}
		})
	}
}

func TestMarshalParameter(t *testing.T) {
	// used so can make a pointer to it
	trueBoolVal := true

	type testcase struct {
		Arg      providers.FunctionParameterSpec
		Expected FunctionParam
	}

	tests := map[string]testcase{
		"basic": {
			Arg: providers.FunctionParameterSpec{
				Description: "basic string func",
				Type:        cty.String,
			},
			Expected: FunctionParam{
				Description: "basic string func",
				Type:        cty.String,
			},
		},
		"nullable": {
			Arg: providers.FunctionParameterSpec{
				Description:    "nullable number func",
				Type:           cty.Number,
				AllowNullValue: trueBoolVal,
			},
			Expected: FunctionParam{
				Description: "nullable number func",
				Type:        cty.Number,
				IsNullable:  &trueBoolVal,
			},
		},
	}

	for tn, tc := range tests {
		t.Run(tn, func(t *testing.T) {
			actual := marshalParameter(tc.Arg)

			// to avoid the nightmare of comparing cty primitve types we can marshal them to json and compare that
			actualJSON, _ := json.Marshal(actual)
			expectedJSON, _ := json.Marshal(tc.Expected)
			if !cmp.Equal(actualJSON, expectedJSON) {
				t.Fatalf("values don't match:\n %v\n", cmp.Diff(string(actualJSON), string(expectedJSON)))
			}
		})
	}
}

func TestMarshalParameters(t *testing.T) {
	type testcase struct {
		Arg      []providers.FunctionParameterSpec
		Expected []FunctionParam
	}

	tests := map[string]testcase{
		"basic": {
			Arg: []providers.FunctionParameterSpec{{
				Description: "basic string func",
				Type:        cty.String,
			}},
			Expected: []FunctionParam{{
				Description: "basic string func",
				Type:        cty.String,
			}},
		},
	}

	for tn, tc := range tests {
		t.Run(tn, func(t *testing.T) {
			actual := marshalParameters(tc.Arg)

			// to avoid the nightmare of comparing cty primitve types we can marshal them to json and compare that
			actualJSON, _ := json.Marshal(actual)
			expectedJSON, _ := json.Marshal(tc.Expected)
			if !cmp.Equal(actualJSON, expectedJSON) {
				t.Fatalf("values don't match:\n %v\n", cmp.Diff(string(actualJSON), string(expectedJSON)))
			}
		})
	}
}

func TestMarshalFunction(t *testing.T) {
	type testcase struct {
		Arg      providers.FunctionSpec
		Expected Function
	}

	tests := map[string]testcase{
		"basic": {
			Arg: providers.FunctionSpec{
				Description: "basic string func",
				Return:      cty.String,
			},
			Expected: Function{
				Description: "basic string func",
				ReturnType:  cty.String,
			},
		},
		"variadic": {
			Arg: providers.FunctionSpec{
				Description: "basic string func",
				Return:      cty.String,
				VariadicParameter: &providers.FunctionParameterSpec{
					Description: "basic string func",
					Type:        cty.String,
				},
			},
			Expected: Function{
				Description: "basic string func",
				ReturnType:  cty.String,
				VariadicParameter: &FunctionParam{
					Description: "basic string func",
					Type:        cty.String,
				},
			},
		},
	}

	for tn, tc := range tests {
		t.Run(tn, func(t *testing.T) {
			actual := marshalFunction(tc.Arg)

			// to avoid the nightmare of comparing cty primitve types we can marshal them to json and compare that
			actualJSON, _ := json.Marshal(actual)
			expectedJSON, _ := json.Marshal(tc.Expected)
			if !cmp.Equal(actualJSON, expectedJSON) {
				t.Fatalf("values don't match:\n %v\n", cmp.Diff(string(actualJSON), string(expectedJSON)))
			}
		})
	}
}

func TestMarshalFunctions(t *testing.T) {
	type testcase struct {
		Arg      map[string]providers.FunctionSpec
		Expected map[string]Function
	}

	tests := map[string]testcase{
		"basic": {
			Arg: map[string]providers.FunctionSpec{"basic_func": {
				Description: "basic string func",
				Return:      cty.String,
			}},
			Expected: map[string]Function{"basic_func": {
				Description: "basic string func",
				ReturnType:  cty.String,
			}},
		},
	}

	for tn, tc := range tests {
		t.Run(tn, func(t *testing.T) {
			actual := marshalFunctions(tc.Arg)

			// to avoid the nightmare of comparing cty primitve types we can marshal them to json and compare that
			actualJSON, _ := json.Marshal(actual)
			expectedJSON, _ := json.Marshal(tc.Expected)
			if !cmp.Equal(actualJSON, expectedJSON) {
				t.Fatalf("values don't match:\n %v\n", cmp.Diff(string(actualJSON), string(expectedJSON)))
			}
		})
	}
}
