// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package protohcl

import (
	"encoding/json"
	"fmt"
	"reflect"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
	"github.com/zclconf/go-cty/cty"
	"github.com/zclconf/go-cty/cty/function"
	"google.golang.org/protobuf/proto"
	"google.golang.org/protobuf/types/known/anypb"

	"github.com/hashicorp/consul/internal/protohcl/testproto"
	"github.com/hashicorp/hcl/v2/hclparse"
)

func TestPrimitives(t *testing.T) {
	hcl := `
		double_val = 1.234
  		float_val = 2.345
  		int32_val = 536870912
  		int64_val = 25769803776
  		uint32_val = 2148532224
  		uint64_val = 9223372041149743104
  		sint32_val = 536870912
  		sint64_val = 25769803776
  		fixed32_val = 2148532224
  		fixed64_val = 9223372041149743104
  		sfixed32_val = 536870912
  		sfixed64_val = 25769803776
  		bool_val = true
  		string_val = "foo"
		// This is base64 encoded "bar"
  		byte_val = "YmFy"
	`

	var out testproto.Primitives

	err := Unmarshal([]byte(hcl), &out)
	require.NoError(t, err)

	require.Equal(t, out.DoubleVal, float64(1.234))
	require.Equal(t, out.FloatVal, float32(2.345))
	require.Equal(t, out.Int32Val, int32(536870912))
	require.Equal(t, out.Int64Val, int64(25769803776))
	require.Equal(t, out.Uint32Val, uint32(2148532224))
	require.Equal(t, out.Uint64Val, uint64(9223372041149743104))
	require.Equal(t, out.Sint32Val, int32(536870912))
	require.Equal(t, out.Sint64Val, int64(25769803776))
	require.Equal(t, out.Fixed32Val, uint32(2148532224))
	require.Equal(t, out.Fixed64Val, uint64(9223372041149743104))
	require.Equal(t, out.Sfixed32Val, int32(536870912))
	require.Equal(t, out.Sfixed64Val, int64(25769803776))
	require.Equal(t, out.BoolVal, true)
	require.Equal(t, out.StringVal, "foo")
	require.Equal(t, out.ByteVal, []byte("bar"))
}

func TestNestedAndCollections(t *testing.T) {
	hcl := `
		primitives {
			uint32_val = 42
		}
		
		primitives_map "foo" {
			uint32_val = 42
		}
		
		protocol_map = {
			"foo" = "PROTOCOL_TCP"
		}
		
		primitives_list {
			uint32_val = 42
		}
		
		primitives_list {
			uint32_val = 56
		}
		
		int_list = [
			1,
			2
		]
	
	`

	var out testproto.NestedAndCollections

	err := Unmarshal([]byte(hcl), &out)
	require.NoError(t, err)

	require.NotNil(t, out.Primitives)
	require.Equal(t, out.Primitives.Uint32Val, uint32(42))
	require.NotNil(t, out.PrimitivesMap)
	require.Equal(t, out.PrimitivesMap["foo"].Uint32Val, uint32(42))
	require.NotNil(t, out.ProtocolMap)
	require.Equal(t, out.ProtocolMap["foo"], testproto.Protocol_PROTOCOL_TCP)
	require.Len(t, out.PrimitivesList, 2)
	require.Equal(t, out.PrimitivesList[0].Uint32Val, uint32(42))
	require.Equal(t, out.PrimitivesList[1].Uint32Val, uint32(56))
	require.Len(t, out.IntList, 2)
	require.Equal(t, out.IntList[1], int32(2))
}

func TestNestedAndCollections_AttributeSyntax(t *testing.T) {
	hcl := `
		primitives {
			uint32_val = 42
		}
		
		primitives_map = {
			"foo" = {
			   uint32_val = 42
			}
		}
		
		protocol_map = {
			"foo" = "PROTOCOL_TCP"
		}
		
		primitives_list = [
			{
				uint32_val = 42
			},
			{
				uint32_val = 56
			}
		] 
		
		int_list = [
			1,
			2
		]
	
	`

	var out testproto.NestedAndCollections

	err := Unmarshal([]byte(hcl), &out)
	require.NoError(t, err)

	require.NotNil(t, out.Primitives)
	require.Equal(t, out.Primitives.Uint32Val, uint32(42))
	require.NotNil(t, out.PrimitivesMap)
	require.Equal(t, out.PrimitivesMap["foo"].Uint32Val, uint32(42))
	require.NotNil(t, out.ProtocolMap)
	require.Equal(t, out.ProtocolMap["foo"], testproto.Protocol_PROTOCOL_TCP)
	require.Len(t, out.PrimitivesList, 2)
	require.Equal(t, out.PrimitivesList[0].Uint32Val, uint32(42))
	require.Equal(t, out.PrimitivesList[1].Uint32Val, uint32(56))
	require.Len(t, out.IntList, 2)
	require.Equal(t, out.IntList[1], int32(2))
}

