// Copyright 2016-2023, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package pulumix_test // to avoid import cycles

import (
	"reflect"
	"testing"

	"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumix"
	"github.com/stretchr/testify/require"
)

type testResourceInputs struct {
	PuxString pulumix.Input[string]
	PuString  pulumi.StringInput

	PuxStringPtr pulumix.Input[*string]
	PuStringPtr  pulumi.StringPtrInput

	PuxIntArray pulumix.Input[[]int]
	PuIntArray  pulumi.IntArrayInput

	PuxIntMap pulumix.Input[map[string]int]
	PuIntMap  pulumi.IntMapInput
}

func (*testResourceInputs) ElementType() reflect.Type {
	return reflect.TypeOf((*testResourceArgs)(nil))
}

type testResourceArgs struct {
	PuxString string `pulumi:"puxString"`
	PuString  string `pulumi:"puString"`

	PuxStringPtr *string `pulumi:"puxStringPtr"`
	PuStringPtr  *string `pulumi:"puStringPtr"`

	PuxIntArray []int `pulumi:"puxIntArray"`
	PuIntArray  []int `pulumi:"puIntArray"`

	PuxIntMap map[string]int `pulumi:"puxIntMap"`
	PuIntMap  map[string]int `pulumi:"puIntMap"`
}

func TestRegisterResource_inputSerialization(t *testing.T) {
	t.Parallel()

	j := "j"

	tests := []struct {
		desc string
		give pulumi.Input
		want resource.PropertyMap
	}{
		// --- pulumix.Input[string] ---
		{
			desc: "pux.String/pu.String",
			give: &testResourceInputs{PuxString: pulumi.String("a")},
			want: resource.PropertyMap{"puxString": resource.NewStringProperty("a")},
		},
		{
			desc: "pux.String/pu.StringOutput",
			give: &testResourceInputs{PuxString: pulumi.String("b").ToStringOutput()},
			want: resource.PropertyMap{"puxString": resource.NewStringProperty("b")},
		},
		{
			desc: "pux.String/pux.Output[string]",
			give: &testResourceInputs{PuxString: pulumix.Val("c")},
			want: resource.PropertyMap{"puxString": resource.NewStringProperty("c")},
		},

		// --- pulumi.StringInput ---
		{
			desc: "pu.String/pu.String",
			give: &testResourceInputs{PuString: pulumi.String("d")},
			want: resource.PropertyMap{"puString": resource.NewStringProperty("d")},
		},
		{
			desc: "pu.String/pu.StringOutput",
			give: &testResourceInputs{PuString: pulumi.String("e").ToStringOutput()},
			want: resource.PropertyMap{"puString": resource.NewStringProperty("e")},
		},
		{
			desc: "pu.String/pux.Output[string] untyped",
			give: &testResourceInputs{PuString: pulumix.Val("f").Untyped().(pulumi.StringOutput)},
			want: resource.PropertyMap{"puString": resource.NewStringProperty("f")},
		},

		// --- pulumix.Input[*string] ---
		{
			desc: "pux.StringPtr/pu.String PtrOf",
			give: &testResourceInputs{PuxStringPtr: pulumix.PtrOf[string](pulumi.String("g"))},
			want: resource.PropertyMap{"puxStringPtr": resource.NewStringProperty("g")},
		},
		{
			desc: "pux.StringPtr/pu.StringPtrOutput",
			give: &testResourceInputs{PuxStringPtr: pulumi.String("h").ToStringPtrOutput()},
			want: resource.PropertyMap{"puxStringPtr": resource.NewStringProperty("h")},
		},
		{
			desc: "pux.StringPtr/pux.PtrOutput[string]",
			give: &testResourceInputs{PuxStringPtr: pulumix.Ptr("i")},
			want: resource.PropertyMap{"puxStringPtr": resource.NewStringProperty("i")},
		},
		{
			desc: "pux.StringPtr/pux.Output[*string]",
			give: &testResourceInputs{PuxStringPtr: pulumix.Val[*string](&j)},
			want: resource.PropertyMap{"puxStringPtr": resource.NewStringProperty("j")},
		},

		// --- pulumi.StringPtrInput ---
		{
			desc: "pu.StringPtr/pu.StringPtr",
			give: &testResourceInputs{PuStringPtr: pulumi.StringPtr("j")},
			want: resource.PropertyMap{"puStringPtr": resource.NewStringProperty("j")},
		},
		{
			desc: "pu.StringPtr/pu.String",
			give: &testResourceInputs{PuStringPtr: pulumi.String("k")},
			want: resource.PropertyMap{"puStringPtr": resource.NewStringProperty("k")},
		},
		{
			desc: "pu.StringPtr/pu.StringPtrOutput",
			give: &testResourceInputs{PuStringPtr: pulumi.String("l").ToStringPtrOutput()},
			want: resource.PropertyMap{"puStringPtr": resource.NewStringProperty("l")},
		},
		{
			desc: "pu.StringPtr/pux.PtrOutput[string] untyped",
			give: &testResourceInputs{PuStringPtr: pulumix.Ptr("m").Untyped().(pulumi.StringPtrOutput)},
			want: resource.PropertyMap{"puStringPtr": resource.NewStringProperty("m")},
		},
		{
			desc: "pu.StringPtr/pux.GPtrOutput[string] untyped",
			give: &testResourceInputs{
				PuStringPtr: pulumix.Cast[pulumix.GPtrOutput[string, pulumi.StringOutput], *string](
					pulumix.Ptr("n"),
				).Untyped().(pulumi.StringPtrOutput),
			},
			want: resource.PropertyMap{"puStringPtr": resource.NewStringProperty("n")},
		},

		// --- pulumix.Input[[]int] ---
		{
			desc: "pux.IntArray/pu.IntArray",
			give: &testResourceInputs{
				PuxIntArray: pulumi.IntArray{
					pulumi.Int(1),
					pulumi.Int(2),
					pulumi.Int(3),
				},
			},
			want: resource.PropertyMap{
				"puxIntArray": resource.NewArrayProperty(
					[]resource.PropertyValue{
						resource.NewNumberProperty(1),
						resource.NewNumberProperty(2),
						resource.NewNumberProperty(3),
					},
				),
			},
		},
		{
			desc: "pux.IntArray/pu.IntArrayOutput",
			give: &testResourceInputs{
				PuxIntArray: pulumi.IntArray{
					pulumi.Int(4),
					pulumi.Int(5),
					pulumi.Int(6),
				}.ToIntArrayOutput(),
			},
			want: resource.PropertyMap{
				"puxIntArray": resource.NewArrayProperty(
					[]resource.PropertyValue{
						resource.NewNumberProperty(4),
						resource.NewNumberProperty(5),
						resource.NewNumberProperty(6),
					},
				),
			},
		},
		{
			desc: "pux.IntArray/pux.Output[[]int]",
			give: &testResourceInputs{
				PuxIntArray: pulumix.Val([]int{7, 8, 9}),
			},
			want: resource.PropertyMap{
				"puxIntArray": resource.NewArrayProperty(
					[]resource.PropertyValue{
						resource.NewNumberProperty(7),
						resource.NewNumberProperty(8),
						resource.NewNumberProperty(9),
					},
				),
			},
		},
		{
			desc: "pux.IntArray/pux.ArrayOutput[int]",
			give: &testResourceInputs{
				PuxIntArray: pulumix.Cast[pulumix.ArrayOutput[int], []int](
					pulumix.Val([]int{10, 11, 12}),
				),
			},
			want: resource.PropertyMap{
				"puxIntArray": resource.NewArrayProperty(
					[]resource.PropertyValue{
						resource.NewNumberProperty(10),
						resource.NewNumberProperty(11),
						resource.NewNumberProperty(12),
					},
				),
			},
		},
		{
			desc: "pux.IntArray/pux.GArrayOutput",
			give: &testResourceInputs{
				PuxIntArray: pulumix.Cast[pulumix.GArrayOutput[int, pulumi.IntOutput], []int](
					pulumix.Val([]int{13, 14, 15}),
				),
			},
			want: resource.PropertyMap{
				"puxIntArray": resource.NewArrayProperty(
					[]resource.PropertyValue{
						resource.NewNumberProperty(13),
						resource.NewNumberProperty(14),
						resource.NewNumberProperty(15),
					},
				),
			},
		},

		// --- pulumi.IntArrayInput ---
		{
			desc: "pu.IntArray/pux.Output[[]int] untyped",
			give: &testResourceInputs{
				PuIntArray: pulumix.Val([]int{1, 2, 3}).Untyped().(pulumi.IntArrayOutput),
			},
			want: resource.PropertyMap{
				"puIntArray": resource.NewArrayProperty(
					[]resource.PropertyValue{
						resource.NewNumberProperty(1),
						resource.NewNumberProperty(2),
						resource.NewNumberProperty(3),
					},
				),
			},
		},
		{
			desc: "pu.IntArray/pux.ArrayOutput[int] untyped",
			give: &testResourceInputs{
				PuIntArray: pulumix.Cast[pulumix.ArrayOutput[int], []int](
					pulumix.Val([]int{4, 5, 6}),
				).Untyped().(pulumi.IntArrayOutput),
			},
			want: resource.PropertyMap{
				"puIntArray": resource.NewArrayProperty(
					[]resource.PropertyValue{
						resource.NewNumberProperty(4),
						resource.NewNumberProperty(5),
						resource.NewNumberProperty(6),
					},
				),
			},
		},
		{
			desc: "pu.IntArray/pux.GArrayOutput untyped",
			give: &testResourceInputs{
				PuIntArray: pulumix.Cast[pulumix.GArrayOutput[int, pulumi.IntOutput], []int](
					pulumix.Val([]int{7, 8, 9}),
				).Untyped().(pulumi.IntArrayOutput),
			},
			want: resource.PropertyMap{
				"puIntArray": resource.NewArrayProperty(
					[]resource.PropertyValue{
						resource.NewNumberProperty(7),
						resource.NewNumberProperty(8),
						resource.NewNumberProperty(9),
					},
				),
			},
		},

		// --- pulumix.Input[map[string]int] ---
		{
			desc: "pux.IntMap/pu.IntMap",
			give: &testResourceInputs{
				PuxIntMap: pulumi.IntMap{"a": pulumi.Int(1), "b": pulumi.Int(2)},
			},
			want: resource.PropertyMap{
				"puxIntMap": resource.NewObjectProperty(
					resource.PropertyMap{
						"a": resource.NewNumberProperty(1),
						"b": resource.NewNumberProperty(2),
					},
				),
			},
		},
		{
			desc: "pux.IntMap/pu.IntMapOutput",
			give: &testResourceInputs{
				PuxIntMap: pulumi.IntMap{"c": pulumi.Int(3), "d": pulumi.Int(4)}.ToIntMapOutput(),
			},
			want: resource.PropertyMap{
				"puxIntMap": resource.NewObjectProperty(
					resource.PropertyMap{
						"c": resource.NewNumberProperty(3),
						"d": resource.NewNumberProperty(4),
					},
				),
			},
		},
		{
			desc: "pux.IntMap/pux.Output[map[string]int]",
			give: &testResourceInputs{
				PuxIntMap: pulumix.Val(map[string]int{"e": 5, "f": 6}),
			},
			want: resource.PropertyMap{
				"puxIntMap": resource.NewObjectProperty(
					resource.PropertyMap{
						"e": resource.NewNumberProperty(5),
						"f": resource.NewNumberProperty(6),
					},
				),
			},
		},
		{
			desc: "pux.IntMap/pux.MapOutput[int]",
			give: &testResourceInputs{
				PuxIntMap: pulumix.Cast[pulumix.MapOutput[int], map[string]int](
					pulumix.Val(map[string]int{"g": 7, "h": 8}),
				),
			},
			want: resource.PropertyMap{
				"puxIntMap": resource.NewObjectProperty(
					resource.PropertyMap{
						"g": resource.NewNumberProperty(7),
						"h": resource.NewNumberProperty(8),
					},
				),
			},
		},
		{
			desc: "pux.IntMap/pux.GMapOutput",
			give: &testResourceInputs{
				PuxIntMap: pulumix.Cast[pulumix.GMapOutput[int, pulumi.IntOutput], map[string]int](
					pulumix.Val(map[string]int{"i": 9, "j": 10}),
				),
			},
			want: resource.PropertyMap{
				"puxIntMap": resource.NewObjectProperty(
					resource.PropertyMap{
						"i": resource.NewNumberProperty(9),
						"j": resource.NewNumberProperty(10),
					},
				),
			},
		},

		// --- pulumi.IntMapInput ---
		{
			desc: "pu.IntMap/pu.IntMap",
			give: &testResourceInputs{
				PuIntMap: pulumi.IntMap{"a": pulumi.Int(1), "b": pulumi.Int(2)},
			},
			want: resource.PropertyMap{
				"puIntMap": resource.NewObjectProperty(
					resource.PropertyMap{
						"a": resource.NewNumberProperty(1),
						"b": resource.NewNumberProperty(2),
					},
				),
			},
		},
		{
			desc: "pu.IntMap/pux.Output[map[string]int] untyped",
			give: &testResourceInputs{
				PuIntMap: pulumix.Val(map[string]int{"c": 3, "d": 4}).Untyped().(pulumi.IntMapOutput),
			},
			want: resource.PropertyMap{
				"puIntMap": resource.NewObjectProperty(
					resource.PropertyMap{
						"c": resource.NewNumberProperty(3),
						"d": resource.NewNumberProperty(4),
					},
				),
			},
		},
		{
			desc: "pu.IntMap/pux.MapOutput[int] untyped",
			give: &testResourceInputs{
				PuIntMap: pulumix.Cast[pulumix.MapOutput[int], map[string]int](
					pulumix.Val(map[string]int{"e": 5, "f": 6}),
				).Untyped().(pulumi.IntMapOutput),
			},
			want: resource.PropertyMap{
				"puIntMap": resource.NewObjectProperty(
					resource.PropertyMap{
						"e": resource.NewNumberProperty(5),
						"f": resource.NewNumberProperty(6),
					},
				),
			},
		},
		{
			desc: "pu.IntMap/pux.GMapOutput untyped",
			give: &testResourceInputs{
				PuIntMap: pulumix.Cast[pulumix.GMapOutput[int, pulumi.IntOutput], map[string]int](
					pulumix.Val(map[string]int{"g": 7, "h": 8}),
				).Untyped().(pulumi.IntMapOutput),
			},
			want: resource.PropertyMap{
				"puIntMap": resource.NewObjectProperty(
					resource.PropertyMap{
						"g": resource.NewNumberProperty(7),
						"h": resource.NewNumberProperty(8),
					},
				),
			},
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.desc, func(t *testing.T) {
			t.Parallel()

			var (
				got resource.PropertyMap
				ok  bool
			)
			err := pulumi.RunErr(func(ctx *pulumi.Context) error {
				var res pulumi.CustomResourceState
				require.NoError(t,
					ctx.RegisterResource("test:index:MyResource", "testResource", tt.give, &res))
				return nil
			}, pulumi.WithMocks("project", "stack", &mockResourceMonitor{
				CallF: func(args pulumi.MockCallArgs) (resource.PropertyMap, error) {
					t.Fatalf("unexpected Call(%#v)", args)
					return nil, nil
				},
				NewResourceF: func(args pulumi.MockResourceArgs) (string, resource.PropertyMap, error) {
					if args.TypeToken == "test:index:MyResource" {
						got = args.Inputs
						ok = true
					} else {
						t.Fatalf("unexpected NewResource(%#v)", args)
					}
					return args.Name + "_id", nil, nil
				},
			}))
			require.NoError(t, err)
			require.True(t, ok)

			require.Equal(t, tt.want, got)
		})
	}
}

