package lifecycletest

import (
	"errors"
	"testing"

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

	. "github.com/pulumi/pulumi/pkg/v3/engine" //nolint:revive
	"github.com/pulumi/pulumi/pkg/v3/resource/deploy"
	"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"
	"github.com/pulumi/pulumi/sdk/v3/go/common/workspace"
)

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

	readInputs := resource.PropertyMap{
		"foo": resource.NewStringProperty("bar"),
	}
	readOutputs := resource.PropertyMap{
		"foo": resource.NewStringProperty("bar"),
		"out": resource.NewNumberProperty(41),
	}

	// For imports we expect inputs and state to be nil, but when we change to do a read they should both be set to the
	// resource inputs.
	var expectedInputs, expectedState resource.PropertyMap
	loaders := []*deploytest.ProviderLoader{
		deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
			return &deploytest.Provider{
				DiffF: func(urn resource.URN, id resource.ID,
					oldInputs, oldOutputs, newInputs resource.PropertyMap, ignoreChanges []string,
				) (plugin.DiffResult, error) {
					if oldOutputs["foo"].DeepEquals(newInputs["foo"]) {
						return plugin.DiffResult{Changes: plugin.DiffNone}, nil
					}

					diffKind := plugin.DiffUpdate
					if newInputs["foo"].IsString() && newInputs["foo"].StringValue() == "replace" {
						diffKind = plugin.DiffUpdateReplace
					}

					return plugin.DiffResult{
						Changes: plugin.DiffSome,
						DetailedDiff: map[string]plugin.PropertyDiff{
							"foo": {Kind: diffKind},
						},
					}, nil
				},
				CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64,
					preview bool,
				) (resource.ID, resource.PropertyMap, resource.Status, error) {
					return "created-id", news, resource.StatusOK, nil
				},
				ReadF: func(urn resource.URN, id resource.ID,
					inputs, state resource.PropertyMap,
				) (plugin.ReadResult, resource.Status, error) {
					assert.Equal(t, expectedInputs, inputs)
					assert.Equal(t, expectedState, state)

					return plugin.ReadResult{
						Inputs:  readInputs,
						Outputs: readOutputs,
					}, resource.StatusOK, nil
				},
			}, nil
		}),
	}

	readID, importID, inputs := resource.ID(""), resource.ID("id"), resource.PropertyMap{}
	programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
		var err error
		if readID != "" {
			_, _, err = monitor.ReadResource("pkgA:m:typA", "resA", readID, "", inputs, "", "", "")
		} else {
			_, err = monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{
				Inputs:   inputs,
				ImportID: importID,
			})
		}
		assert.NoError(t, err)
		return nil
	})
	hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)

	p := &TestPlan{
		Options: TestUpdateOptions{HostF: hostF},
	}
	provURN := p.NewProviderURN("pkgA", "default", "")
	resURN := p.NewURN("pkgA:m:typA", "resA", "")

	// Run the initial update. The import should fail due to a mismatch in inputs between the program and the
	// actual resource state.
	project := p.GetProject()
	_, err := TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil)
	assert.ErrorContains(t, err, "step application failed: inputs to import do not match the existing resource")

	// Run a second update after fixing the inputs. The import should succeed.
	inputs["foo"] = resource.NewStringProperty("bar")
	snap, err := TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient,
		func(_ workspace.Project, _ deploy.Target, entries JournalEntries, _ []Event, err error) error {
			for _, entry := range entries {
				switch urn := entry.Step.URN(); urn {
				case provURN:
					assert.Equal(t, deploy.OpCreate, entry.Step.Op())
				case resURN:
					assert.Equal(t, deploy.OpImport, entry.Step.Op())
				default:
					t.Fatalf("unexpected resource %v", urn)
				}
			}
			return err
		})
	assert.NoError(t, err)
	assert.Len(t, snap.Resources, 2)
	assert.Equal(t, readInputs, snap.Resources[1].Inputs)
	assert.Equal(t, readOutputs, snap.Resources[1].Outputs)

	// Now, run another update. The update should succeed and there should be no diffs.
	snap, err = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient,
		func(_ workspace.Project, _ deploy.Target, entries JournalEntries, _ []Event, err error) error {
			for _, entry := range entries {
				switch urn := entry.Step.URN(); urn {
				case provURN, resURN:
					assert.Equal(t, deploy.OpSame, entry.Step.Op())
				default:
					t.Fatalf("unexpected resource %v", urn)
				}
			}
			return err
		})
	assert.NoError(t, err)
	assert.Equal(t, readInputs, snap.Resources[1].Inputs)
	assert.Equal(t, readOutputs, snap.Resources[1].Outputs)

	// Change a property value and run a third update. The update should succeed.
	inputs["foo"] = resource.NewStringProperty("rab")
	snap, err = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient,
		func(_ workspace.Project, _ deploy.Target, entries JournalEntries, _ []Event, err error) error {
			for _, entry := range entries {
				switch urn := entry.Step.URN(); urn {
				case provURN:
					assert.Equal(t, deploy.OpSame, entry.Step.Op())
				case resURN:
					assert.Equal(t, deploy.OpUpdate, entry.Step.Op())
				default:
					t.Fatalf("unexpected resource %v", urn)
				}
			}
			return err
		})
	assert.NoError(t, err)
	// This should call update not read, which just returns the passed inputs as outputs.
	assert.Equal(t, inputs, snap.Resources[1].Inputs)
	assert.Equal(t, inputs, snap.Resources[1].Outputs)

	// Change the property value s.t. the resource requires replacement. The update should fail.
	inputs["foo"] = resource.NewStringProperty("replace")
	_, err = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, nil)
	assert.ErrorContains(t, err, "reviously-imported resources that still specify an ID may not be replaced")

	// Finally, destroy the stack. The `Delete` function should be called.
	_, err = TestOp(Destroy).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient,
		func(_ workspace.Project, _ deploy.Target, entries JournalEntries, _ []Event, err error) error {
			for _, entry := range entries {
				switch urn := entry.Step.URN(); urn {
				case provURN, resURN:
					assert.Equal(t, deploy.OpDelete, entry.Step.Op())
				default:
					t.Fatalf("unexpected resource %v", urn)
				}
			}
			return err
		})
	assert.NoError(t, err)

	// Now clear the ID to import and run an initial update to create a resource that we will import-replace.
	importID, inputs["foo"] = "", resource.NewStringProperty("bar")
	snap, err = TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient,
		func(_ workspace.Project, _ deploy.Target, entries JournalEntries, _ []Event, err error) error {
			for _, entry := range entries {
				switch urn := entry.Step.URN(); urn {
				case provURN, resURN:
					assert.Equal(t, deploy.OpCreate, entry.Step.Op())
				default:
					t.Fatalf("unexpected resource %v", urn)
				}
			}
			return err
		})
	assert.NoError(t, err)
	assert.Len(t, snap.Resources, 2)
	// This will have just called create which returns the inputs as outputs.
	assert.Equal(t, inputs, snap.Resources[1].Inputs)
	assert.Equal(t, inputs, snap.Resources[1].Outputs)

	// Set the import ID to the same ID as the existing resource and run an update. This should produce no changes.
	for _, r := range snap.Resources {
		if r.URN == resURN {
			importID = r.ID
		}
	}
	snap, err = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient,
		func(_ workspace.Project, _ deploy.Target, entries JournalEntries, _ []Event, err error) error {
			for _, entry := range entries {
				switch urn := entry.Step.URN(); urn {
				case provURN, resURN:
					assert.Equal(t, deploy.OpSame, entry.Step.Op())
				default:
					t.Fatalf("unexpected resource %v", urn)
				}
			}
			return err
		})
	assert.NoError(t, err)
	// This will have 'same'd so the inputs and outputs will be the same as the lat run with create.
	assert.Equal(t, inputs, snap.Resources[1].Inputs)
	assert.Equal(t, inputs, snap.Resources[1].Outputs)

	// Then set the import ID and run another update. The update should succeed and should show an import-replace and
	// a delete-replaced.
	importID = "id"
	snap, err = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient,
		func(_ workspace.Project, _ deploy.Target, entries JournalEntries, _ []Event, err error) error {
			for _, entry := range entries {
				switch urn := entry.Step.URN(); urn {
				case provURN:
					assert.Equal(t, deploy.OpSame, entry.Step.Op())
				case resURN:
					switch entry.Step.Op() {
					case deploy.OpReplace, deploy.OpImportReplacement:
						assert.Equal(t, importID, entry.Step.New().ID)
					case deploy.OpDeleteReplaced:
						assert.NotEqual(t, importID, entry.Step.Old().ID)
					}
				default:
					t.Fatalf("unexpected resource %v", urn)
				}
			}
			return err
		})
	assert.NoError(t, err)
	assert.Equal(t, readInputs, snap.Resources[1].Inputs)
	assert.Equal(t, readOutputs, snap.Resources[1].Outputs)

	// Change the program to read a resource rather than creating one.
	readID = "id"
	expectedInputs, expectedState = inputs, inputs
	snap, err = TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient,
		func(_ workspace.Project, _ deploy.Target, entries JournalEntries, _ []Event, err error) error {
			for _, entry := range entries {
				switch urn := entry.Step.URN(); urn {
				case provURN:
					assert.Equal(t, deploy.OpCreate, entry.Step.Op())
				case resURN:
					assert.Equal(t, deploy.OpRead, entry.Step.Op())
				default:
					t.Fatalf("unexpected resource %v", urn)
				}
			}
			return err
		})
	assert.NoError(t, err)
	assert.Len(t, snap.Resources, 2)
	assert.Equal(t, readInputs, snap.Resources[1].Inputs)
	assert.Equal(t, readOutputs, snap.Resources[1].Outputs)

	// Now have the program import the resource. We should see an import-replace and a read-discard.
	readID, importID = "", readID
	expectedInputs, expectedState = nil, nil
	_, err = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient,
		func(_ workspace.Project, _ deploy.Target, entries JournalEntries, _ []Event, err error) error {
			for _, entry := range entries {
				switch urn := entry.Step.URN(); urn {
				case provURN:
					assert.Equal(t, deploy.OpSame, entry.Step.Op())
				case resURN:
					switch entry.Step.Op() {
					case deploy.OpReplace, deploy.OpImportReplacement:
						assert.Equal(t, importID, entry.Step.New().ID)
					case deploy.OpDiscardReplaced:
						assert.Equal(t, importID, entry.Step.Old().ID)
					}
				default:
					t.Fatalf("unexpected resource %v", urn)
				}
			}
			return err
		})
	assert.NoError(t, err)
	assert.Equal(t, readInputs, snap.Resources[1].Inputs)
	assert.Equal(t, readOutputs, snap.Resources[1].Outputs)
}