func TestPrimitiveWrappers(t *testing.T) {
	hcl := `
		double_val = 1.234
  		float_val = 2.345
  		int32_val = 536870912
  		int64_val = 25769803776
  		uint32_val = 2148532224
  		uint64_val = 9223372041149743104
  		bool_val = true
  		string_val = "foo"
		// This is base64 encoded "bar"
  		bytes_val = "YmFy"
	`
	var out testproto.Wrappers

	err := Unmarshal([]byte(hcl), &out)
	require.NoError(t, err)
	require.Equal(t, out.DoubleVal.Value, float64(1.234))
	require.Equal(t, out.FloatVal.Value, float32(2.345))
	require.Equal(t, out.Int32Val.Value, int32(536870912))
	require.Equal(t, out.Int64Val.Value, int64(25769803776))
	require.Equal(t, out.Uint32Val.Value, uint32(2148532224))
	require.Equal(t, out.Uint64Val.Value, uint64(9223372041149743104))
	require.Equal(t, out.BoolVal.Value, true)
	require.Equal(t, out.StringVal.Value, "foo")
	require.Equal(t, out.BytesVal.Value, []byte("bar"))
}

func TestNonDynamicWellKnown(t *testing.T) {
	hcl := `
		empty_val = {}
		timestamp_val = "2023-02-27T12:34:56.789Z"
		duration_val = "12s"
	`
	var out testproto.NonDynamicWellKnown

	err := Unmarshal([]byte(hcl), &out)
	require.NoError(t, err)
	require.NotNil(t, out.EmptyVal)
	require.NotNil(t, out.TimestampVal)
	require.Equal(t, out.TimestampVal.AsTime(), time.Date(2023, 2, 27, 12, 34, 56, 789000000, time.UTC))
	require.NotNil(t, out.DurationVal)
	require.Equal(t, out.DurationVal.AsDuration(), time.Second*12)
}

func TestInvalidTimestamp(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()

	cases := map[string]struct {
		hcl       string
		expectXDS bool
	}{
		"invalid": {
			hcl: `
				timestamp_val = "Sat Jun 12 2023 14:59:57 GMT+0200"
			`,
		},
		"range error": {
			hcl: `
				timestamp_val = "2023-02-27T25:34:56.789Z"
			`,
		},
	}

	for name, tc := range cases {
		tc := tc
		var out testproto.NonDynamicWellKnown
		t.Run(name, func(t *testing.T) {

			err := Unmarshal([]byte(tc.hcl), &out)
			require.Error(t, err)
			require.Nil(t, out.TimestampVal)
			require.ErrorContains(t, err, "error parsing timestamp")
		})
	}
}

func TestInvalidDuration(t *testing.T) {
	hcl := `
		duration_val = "abc"
	`
	var out testproto.NonDynamicWellKnown

	err := Unmarshal([]byte(hcl), &out)
	require.ErrorContains(t, err, "error parsing string duration:")
	require.Nil(t, out.DurationVal)
}

func TestOneOf(t *testing.T) {
	hcl1 := `
		int32_val = 3
	`

	hcl2 := `
		primitives {
			int32_val = 3
		}
	`

	hcl3 := `
		int32_val = 3
		primitives {
			int32_val = 4
		}
	`

	var out testproto.OneOf

	err := Unmarshal([]byte(hcl1), &out)
	require.NoError(t, err)
	require.Equal(t, out.GetInt32Val(), int32(3))

	err = Unmarshal([]byte(hcl2), &out)
	require.NoError(t, err)
	primitives := out.GetPrimitives()
	require.NotNil(t, primitives)
	require.Equal(t, primitives.Int32Val, int32(3))

	err = Unmarshal([]byte(hcl3), &out)
	require.Error(t, err)
}

