package lifecycletest

import (
	"testing"

	"github.com/blang/semver"
	"github.com/stretchr/testify/assert"

	. "github.com/pulumi/pulumi/pkg/v3/engine" //nolint:revive
	"github.com/pulumi/pulumi/pkg/v3/resource/deploy/deploytest"
	"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
	"github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin"
)

// TestResourceReferences tests that resource references can be marshaled between the engine, language host,
// resource providers, and statefile if each entity supports resource references.
func TestResourceReferences(t *testing.T) {
	t.Parallel()

	var urnA resource.URN
	var urnB resource.URN
	var idB resource.ID

	loaders := []*deploytest.ProviderLoader{
		deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
			v := &deploytest.Provider{
				CreateF: func(urn resource.URN, news resource.PropertyMap,
					timeout float64, preview bool,
				) (resource.ID, resource.PropertyMap, resource.Status, error) {
					id := "created-id"
					if preview {
						id = ""
					}

					if urn.Name() == "resC" {
						assert.True(t, news.DeepEquals(resource.PropertyMap{
							"resA": resource.MakeComponentResourceReference(urnA, ""),
							"resB": resource.MakeCustomResourceReference(urnB, idB, ""),
						}))
					}

					return resource.ID(id), news, resource.StatusOK, nil
				},
				ReadF: func(urn resource.URN, id resource.ID,
					inputs, state resource.PropertyMap,
				) (plugin.ReadResult, resource.Status, error) {
					return plugin.ReadResult{Inputs: inputs, Outputs: state}, resource.StatusOK, nil
				},
			}
			return v, nil
		}),
	}

	programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
		var err error
		respA, err := monitor.RegisterResource("component", "resA", false)
		assert.NoError(t, err)
		urnA = respA.URN

		err = monitor.RegisterResourceOutputs(urnA, resource.PropertyMap{})
		assert.NoError(t, err)

		respB, err := monitor.RegisterResource("pkgA:m:typA", "resB", true)
		assert.NoError(t, err)
		urnB, idB = respB.URN, respB.ID

		resp, err := monitor.RegisterResource("pkgA:m:typA", "resC", true, deploytest.ResourceOptions{
			Inputs: resource.PropertyMap{
				"resA": resource.MakeComponentResourceReference(urnA, ""),
				"resB": resource.MakeCustomResourceReference(urnB, idB, ""),
			},
		})
		assert.NoError(t, err)

		assert.True(t, resp.Outputs.DeepEquals(resource.PropertyMap{
			"resA": resource.MakeComponentResourceReference(urnA, ""),
			"resB": resource.MakeCustomResourceReference(urnB, idB, ""),
		}))
		return nil
	})
	hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)

	p := &TestPlan{
		// Skip display tests because different ordering makes the colouring different.
		Options: TestUpdateOptions{T: t, HostF: hostF, SkipDisplayTests: true},
		Steps:   MakeBasicLifecycleSteps(t, 4),
	}
	p.Run(t, nil)
}

// TestResourceReferences_DownlevelSDK tests that resource references are properly marshaled as URNs (for references to
// component resources) or IDs (for references to custom resources) if the SDK does not support resource references.
func TestResourceReferences_DownlevelSDK(t *testing.T) {
	t.Parallel()

	var urnA resource.URN
	var urnB resource.URN
	var idB resource.ID

	loaders := []*deploytest.ProviderLoader{
		deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
			v := &deploytest.Provider{
				CreateF: func(urn resource.URN, news resource.PropertyMap,
					timeout float64, preview bool,
				) (resource.ID, resource.PropertyMap, resource.Status, error) {
					id := "created-id"
					if preview {
						id = ""
					}

					state := resource.PropertyMap{}
					if urn.Name() == "resC" {
						state = resource.PropertyMap{
							"resA": resource.MakeComponentResourceReference(urnA, ""),
							"resB": resource.MakeCustomResourceReference(urnB, idB, ""),
						}
					}

					return resource.ID(id), state, resource.StatusOK, nil
				},
				ReadF: func(urn resource.URN, id resource.ID,
					inputs, state resource.PropertyMap,
				) (plugin.ReadResult, resource.Status, error) {
					return plugin.ReadResult{Inputs: inputs, Outputs: state}, resource.StatusOK, nil
				},
			}
			return v, nil
		}),
	}

	opts := deploytest.ResourceOptions{DisableResourceReferences: true}
	programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
		var err error
		respA, err := monitor.RegisterResource("component", "resA", false, opts)
		assert.NoError(t, err)
		urnA = respA.URN

		err = monitor.RegisterResourceOutputs(urnA, resource.PropertyMap{})
		assert.NoError(t, err)

		respB, err := monitor.RegisterResource("pkgA:m:typA", "resB", true, opts)
		assert.NoError(t, err)
		urnB, idB = respB.URN, respB.ID

		respC, err := monitor.RegisterResource("pkgA:m:typA", "resC", true, opts)
		assert.NoError(t, err)

		assert.Equal(t, resource.NewStringProperty(string(urnA)), respC.Outputs["resA"])
		if idB != "" {
			assert.Equal(t, resource.NewStringProperty(string(idB)), respC.Outputs["resB"])
		} else {
			assert.True(t, respC.Outputs["resB"].IsComputed())
		}
		return nil
	})
	hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)

	p := &TestPlan{
		// Skip display tests because different ordering makes the colouring different.
		Options: TestUpdateOptions{T: t, HostF: hostF, SkipDisplayTests: true},
		Steps:   MakeBasicLifecycleSteps(t, 4),
	}
	p.Run(t, nil)
}