// TestImportWithDifferingImportIdentifierFormat tests importing a resource that has a different format of identifier
// for the import input than for the ID property, ensuring that a second update does not result in a replace.
func TestImportWithDifferingImportIdentifierFormat(t *testing.T) {
	t.Parallel()

	loaders := []*deploytest.ProviderLoader{
		deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
			return &deploytest.Provider{
				DiffF: func(urn resource.URN, id resource.ID,
					oldInputs, oldOutputs, newInputs resource.PropertyMap, ignoreChanges []string,
				) (plugin.DiffResult, error) {
					if oldOutputs["foo"].DeepEquals(newInputs["foo"]) {
						return plugin.DiffResult{Changes: plugin.DiffNone}, nil
					}

					return plugin.DiffResult{
						Changes: plugin.DiffSome,
						DetailedDiff: map[string]plugin.PropertyDiff{
							"foo": {Kind: plugin.DiffUpdate},
						},
					}, nil
				},
				CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64,
					preview bool,
				) (resource.ID, resource.PropertyMap, resource.Status, error) {
					return "created-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{
						// This ID is deliberately not the same as the ID used to import.
						ID: "id",
						Inputs: resource.PropertyMap{
							"foo": resource.NewStringProperty("bar"),
						},
						Outputs: resource.PropertyMap{
							"foo": resource.NewStringProperty("bar"),
						},
					}, resource.StatusOK, nil
				},
			}, nil
		}),
	}

	programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
		_, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{
			Inputs: resource.PropertyMap{
				"foo": resource.NewStringProperty("bar"),
			},
			// The import ID is deliberately not the same as the ID returned from Read.
			ImportID: resource.ID("import-id"),
		})
		assert.NoError(t, err)
		return nil
	})
	hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)

	p := &TestPlan{
		Options: TestUpdateOptions{HostF: hostF},
	}
	provURN := p.NewProviderURN("pkgA", "default", "")
	resURN := p.NewURN("pkgA:m:typA", "resA", "")

	// Run the initial update. The import should succeed.
	project := p.GetProject()
	snap, err := TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient,
		func(_ workspace.Project, _ deploy.Target, entries JournalEntries, _ []Event, err error) error {
			for _, entry := range entries {
				switch urn := entry.Step.URN(); urn {
				case provURN:
					assert.Equal(t, deploy.OpCreate, entry.Step.Op())
				case resURN:
					assert.Equal(t, deploy.OpImport, entry.Step.Op())
				default:
					t.Fatalf("unexpected resource %v", urn)
				}
			}
			return err
		})
	assert.NoError(t, err)
	assert.Len(t, snap.Resources, 2)

	// Now, run another update. The update should succeed and there should be no diffs.
	_, err = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient,
		func(_ workspace.Project, _ deploy.Target, entries JournalEntries, _ []Event, err error) error {
			for _, entry := range entries {
				switch urn := entry.Step.URN(); urn {
				case provURN, resURN:
					assert.Equal(t, deploy.OpSame, entry.Step.Op())
				default:
					t.Fatalf("unexpected resource %v", urn)
				}
			}
			return err
		})
	assert.NoError(t, err)
}

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

	p := &TestPlan{}

	provURN := p.NewProviderURN("pkgA", "default", "")
	resURN := p.NewURN("pkgA:m:typA", "resA", "")
	importID := resource.ID("myID")
	actualID := resource.ID("myNewID")

	loaders := []*deploytest.ProviderLoader{
		deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
			return &deploytest.Provider{
				ReadF: func(
					urn resource.URN, id resource.ID, inputs, state resource.PropertyMap,
				) (plugin.ReadResult, resource.Status, error) {
					return plugin.ReadResult{
						ID:      actualID,
						Outputs: resource.PropertyMap{},
						Inputs:  resource.PropertyMap{},
					}, resource.StatusOK, nil
				},
			}, nil
		}),
	}

	programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
		resp, err := monitor.RegisterResource("pkgA:m:typA", "resA", false, deploytest.ResourceOptions{
			ImportID: importID,
		})
		assert.NoError(t, err)
		assert.Equal(t, actualID, resp.ID)
		return nil
	})
	p.Options.HostF = deploytest.NewPluginHostF(nil, nil, programF, loaders...)

	p.Steps = []TestStep{{Op: Refresh, SkipPreview: true}}

	// Refresh requires at least one resource in order to proceed.
	stackURN := resource.URN("urn:pulumi:stack::stack::pulumi:pulumi:Stack::foo")
	stackResource := newResource(
		stackURN,
		"",
		"foo",
		"",
		nil,
		nil,
		nil,
		false,
	)
	snap := p.Run(t, &deploy.Snapshot{Resources: []*resource.State{stackResource}})

	require.NotEmpty(t, snap.Resources)

	for _, resource := range snap.Resources {
		switch urn := resource.URN; urn {
		case provURN, stackURN:
			// continue
		case resURN:
			assert.Equal(t, actualID, resource.ID)
		default:
			t.Fatalf("unexpected resource %v", urn)
		}
	}
}