func TestAny(t *testing.T) {
	hcl := `
		any_val {
		    type_url = "hashicorp.consul.internal.protohcl.testproto.Primitives"
		    uint32_val = 42
		}

		any_list = [
			{
				type_url = "hashicorp.consul.internal.protohcl.testproto.Primitives"
				uint32_val = 123
			},
			{
				type_url = "hashicorp.consul.internal.protohcl.testproto.Wrappers"
				uint32_val = 321
			}
		]
	`
	var out testproto.DynamicWellKnown

	err := Unmarshal([]byte(hcl), &out)
	require.NoError(t, err)
	require.NotNil(t, out.AnyVal)
	require.Equal(t, out.AnyVal.TypeUrl, "hashicorp.consul.internal.protohcl.testproto.Primitives")

	raw, err := anypb.UnmarshalNew(out.AnyVal, proto.UnmarshalOptions{})
	require.NoError(t, err)
	require.NotNil(t, raw)

	primitives, ok := raw.(*testproto.Primitives)
	require.True(t, ok)
	require.Equal(t, primitives.Uint32Val, uint32(42))
}

func TestAnyTypeDynamicWellKnown(t *testing.T) {
	hcl := `
		any_val {
			type_url = "hashicorp.consul.internal.protohcl.testproto.DynamicWellKnown"
		    any_val {
				type_url = "hashicorp.consul.internal.protohcl.testproto.Primitives"
				uint32_val = 42
			}
		}
	`
	var out testproto.DynamicWellKnown

	err := Unmarshal([]byte(hcl), &out)
	require.NoError(t, err)
	require.NotNil(t, out.AnyVal)
	require.Equal(t, out.AnyVal.TypeUrl, "hashicorp.consul.internal.protohcl.testproto.DynamicWellKnown")

	raw, err := anypb.UnmarshalNew(out.AnyVal, proto.UnmarshalOptions{})
	require.NoError(t, err)
	require.NotNil(t, raw)

	anyVal, ok := raw.(*testproto.DynamicWellKnown)
	require.True(t, ok)

	res, err := anypb.UnmarshalNew(anyVal.AnyVal, proto.UnmarshalOptions{})
	require.NoError(t, err)
	require.NotNil(t, res)

	primitives, ok := res.(*testproto.Primitives)
	require.True(t, ok)
	require.Equal(t, primitives.Uint32Val, uint32(42))
}

func TestAnyTypeNestedAndCollections(t *testing.T) {
	hcl := `
		any_val {
			type_url = "hashicorp.consul.internal.protohcl.testproto.NestedAndCollections"
		    primitives {
				uint32_val = 42
			}
		}
	`
	var out testproto.DynamicWellKnown

	err := Unmarshal([]byte(hcl), &out)
	require.NoError(t, err)
	require.NotNil(t, out.AnyVal)
	require.Equal(t, out.AnyVal.TypeUrl, "hashicorp.consul.internal.protohcl.testproto.NestedAndCollections")

	raw, err := anypb.UnmarshalNew(out.AnyVal, proto.UnmarshalOptions{})
	require.NoError(t, err)
	require.NotNil(t, raw)

	nestedCollections, ok := raw.(*testproto.NestedAndCollections)
	require.True(t, ok)
	require.NotNil(t, nestedCollections.Primitives)
	require.Equal(t, nestedCollections.Primitives.Uint32Val, uint32(42))
}

func TestAnyTypeErrors(t *testing.T) {
	type testCase struct {
		description string
		hcl         string
		error       string
	}
	testCases := []testCase{
		{
			description: "type_url is expected",
			hcl: `
			  any_val {
				uint32_val = 42
			}
			`,
			error: "type_url field is required to decode Any",
		},
		{
			description: "type_url is unknown",
			hcl: `
			  any_val {
				type_url = "hashicorp.consul.internal.protohcl.testproto.Integer"
				uint32_val = 42
			}
			`,
			error: "error looking up type information for hashicorp.consul.internal.protohcl.testproto.Integer",
		},
		{
			description: "unknown field",
			hcl: `
			  any_val {
				type_url = "hashicorp.consul.internal.protohcl.testproto.Primitives"
				int_val = 42
			}
			`,
			error: "Unsupported argument; An argument named \"int_val\" is not expected here",
		},
	}

	for _, tc := range testCases {
		tc := tc
		t.Run(tc.description, func(t *testing.T) {
			t.Parallel()
			var out testproto.DynamicWellKnown

			err := Unmarshal([]byte(tc.hcl), &out)
			require.Error(t, err)
			require.Contains(t, err.Error(), tc.error)
		})
	}
}