// TestResourceReferences_DownlevelEngine tests an SDK that supports resource references communicating with an engine
// that does not.
func TestResourceReferences_DownlevelEngine(t *testing.T) {
	t.Parallel()

	var urnA resource.URN
	var refB resource.PropertyValue

	loaders := []*deploytest.ProviderLoader{
		deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
			v := &deploytest.Provider{
				CreateF: func(urn resource.URN, news resource.PropertyMap,
					timeout float64, preview bool,
				) (resource.ID, resource.PropertyMap, resource.Status, error) {
					id := "created-id"
					if preview {
						id = ""
					}

					// If we have resource references here, the engine has not properly disabled them.
					if urn.Name() == "resC" {
						assert.Equal(t, resource.NewStringProperty(string(urnA)), news["resA"])
						assert.Equal(t, refB.ResourceReferenceValue().ID, news["resB"])
					}

					return resource.ID(id), news, resource.StatusOK, nil
				},
				ReadF: func(urn resource.URN, id resource.ID,
					inputs, state resource.PropertyMap,
				) (plugin.ReadResult, resource.Status, error) {
					return plugin.ReadResult{Inputs: inputs, Outputs: state}, resource.StatusOK, nil
				},
			}
			return v, nil
		}),
	}

	programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
		var err error
		respA, err := monitor.RegisterResource("component", "resA", false)
		assert.NoError(t, err)
		urnA = respA.URN

		err = monitor.RegisterResourceOutputs(urnA, resource.PropertyMap{})
		assert.NoError(t, err)

		respB, err := monitor.RegisterResource("pkgA:m:typA", "resB", true)
		assert.NoError(t, err)

		refB = resource.MakeCustomResourceReference(respB.URN, respB.ID, "")
		resp, err := monitor.RegisterResource("pkgA:m:typA", "resC", true, deploytest.ResourceOptions{
			Inputs: resource.PropertyMap{
				"resA": resource.MakeComponentResourceReference(urnA, ""),
				"resB": refB,
			},
		})
		assert.NoError(t, err)

		assert.Equal(t, resource.NewStringProperty(string(urnA)), resp.Outputs["resA"])
		if refB.ResourceReferenceValue().ID.IsComputed() {
			assert.True(t, resp.Outputs["resB"].IsComputed())
		} else {
			assert.True(t, refB.ResourceReferenceValue().ID.DeepEquals(resp.Outputs["resB"]))
		}
		return nil
	})

	hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)

	p := &TestPlan{
		// Skip display tests because different ordering makes the colouring different.
		Options: TestUpdateOptions{
			T:                t,
			HostF:            hostF,
			UpdateOptions:    UpdateOptions{DisableResourceReferences: true},
			SkipDisplayTests: true,
		},
		Steps: MakeBasicLifecycleSteps(t, 4),
	}
	p.Run(t, nil)
}

// TestResourceReferences_GetResource tests that invoking the built-in 'pulumi:pulumi:getResource' function
// returns resource references for any resource reference in a resource's state.
func TestResourceReferences_GetResource(t *testing.T) {
	t.Parallel()

	loaders := []*deploytest.ProviderLoader{
		deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
			v := &deploytest.Provider{
				CreateF: func(urn resource.URN, news resource.PropertyMap,
					timeout float64, preview bool,
				) (resource.ID, resource.PropertyMap, resource.Status, error) {
					id := "created-id"
					if preview {
						id = ""
					}
					return resource.ID(id), news, resource.StatusOK, nil
				},
				ReadF: func(urn resource.URN, id resource.ID,
					inputs, state resource.PropertyMap,
				) (plugin.ReadResult, resource.Status, error) {
					return plugin.ReadResult{Inputs: inputs, Outputs: state}, resource.StatusOK, nil
				},
			}
			return v, nil
		}),
	}

	programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
		childResp, err := monitor.RegisterResource("pkgA:m:typChild", "resChild", true)
		assert.NoError(t, err)

		refChild := resource.MakeCustomResourceReference(childResp.URN, childResp.ID, "")
		resp, err := monitor.RegisterResource("pkgA:m:typContainer", "resContainer", true,
			deploytest.ResourceOptions{
				Inputs: resource.PropertyMap{
					"child": refChild,
				},
			})
		assert.NoError(t, err)

		// Expect the `child` property from `resContainer`'s state to come back from 'pulumi:pulumi:getResource'
		// as a resource reference.
		result, failures, err := monitor.Invoke("pulumi:pulumi:getResource", resource.PropertyMap{
			"urn": resource.NewStringProperty(string(resp.URN)),
		}, "", "")
		assert.NoError(t, err)
		assert.Empty(t, failures)
		assert.Equal(t, resource.NewStringProperty(string(resp.URN)), result["urn"])
		assert.Equal(t, resource.NewStringProperty(string(resp.ID)), result["id"])
		state := result["state"].ObjectValue()
		assert.Equal(t, refChild, state["child"])

		return nil
	})

	hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)

	p := &TestPlan{
		// Skip display tests because different ordering makes the colouring different.
		Options: TestUpdateOptions{T: t, HostF: hostF, SkipDisplayTests: true},
		Steps:   MakeBasicLifecycleSteps(t, 4),
	}
	p.Run(t, nil)
}