const importSchema = `{
  "version": "0.0.1",
  "name": "pkgA",
  "resources": {
	"pkgA:m:typA": {
      "inputProperties": {
	    "foo": {
		  "type": "string"
		},
	    "frob": {
		  "type": "number"
		}
      },
	  "requiredInputs": [
		  "frob"
	  ],
      "properties": {
	    "foo": {
		  "type": "string"
		},
	    "frob": {
		  "type": "number"
		}
      }
    }
  }
}`

func diffImportResource(urn resource.URN, id resource.ID,
	oldInputs, oldOutputs, newInputs resource.PropertyMap, ignoreChanges []string,
) (plugin.DiffResult, error) {
	if oldOutputs["foo"].DeepEquals(newInputs["foo"]) && oldOutputs["frob"].DeepEquals(newInputs["frob"]) {
		return plugin.DiffResult{Changes: plugin.DiffNone}, nil
	}

	detailedDiff := make(map[string]plugin.PropertyDiff)
	if !oldOutputs["foo"].DeepEquals(newInputs["foo"]) {
		detailedDiff["foo"] = plugin.PropertyDiff{Kind: plugin.DiffUpdate}
	}
	if !oldOutputs["frob"].DeepEquals(newInputs["frob"]) {
		detailedDiff["frob"] = plugin.PropertyDiff{Kind: plugin.DiffUpdate}
	}

	return plugin.DiffResult{
		Changes:      plugin.DiffSome,
		DetailedDiff: detailedDiff,
	}, nil
}

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

	readInputs := resource.PropertyMap{
		"foo": resource.NewStringProperty("bar"),
	}
	readOutputs := resource.PropertyMap{
		"foo":  resource.NewStringProperty("bar"),
		"frob": resource.NewNumberProperty(1),
	}

	loaders := []*deploytest.ProviderLoader{
		deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
			return &deploytest.Provider{
				GetSchemaF: func(version int) ([]byte, error) {
					return []byte(importSchema), nil
				},
				DiffF: diffImportResource,
				CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64,
					preview bool,
				) (resource.ID, resource.PropertyMap, resource.Status, error) {
					return "created-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{
						ID:      "actual-id",
						Inputs:  readInputs,
						Outputs: readOutputs,
					}, resource.StatusOK, nil
				},
			}, nil
		}),
	}

	programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
		_, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{})
		assert.NoError(t, err)
		return nil
	})
	hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)

	p := &TestPlan{
		Options: TestUpdateOptions{HostF: hostF},
	}

	// Run the initial update.
	project := p.GetProject()
	snap, err := TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil)
	assert.NoError(t, err)

	// Run an import.
	snap, err = ImportOp([]deploy.Import{{
		Type: "pkgA:m:typA",
		Name: "resB",
		ID:   "imported-id",
	}}).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, nil)

	assert.NoError(t, err)
	assert.Len(t, snap.Resources, 4)

	// Import should save the ID, inputs and outputs
	assert.Equal(t, resource.ID("actual-id"), snap.Resources[3].ID)
	assert.Equal(t, readInputs, snap.Resources[3].Inputs)
	assert.Equal(t, readOutputs, snap.Resources[3].Outputs)

	// Import should set Created and Modified timestamps on state.
	for _, r := range snap.Resources {
		assert.NotNil(t, r.Created)
		assert.NotNil(t, r.Modified)
	}
}

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

	loaders := []*deploytest.ProviderLoader{
		deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
			return &deploytest.Provider{
				DiffF: diffImportResource,
				CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64,
					preview bool,
				) (resource.ID, resource.PropertyMap, resource.Status, error) {
					return "created-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: resource.PropertyMap{
							"foo":  resource.NewStringProperty("bar"),
							"frob": resource.NewNumberProperty(1),
						},
						Outputs: resource.PropertyMap{
							"foo":  resource.NewStringProperty("bar"),
							"frob": resource.NewNumberProperty(1),
						},
					}, resource.StatusOK, nil
				},
			}, nil
		}),
	}

	programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
		_, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{
			Inputs: resource.PropertyMap{
				"foo":  resource.NewStringProperty("foo"),
				"frob": resource.NewNumberProperty(1),
			},
			ImportID:      "import-id",
			IgnoreChanges: []string{"foo"},
		})
		assert.NoError(t, err)
		return nil
	})
	hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)

	p := &TestPlan{
		Options: TestUpdateOptions{HostF: hostF},
	}

	project := p.GetProject()
	snap, err := TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil)
	assert.NoError(t, err)

	assert.Len(t, snap.Resources, 2)
	assert.Equal(t, resource.NewStringProperty("bar"), snap.Resources[1].Outputs["foo"])
}

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

	loaders := []*deploytest.ProviderLoader{
		deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
			return &deploytest.Provider{
				GetSchemaF: func(version int) ([]byte, error) {
					return []byte(importSchema), nil
				},
				DiffF: diffImportResource,
				CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64,
					preview bool,
				) (resource.ID, resource.PropertyMap, resource.Status, error) {
					return "created-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: resource.PropertyMap{
							"foo":  resource.NewStringProperty("bar"),
							"frob": resource.NewNumberProperty(1),
						},
						Outputs: resource.PropertyMap{
							"foo":  resource.NewStringProperty("bar"),
							"frob": resource.NewNumberProperty(1),
						},
					}, resource.StatusOK, nil
				},
			}, nil
		}),
	}

	programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
		resp, err := monitor.RegisterResource("pulumi:pulumi:Stack", "test", false)
		require.NoError(t, err)

		_, err = monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{
			Inputs: resource.PropertyMap{
				"foo":  resource.NewStringProperty("bar"),
				"frob": resource.NewNumberProperty(1),
			},
			ImportID: "imported-id",
			Parent:   resp.URN,
		})
		require.NoError(t, err)

		err = monitor.RegisterResourceOutputs(resp.URN, resource.PropertyMap{})
		assert.NoError(t, err)
		return nil
	})
	hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)

	p := &TestPlan{
		Options: TestUpdateOptions{HostF: hostF},
	}

	// Run the initial update.
	project := p.GetProject()
	snap, err := TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil)
	assert.NoError(t, err)

	// Run an import with a different ID. This should fail.
	_, err = ImportOp([]deploy.Import{{
		Type: "pkgA:m:typA",
		Name: "resA",
		ID:   "imported-id-2",
	}}).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, nil)
	assert.Error(t, err)

	// Run an import with a matching ID. This should succeed and do nothing.
	snap, err = ImportOp([]deploy.Import{{
		Type: "pkgA:m:typA",
		Name: "resA",
		ID:   "imported-id",
	}}).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient,
		func(_ workspace.Project, _ deploy.Target, entries JournalEntries, _ []Event, _ error) error {
			for _, e := range entries {
				assert.Equal(t, deploy.OpSame, e.Step.Op())
			}
			return nil
		})

	assert.NoError(t, err)
	assert.Len(t, snap.Resources, 3)
}

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

	loaders := []*deploytest.ProviderLoader{
		deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
			return &deploytest.Provider{
				GetSchemaF: func(version int) ([]byte, error) {
					return []byte(importSchema), nil
				},
				DiffF: diffImportResource,
				CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64,
					preview bool,
				) (resource.ID, resource.PropertyMap, resource.Status, error) {
					return "created-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: resource.PropertyMap{
							"foo":  resource.NewStringProperty("bar"),
							"frob": resource.NewNumberProperty(1),
						},
						Outputs: resource.PropertyMap{
							"foo":  resource.NewStringProperty("bar"),
							"frob": resource.NewNumberProperty(1),
						},
					}, resource.StatusOK, nil
				},
			}, nil
		}),
	}
	programF := deploytest.NewLanguageRuntimeF(nil)
	hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)

	p := &TestPlan{
		Options: TestUpdateOptions{HostF: hostF},
	}

	// Run the initial import.
	project := p.GetProject()
	snap, err := ImportOp([]deploy.Import{{
		Type: "pkgA:m:typA",
		Name: "resB",
		ID:   "imported-id",
	}}).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil)

	assert.NoError(t, err)
	assert.Len(t, snap.Resources, 3)
}

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

	loaders := []*deploytest.ProviderLoader{
		deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
			return &deploytest.Provider{
				GetSchemaF: func(version int) ([]byte, error) {
					return []byte(importSchema), nil
				},
				DiffF: diffImportResource,
				CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64,
					preview bool,
				) (resource.ID, resource.PropertyMap, resource.Status, error) {
					return "created-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: resource.PropertyMap{
							"foo":  resource.NewStringProperty("bar"),
							"frob": resource.NewNumberProperty(1),
						},
						Outputs: resource.PropertyMap{
							"foo":  resource.NewStringProperty("bar"),
							"frob": resource.NewNumberProperty(1),
						},
					}, resource.StatusOK, nil
				},
			}, nil
		}),
	}

	programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
		_, err := monitor.RegisterResource("pulumi:providers:pkgA", "provA", true)
		assert.NoError(t, err)
		return nil
	})
	hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)

	p := &TestPlan{
		Options: TestUpdateOptions{HostF: hostF},
	}

	// Run the initial update.
	project := p.GetProject()
	snap, err := TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil)
	assert.NoError(t, err)

	snap, err = ImportOp([]deploy.Import{{
		Type:     "pkgA:m:typA",
		Name:     "resB",
		ID:       "imported-id",
		Provider: p.NewProviderURN("pkgA", "provA", ""),
	}}).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, nil)

	assert.NoError(t, err)
	assert.Len(t, snap.Resources, 3)
}

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

	loaders := []*deploytest.ProviderLoader{
		deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
			return &deploytest.Provider{
				GetSchemaF: func(version int) ([]byte, error) {
					return []byte(importSchema), nil
				},
				DiffF: diffImportResource,
				CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64,
					preview bool,
				) (resource.ID, resource.PropertyMap, resource.Status, error) {
					return "created-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: resource.PropertyMap{
							"foo":  resource.NewStringProperty("bar"),
							"frob": resource.NewNumberProperty(1),
							"baz":  resource.NewNumberProperty(2),
						},
						Outputs: resource.PropertyMap{
							"foo":  resource.NewStringProperty("bar"),
							"frob": resource.NewNumberProperty(1),
							"baz":  resource.NewNumberProperty(2),
						},
					}, resource.StatusOK, nil
				},
				CheckF: func(
					urn resource.URN, olds, news resource.PropertyMap,
					randomSeed []byte,
				) (resource.PropertyMap, []plugin.CheckFailure, error) {
					// Error unless "foo" and "frob" are in news

					if _, has := news["foo"]; !has {
						return nil, nil, errors.New("Need foo")
					}

					if _, has := news["frob"]; !has {
						return nil, nil, errors.New("Need frob")
					}

					return news, nil, nil
				},
			}, nil
		}),
	}

	programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
		_, err := monitor.RegisterResource("pulumi:providers:pkgA", "provA", true)
		assert.NoError(t, err)
		return nil
	})
	hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)

	p := &TestPlan{
		Options: TestUpdateOptions{HostF: hostF},
	}

	// Run the initial update.
	project := p.GetProject()
	snap, err := TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil)
	assert.NoError(t, err)

	// Import specifying to use just foo and frob
	snap, err = ImportOp([]deploy.Import{{
		Type:       "pkgA:m:typA",
		Name:       "resB",
		ID:         "imported-id",
		Provider:   p.NewProviderURN("pkgA", "provA", ""),
		Properties: []string{"foo", "frob"},
	}}).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, nil)

	assert.NoError(t, err)
	assert.Len(t, snap.Resources, 3)

	// We should still have the baz output but will be missing its input
	assert.Equal(t, resource.NewNumberProperty(2), snap.Resources[2].Outputs["baz"])
	assert.NotContains(t, snap.Resources[2].Inputs, "baz")
}