func TestStruct(t *testing.T) {
	hcl := `
		struct_val = {
			"null"= null
			"bool"= true
			"foo" = "bar"
			"baz" = 1.234
			"nested" = {
				"foo" = 12,
				"bar" = "something"
			}
		}
	`

	var out testproto.DynamicWellKnown

	err := Unmarshal([]byte(hcl), &out)
	require.NoError(t, err)
	require.NotNil(t, out.StructVal)

	valMap := out.StructVal.AsMap()
	jsonVal, err := json.Marshal(valMap)
	require.NoError(t, err)

	expected := `{
		"null": null,
		"bool": true,
		"foo": "bar",
		"baz": 1.234,
		"nested": {
			"foo": 12,
			"bar": "something"
		}
	}
	`
	require.JSONEq(t, expected, string(jsonVal))
}

func TestStructList(t *testing.T) {
	hcl := `
		struct_val = {
			"list_int" = [
				1,
				2,
				3,
			]
			"list_string": [
				"abc",
				"def"
			]
			"list_bool": [
				true,
				false
			]
			"list_maps" = [
				{
					"arrr" = "matey"
				},
				{
					"hoist" = "the colors"
				}
			]
			"list_list" = [
				[
					"hello",
					"world",
					null
				]
			]
		}
	`

	var out testproto.DynamicWellKnown

	err := Unmarshal([]byte(hcl), &out)
	require.NoError(t, err)
	require.NotNil(t, out.StructVal)

	valMap := out.StructVal.AsMap()
	jsonVal, err := json.Marshal(valMap)
	require.NoError(t, err)

	expected := `{
		"list_int": [
			1,
			2,
			3
		],
		"list_string": [
			"abc",
			"def"
		],
		"list_bool": [
			true,
			false
		],
		"list_maps": [
			{
				"arrr": "matey"
			},
			{
				"hoist": "the colors"
			}
		],
		"list_list": [
			[
				"hello",
				"world",
				null
			]
		]
	}
	`
	require.JSONEq(t, expected, string(jsonVal))
}

func TestFunctionExecution(t *testing.T) {
	hcl := `
		primitives = primitive_defaults()
	`

	var out testproto.NestedAndCollections

	var (
		testType = cty.Capsule("type", reflect.TypeOf(testproto.Primitives{}))

		test = function.New(&function.Spec{
			Params: []function.Parameter{},
			Type:   function.StaticReturnType(testType),
			Impl: func(args []cty.Value, _ cty.Type) (cty.Value, error) {
				t := &testproto.Primitives{
					StringVal: "test",
					Int32Val:  10,
					BoolVal:   false,
				}
				return cty.CapsuleVal(testType, t), nil
			},
		})
	)

	err := UnmarshalOptions{
		Functions: map[string]function.Function{"primitive_defaults": test},
	}.Unmarshal([]byte(hcl), &out)

	require.NoError(t, err)

	require.NotNil(t, out.Primitives)
	require.Equal(t, out.Primitives.StringVal, "test")
	require.Equal(t, out.Primitives.Int32Val, int32(10))
	require.Equal(t, out.Primitives.BoolVal, false)
}

func TestSkipFields(t *testing.T) {

	u := UnmarshalOptions{}

	hcl := `
		any_val {
			type_url = "hashicorp.consul.internal.protohcl.testproto.Primitives"
			uint32_val = 10
		}`

	file, diags := hclparse.NewParser().ParseHCL([]byte(hcl), "")

	require.False(t, diags.HasErrors())

	decoder := u.bodyDecoder(file.Body)

	decoder = decoder.SkipFields("type_url")

	decoder = decoder.SkipFields("type_url", "uint32_val")

	expected := map[string]struct{}{
		"type_url":   {},
		"uint32_val": {},
	}

	require.Contains(t, fmt.Sprintf("%v", decoder), fmt.Sprintf("%v", expected))
}