func TestRegisterResourceOutputs(t *testing.T) {
	t.Parallel()

	tests := []struct {
		desc string
		give pulumi.Input
		// TODO: find a way to intercept RegisterResourceOutput calls.
	}{
		{"pu.String", pulumi.String("a")},
		{"pu.StringOutput", pulumi.String("b").ToStringOutput()},
		{"pux.Output[string]", pulumix.Val("c")},
		{"pux.Output[string]/untyped", pulumix.Val("c").Untyped()},
		{"pux.PtrOf", pulumix.PtrOf[string](pulumi.String("d"))},
		{"pu.StringPtr", pulumi.StringPtr("e")},
		{"pu.StringPtrOutput", pulumi.String("f").ToStringPtrOutput()},
		{"pux.Output[*string]", pulumix.Ptr("g")},
		{"pux.Output[*string]/untyped", pulumix.Ptr("h").Untyped()},
		{"pux.Output[[]int]", pulumix.Val([]int{1, 2, 3})},
		{
			"pux.ArrayOutput[int]",
			pulumix.Cast[pulumix.ArrayOutput[int], []int](
				pulumix.Val([]int{4, 5, 6}),
			),
		},
		{
			"pux.GArrayOutput",
			pulumix.Cast[pulumix.GArrayOutput[int, pulumi.IntOutput], []int](
				pulumix.Val([]int{7, 8, 9}),
			),
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.desc, func(t *testing.T) {
			t.Parallel()

			err := pulumi.RunErr(func(ctx *pulumi.Context) error {
				ctx.Export("x", tt.give)
				return nil
			}, pulumi.WithMocks("project", "stack", &mockResourceMonitor{}))
			require.NoError(t, err)
		})
	}
}

type mockResourceMonitor struct {
	CallF        func(pulumi.MockCallArgs) (resource.PropertyMap, error)
	NewResourceF func(pulumi.MockResourceArgs) (string, resource.PropertyMap, error)
}

var _ pulumi.MockResourceMonitor = (*mockResourceMonitor)(nil)

func (m *mockResourceMonitor) Call(args pulumi.MockCallArgs) (resource.PropertyMap, error) {
	return m.CallF(args)
}

func (m *mockResourceMonitor) NewResource(args pulumi.MockResourceArgs) (string, resource.PropertyMap, error) {
	return m.NewResourceF(args)
}