// Test that we can import one resource, then import another resource with the first one as a parent.
func TestImportIntoParent(t *testing.T) {
	t.Parallel()

	loaders := []*deploytest.ProviderLoader{
		deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
			return &deploytest.Provider{
				GetSchemaF: func(version int) ([]byte, error) {
					return []byte(importSchema), nil
				},
				DiffF: diffImportResource,
				CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64,
					preview bool,
				) (resource.ID, resource.PropertyMap, resource.Status, error) {
					return "", news, resource.StatusUnknown, errors.New("not implemented")
				},
				ReadF: func(urn resource.URN, id resource.ID,
					inputs, state resource.PropertyMap,
				) (plugin.ReadResult, resource.Status, error) {
					return plugin.ReadResult{
						Inputs: resource.PropertyMap{
							"foo":  resource.NewStringProperty("bar"),
							"frob": resource.NewNumberProperty(1),
						},
						Outputs: resource.PropertyMap{
							"foo":  resource.NewStringProperty("bar"),
							"frob": resource.NewNumberProperty(1),
						},
					}, resource.StatusOK, nil
				},
			}, nil
		}),
	}
	programF := deploytest.NewLanguageRuntimeF(nil)
	hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)

	p := &TestPlan{
		Options: TestUpdateOptions{HostF: hostF},
	}

	// Run the initial import.
	project := p.GetProject()
	snap, err := ImportOp([]deploy.Import{
		{
			Type:   "pkgA:m:typA",
			Name:   "resB",
			ID:     "imported-idB",
			Parent: p.NewURN("pkgA:m:typA", "resA", ""),
		},
		{
			Type: "pkgA:m:typA",
			Name: "resA",
			ID:   "imported-idA",
		},
	}).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil)

	assert.NoError(t, err)
	assert.Len(t, snap.Resources, 4)
}

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

	loaders := []*deploytest.ProviderLoader{
		deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
			return &deploytest.Provider{
				GetSchemaF: func(version int) ([]byte, error) {
					return []byte(importSchema), nil
				},
				DiffF: diffImportResource,
				CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64,
					preview bool,
				) (resource.ID, resource.PropertyMap, resource.Status, error) {
					return "", nil, resource.StatusUnknown, errors.New("not implemented")
				},
				ReadF: func(urn resource.URN, id resource.ID,
					inputs, state resource.PropertyMap,
				) (plugin.ReadResult, resource.Status, error) {
					return plugin.ReadResult{
						Inputs: resource.PropertyMap{
							"foo":  resource.NewStringProperty("bar"),
							"frob": resource.NewNumberProperty(1),
						},
						Outputs: resource.PropertyMap{
							"foo":  resource.NewStringProperty("bar"),
							"frob": resource.NewNumberProperty(1),
						},
					}, resource.StatusOK, nil
				},
			}, nil
		}),
	}
	programF := deploytest.NewLanguageRuntimeF(nil)
	hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)

	p := &TestPlan{
		Options: TestUpdateOptions{HostF: hostF},
	}

	// Run the initial import.
	project := p.GetProject()
	snap, err := ImportOp([]deploy.Import{
		{
			Type:      "my-component",
			Name:      "comp",
			Component: true,
		},
		{
			Type:   "pkgA:m:typA",
			Name:   "resB",
			ID:     "imported-id",
			Parent: p.NewURN("my-component", "comp", ""),
		},
	}).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil)

	assert.NoError(t, err)
	assert.Len(t, snap.Resources, 4)

	// Ensure that the resource 2 is the component.
	comp := snap.Resources[2]
	assert.Equal(t, resource.URN("urn:pulumi:test::test::my-component::comp"), comp.URN)
	// Ensure it's marked as a component.
	assert.False(t, comp.Custom, "expected component resource to not be marked as custom")

	// Ensure resource 3 is the custom resource
	custom := snap.Resources[3]
	assert.Equal(t, resource.URN("urn:pulumi:test::test::my-component$pkgA:m:typA::resB"), custom.URN)
	// Ensure it's marked as custom.
	assert.True(t, custom.Custom, "expected custom resource to be marked as custom")
}

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

	loaders := []*deploytest.ProviderLoader{
		deploytest.NewProviderLoader("mlc", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
			return &deploytest.Provider{}, nil
		}),
		deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
			return &deploytest.Provider{
				GetSchemaF: func(version int) ([]byte, error) {
					return []byte(importSchema), nil
				},
				DiffF: diffImportResource,
				CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64,
					preview bool,
				) (resource.ID, resource.PropertyMap, resource.Status, error) {
					return "", nil, resource.StatusUnknown, errors.New("not implemented")
				},
				ReadF: func(urn resource.URN, id resource.ID,
					inputs, state resource.PropertyMap,
				) (plugin.ReadResult, resource.Status, error) {
					return plugin.ReadResult{
						Inputs: resource.PropertyMap{
							"foo":  resource.NewStringProperty("bar"),
							"frob": resource.NewNumberProperty(1),
						},
						Outputs: resource.PropertyMap{
							"foo":  resource.NewStringProperty("bar"),
							"frob": resource.NewNumberProperty(1),
						},
					}, resource.StatusOK, nil
				},
			}, nil
		}),
	}
	programF := deploytest.NewLanguageRuntimeF(nil)
	hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)

	p := &TestPlan{
		Options: TestUpdateOptions{HostF: hostF},
	}

	// Run the initial import.
	project := p.GetProject()
	snap, err := ImportOp([]deploy.Import{
		{
			Type:      "mlc:index:Component",
			Name:      "comp",
			Component: true,
			Remote:    true,
		},
		{
			Type:   "pkgA:m:typA",
			Name:   "resB",
			ID:     "imported-id",
			Parent: p.NewURN("mlc:index:Component", "comp", ""),
		},
	}).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil)

	assert.NoError(t, err)
	assert.Len(t, snap.Resources, 5)

	// Ensure that the resource 3 is the component.
	comp := snap.Resources[3]
	assert.Equal(t, resource.URN("urn:pulumi:test::test::mlc:index:Component::comp"), comp.URN)
	// Ensure it's marked as a component.
	assert.False(t, comp.Custom, "expected component resource to not be marked as custom")

	// Ensure resource 4 is the custom resource
	custom := snap.Resources[4]
	assert.Equal(t, resource.URN("urn:pulumi:test::test::mlc:index:Component$pkgA:m:typA::resB"), custom.URN)
	// Ensure it's marked as custom.
	assert.True(t, custom.Custom, "expected custom resource to be marked as custom")
}