package lifecycletest

import (
	"fmt"
	"testing"

	"github.com/blang/semver"
	combinations "github.com/mxschmitt/golang-combinations"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/pulumi/pulumi/pkg/v3/display"
	. "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/pkg/v3/resource/deploy/providers"
	"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/tokens"
	"github.com/pulumi/pulumi/sdk/v3/go/common/workspace"
)

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

	// Try refreshing a stack with combinations of the above resources as target to destroy.
	subsets := combinations.All(complexTestDependencyGraphNames)

	//nolint:paralleltest // false positive because range var isn't used directly in t.Run(name) arg
	for _, subset := range subsets {
		subset := subset
		// limit to up to 3 resources to destroy.  This keeps the test running time under
		// control as it only generates a few hundred combinations instead of several thousand.
		if len(subset) <= 3 {
			t.Run(fmt.Sprintf("%v", subset), func(t *testing.T) {
				t.Parallel()

				destroySpecificTargets(t, subset, true, /*targetDependents*/
					func(urns []resource.URN, deleted map[resource.URN]bool) {})
			})
		}
	}

	t.Run("destroy root", func(t *testing.T) {
		t.Parallel()

		destroySpecificTargets(
			t, []string{"A"}, true, /*targetDependents*/
			func(urns []resource.URN, deleted map[resource.URN]bool) {
				// when deleting 'A' we expect A, B, C, D, E, F, G, H, I, J, K, and L to be deleted
				names := complexTestDependencyGraphNames
				assert.Equal(t, map[resource.URN]bool{
					pickURN(t, urns, names, "A"): true,
					pickURN(t, urns, names, "B"): true,
					pickURN(t, urns, names, "C"): true,
					pickURN(t, urns, names, "D"): true,
					pickURN(t, urns, names, "E"): true,
					pickURN(t, urns, names, "F"): true,
					pickURN(t, urns, names, "G"): true,
					pickURN(t, urns, names, "H"): true,
					pickURN(t, urns, names, "I"): true,
					pickURN(t, urns, names, "J"): true,
					pickURN(t, urns, names, "K"): true,
					pickURN(t, urns, names, "L"): true,
				}, deleted)
			})
	})

	destroySpecificTargets(
		t, []string{"A"}, false, /*targetDependents*/
		func(urns []resource.URN, deleted map[resource.URN]bool) {})
}

func destroySpecificTargets(
	t *testing.T, targets []string, targetDependents bool,
	validate func(urns []resource.URN, deleted map[resource.URN]bool),
) {
	//             A
	//    _________|_________
	//    B        C        D
	//          ___|___  ___|___
	//          E  F  G  H  I  J
	//             |__|
	//             K  L

	p := &TestPlan{}

	urns, old, programF := generateComplexTestDependencyGraph(t, p)

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

	p.Options.HostF = deploytest.NewPluginHostF(nil, nil, programF, loaders...)
	p.Options.TargetDependents = targetDependents

	destroyTargets := []resource.URN{}
	for _, target := range targets {
		destroyTargets = append(destroyTargets, pickURN(t, urns, complexTestDependencyGraphNames, target))
	}

	p.Options.Targets = deploy.NewUrnTargetsFromUrns(destroyTargets)
	t.Logf("Destroying targets: %v", destroyTargets)

	// If we're not forcing the targets to be destroyed, then expect to get a failure here as
	// we'll have downstream resources to delete that weren't specified explicitly.
	p.Steps = []TestStep{{
		Op:            Destroy,
		ExpectFailure: !targetDependents,
		Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
			evts []Event, err error,
		) error {
			assert.NoError(t, err)
			assert.True(t, len(entries) > 0)

			deleted := make(map[resource.URN]bool)
			for _, entry := range entries {
				assert.Equal(t, deploy.OpDelete, entry.Step.Op())
				deleted[entry.Step.URN()] = true
			}

			for _, target := range p.Options.Targets.Literals() {
				assert.Contains(t, deleted, target)
			}

			validate(urns, deleted)
			return err
		},
	}}

	p.Run(t, old)
}

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

	// Try refreshing a stack with combinations of the above resources as target to destroy.
	subsets := combinations.All(complexTestDependencyGraphNames)

	//nolint:paralleltest // false positive because range var isn't used directly in t.Run(name) arg
	for _, subset := range subsets {
		subset := subset
		// limit to up to 3 resources to destroy.  This keeps the test running time under
		// control as it only generates a few hundred combinations instead of several thousand.
		if len(subset) <= 3 {
			t.Run(fmt.Sprintf("update %v", subset), func(t *testing.T) {
				t.Parallel()

				updateSpecificTargets(t, subset, nil, false /*targetDependents*/, -1)
			})
		}
	}

	updateSpecificTargets(t, []string{"A"}, nil, false /*targetDependents*/, -1)

	// Also update a target that doesn't exist to make sure we don't crash or otherwise go off the rails.
	updateInvalidTarget(t)

	// We want to check that targetDependents is respected
	updateSpecificTargets(t, []string{"C"}, nil, true /*targetDependents*/, -1)

	updateSpecificTargets(t, nil, []string{"**C**"}, false, 3)
	updateSpecificTargets(t, nil, []string{"**providers:pkgA**"}, false, 3)
}

func updateSpecificTargets(t *testing.T, targets, globTargets []string, targetDependents bool, expectedUpdates int) {
	//             A
	//    _________|_________
	//    B        C        D
	//          ___|___  ___|___
	//          E  F  G  H  I  J
	//             |__|
	//             K  L

	p := &TestPlan{}

	urns, old, programF := generateComplexTestDependencyGraph(t, p)

	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) {
					// all resources will change.
					return plugin.DiffResult{
						Changes: plugin.DiffSome,
					}, nil
				},

				UpdateF: func(urn resource.URN, id resource.ID,
					oldInputs, oldOutputs, newInputs resource.PropertyMap,
					timeout float64, ignoreChanges []string, preview bool,
				) (resource.PropertyMap, resource.Status, error) {
					outputs := oldOutputs.Copy()

					outputs["output_prop"] = resource.NewPropertyValue(42)
					return outputs, resource.StatusOK, nil
				},
			}, nil
		}),
	}

	p.Options.HostF = deploytest.NewPluginHostF(nil, nil, programF, loaders...)
	p.Options.TargetDependents = targetDependents

	updateTargets := globTargets
	for _, target := range targets {
		updateTargets = append(updateTargets,
			string(pickURN(t, urns, complexTestDependencyGraphNames, target)))
	}

	p.Options.Targets = deploy.NewUrnTargets(updateTargets)
	t.Logf("Updating targets: %v", updateTargets)

	p.Steps = []TestStep{{
		Op:            Update,
		ExpectFailure: false,
		Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
			evts []Event, err error,
		) error {
			assert.NoError(t, err)
			assert.True(t, len(entries) > 0)

			updated := make(map[resource.URN]bool)
			sames := make(map[resource.URN]bool)
			for _, entry := range entries {
				if entry.Step.Op() == deploy.OpUpdate {
					updated[entry.Step.URN()] = true
				} else if entry.Step.Op() == deploy.OpSame {
					sames[entry.Step.URN()] = true
				} else {
					assert.FailNowf(t, "", "Got a step that wasn't a same/update: %v", entry.Step.Op())
				}
			}

			for _, target := range p.Options.Targets.Literals() {
				assert.Contains(t, updated, target)
			}

			if !targetDependents {
				// We should only perform updates on the entries we have targeted.
				for _, target := range p.Options.Targets.Literals() {
					assert.Contains(t, targets, target.Name())
				}
			} else {
				// We expect to find at least one other resource updates.

				// NOTE: The test is limited to only passing a subset valid behavior. By specifying
				// a URN with no dependents, no other urns will be updated and the test will fail
				// (incorrectly).
				found := false
				updateList := []string{}
				for target := range updated {
					updateList = append(updateList, target.Name())
					if !contains(targets, target.Name()) {
						found = true
					}
				}
				assert.True(t, found, "Updates: %v", updateList)
			}

			for _, target := range p.Options.Targets.Literals() {
				assert.NotContains(t, sames, target)
			}
			if expectedUpdates > -1 {
				assert.Equal(t, expectedUpdates, len(updated), "Updates = %#v", updated)
			}
			return err
		},
	}}
	p.Run(t, old)
}

func contains(list []string, entry string) bool {
	for _, e := range list {
		if e == entry {
			return true
		}
	}
	return false
}

func updateInvalidTarget(t *testing.T) {
	p := &TestPlan{}

	_, old, programF := generateComplexTestDependencyGraph(t, p)

	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) {
					// all resources will change.
					return plugin.DiffResult{
						Changes: plugin.DiffSome,
					}, nil
				},

				UpdateF: func(urn resource.URN, id resource.ID,
					oldInputs, oldOutputs, newInputs resource.PropertyMap,
					timeout float64, ignoreChanges []string, preview bool,
				) (resource.PropertyMap, resource.Status, error) {
					outputs := oldOutputs.Copy()

					outputs["output_prop"] = resource.NewPropertyValue(42)
					return outputs, resource.StatusOK, nil
				},
			}, nil
		}),
	}

	p.Options.HostF = deploytest.NewPluginHostF(nil, nil, programF, loaders...)

	p.Options.Targets = deploy.NewUrnTargetsFromUrns([]resource.URN{"foo"})
	t.Logf("Updating invalid targets: %v", p.Options.Targets)

	p.Steps = []TestStep{{
		Op:            Update,
		ExpectFailure: true,
	}}

	p.Run(t, old)
}

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

	loaders := []*deploytest.ProviderLoader{
		deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
			return &deploytest.Provider{}, nil
		}),
	}

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

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

	p.Steps = []TestStep{{Op: Update}}
	snap1 := p.Run(t, nil)

	// Now, create a resource resB.  This shouldn't be a problem since resB isn't referenced by anything.
	program2F := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
		_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true)
		assert.NoError(t, err)

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

		return nil
	})
	host2F := deploytest.NewPluginHostF(nil, nil, program2F, loaders...)

	resA := p.NewURN("pkgA:m:typA", "resA", "")
	resB := p.NewURN("pkgA:m:typA", "resB", "")
	p.Options.HostF = host2F
	p.Options.Targets = deploy.NewUrnTargetsFromUrns([]resource.URN{resA, resB})
	p.Steps = []TestStep{{
		Op:            Update,
		ExpectFailure: false,
		Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
			evts []Event, err error,
		) error {
			assert.NoError(t, err)
			assert.True(t, len(entries) > 0)

			for _, entry := range entries {
				if entry.Step.URN() == resA {
					assert.Equal(t, deploy.OpSame, entry.Step.Op())
				} else if entry.Step.URN() == resB {
					assert.Equal(t, deploy.OpCreate, entry.Step.Op())
				}
			}

			return err
		},
	}}
	p.Run(t, snap1)
}

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

	loaders := []*deploytest.ProviderLoader{
		deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
			return &deploytest.Provider{}, nil
		}),
	}

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

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

	p.Steps = []TestStep{{Op: Update}}
	snap1 := p.Run(t, nil)

	// Now, create a resource resB.  This shouldn't be a problem since resB isn't referenced by anything.
	program2F := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
		_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true)
		assert.NoError(t, err)

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

		return nil
	})
	host2F := deploytest.NewPluginHostF(nil, nil, program2F, loaders...)

	resA := p.NewURN("pkgA:m:typA", "resA", "")

	p.Options.HostF = host2F
	p.Options.Targets = deploy.NewUrnTargetsFromUrns([]resource.URN{resA})
	p.Steps = []TestStep{{
		Op:            Update,
		ExpectFailure: false,
		Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
			evts []Event, err error,
		) error {
			assert.NoError(t, err)
			assert.True(t, len(entries) > 0)

			for _, entry := range entries {
				// everything should be a same op here.
				assert.Equal(t, deploy.OpSame, entry.Step.Op())
			}

			return err
		},
	}}
	p.Run(t, snap1)
}

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

	loaders := []*deploytest.ProviderLoader{
		deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
			return &deploytest.Provider{}, nil
		}),
	}

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

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

	p.Steps = []TestStep{{Op: Update}}
	p.Run(t, nil)

	resA := p.NewURN("pkgA:m:typA", "resA", "")
	resB := p.NewURN("pkgA:m:typA", "resB", "")

	// Now, create a resource resB.  But reference it from A. This will cause a dependency we can't
	// satisfy.
	program2F := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
		_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resB", true)
		assert.NoError(t, err)

		_, _, _, err = monitor.RegisterResource("pkgA:m:typA", "resA", true,
			deploytest.ResourceOptions{
				Dependencies: []resource.URN{resB},
			})
		assert.NoError(t, err)

		return nil
	})
	host2F := deploytest.NewPluginHostF(nil, nil, program2F, loaders...)

	p.Options.HostF = host2F
	p.Options.Targets = deploy.NewUrnTargetsFromUrns([]resource.URN{resA})
	p.Steps = []TestStep{{
		Op:            Update,
		ExpectFailure: true,
	}}
	p.Run(t, nil)
}

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

	loaders := []*deploytest.ProviderLoader{
		deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
			return &deploytest.Provider{}, nil
		}),
	}

	// Create a resource A with --target but don't target its explicit provider.

	programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
		provURN, provID, _, err := monitor.RegisterResource(providers.MakeProviderType("pkgA"), "provA", true)
		assert.NoError(t, err)

		if provID == "" {
			provID = providers.UnknownID
		}

		provRef, err := providers.NewReference(provURN, provID)
		assert.NoError(t, err)

		_, _, _, err = monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{
			Provider: provRef.String(),
		})
		assert.NoError(t, err)
		return nil
	})
	host1F := deploytest.NewPluginHostF(nil, nil, programF, loaders...)

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

	resA := p.NewURN("pkgA:m:typA", "resA", "")

	p.Options.Targets = deploy.NewUrnTargetsFromUrns([]resource.URN{resA})
	p.Steps = []TestStep{{
		Op: Update,
	}}
	p.Run(t, nil)
}

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

	loaders := []*deploytest.ProviderLoader{
		deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
			return &deploytest.Provider{}, nil
		}),
	}

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

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

	p.Steps = []TestStep{{Op: Update}}
	snap1 := p.Run(t, nil)

	resA := p.NewURN("pkgA:m:typA", "resA", "")
	resB := p.NewURN("pkgA:m:typA", "resB", "")

	// Now, create a resource resB.  But reference it from A. This will cause a dependency we can't
	// satisfy.
	program2F := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
		_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resB", true)
		assert.NoError(t, err)

		_, _, _, err = monitor.RegisterResource("pkgA:m:typA", "resC", true,
			deploytest.ResourceOptions{
				Dependencies: []resource.URN{resB},
			})
		assert.NoError(t, err)

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

		return nil
	})
	host2F := deploytest.NewPluginHostF(nil, nil, program2F, loaders...)

	p.Options.HostF = host2F
	p.Options.Targets = deploy.NewUrnTargetsFromUrns([]resource.URN{resA})
	p.Steps = []TestStep{{
		Op:            Update,
		ExpectFailure: false,
		Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
			evts []Event, err error,
		) error {
			assert.NoError(t, err)
			assert.True(t, len(entries) > 0)

			for _, entry := range entries {
				assert.Equal(t, deploy.OpSame, entry.Step.Op())
			}

			return err
		},
	}}
	p.Run(t, snap1)
}

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

	//             A
	//    _________|_________
	//    B        C        D
	//          ___|___  ___|___
	//          E  F  G  H  I  J
	//             |__|
	//             K  L

	p := &TestPlan{}

	urns, old, programF := generateComplexTestDependencyGraph(t, p)

	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) {
					// No resources will change.
					return plugin.DiffResult{Changes: plugin.DiffNone}, 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
				},
			}, nil
		}),
	}

	p.Options.HostF = deploytest.NewPluginHostF(nil, nil, programF, loaders...)

	getURN := func(name string) resource.URN {
		return pickURN(t, urns, complexTestDependencyGraphNames, name)
	}

	p.Options.ReplaceTargets = deploy.NewUrnTargetsFromUrns([]resource.URN{
		getURN("F"),
		getURN("B"),
		getURN("G"),
	})

	p.Steps = []TestStep{{
		Op:            Update,
		ExpectFailure: false,
		Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
			evts []Event, err error,
		) error {
			assert.NoError(t, err)
			assert.True(t, len(entries) > 0)

			replaced := make(map[resource.URN]bool)
			sames := make(map[resource.URN]bool)
			for _, entry := range entries {
				if entry.Step.Op() == deploy.OpReplace {
					replaced[entry.Step.URN()] = true
				} else if entry.Step.Op() == deploy.OpSame {
					sames[entry.Step.URN()] = true
				}
			}

			for _, target := range p.Options.ReplaceTargets.Literals() {
				assert.Contains(t, replaced, target)
			}

			for _, target := range p.Options.ReplaceTargets.Literals() {
				assert.NotContains(t, sames, target)
			}

			return err
		},
	}}

	p.Run(t, old)
}

var componentBasedTestDependencyGraphNames = []string{
	"A", "B", "C", "D", "E", "F", "G", "H",
	"I", "J", "K", "L", "M", "N",
}

func generateParentedTestDependencyGraph(t *testing.T, p *TestPlan) (
	// Parent-child graph
	//      A               B
	//    __|__         ____|____
	//    D   I         E       F
	//  __|__         __|__   __|__
	//  G   H         J   K   L   M
	//
	// A has children D, I
	// D has children G, H
	// B has children E, F
	// E has children J, K
	// F has children L, M
	//
	// Dependency graph
	//  G        H
	//  |      __|__
	//  I      K   N
	//
	// I depends on G
	// K depends on H
	// N depends on H

	[]resource.URN, *deploy.Snapshot, deploytest.LanguageRuntimeFactory,
) {
	resTypeComponent := tokens.Type("pkgA:index:Component")
	resTypeResource := tokens.Type("pkgA:index:Resource")

	names := componentBasedTestDependencyGraphNames

	urnA := p.NewURN(resTypeComponent, names[0], "")
	urnB := p.NewURN(resTypeComponent, names[1], "")
	urnC := p.NewURN(resTypeResource, names[2], "")
	urnD := p.NewURN(resTypeComponent, names[3], urnA)
	urnE := p.NewURN(resTypeComponent, names[4], urnB)
	urnF := p.NewURN(resTypeComponent, names[5], urnB)
	urnG := p.NewURN(resTypeResource, names[6], urnD)
	urnH := p.NewURN(resTypeResource, names[7], urnD)
	urnI := p.NewURN(resTypeResource, names[8], urnA)
	urnJ := p.NewURN(resTypeResource, names[9], urnE)
	urnK := p.NewURN(resTypeResource, names[10], urnE)
	urnL := p.NewURN(resTypeResource, names[11], urnF)
	urnM := p.NewURN(resTypeResource, names[12], urnF)
	urnN := p.NewURN(resTypeResource, names[13], "")

	urns := []resource.URN{urnA, urnB, urnC, urnD, urnE, urnF, urnG, urnH, urnI, urnJ, urnK, urnL, urnM, urnN}

	newResource := func(urn, parent resource.URN, id resource.ID,
		dependencies []resource.URN, propertyDeps propertyDependencies,
	) *resource.State {
		return newResource(urn, parent, id, "", dependencies, propertyDeps,
			nil, urn.Type() != resTypeComponent)
	}

	old := &deploy.Snapshot{
		Resources: []*resource.State{
			newResource(urnA, "", "0", nil, nil),
			newResource(urnB, "", "1", nil, nil),
			newResource(urnC, "", "2", nil, nil),
			newResource(urnD, urnA, "3", nil, nil),
			newResource(urnE, urnB, "4", nil, nil),
			newResource(urnF, urnB, "5", nil, nil),
			newResource(urnG, urnD, "6", nil, nil),
			newResource(urnH, urnD, "7", nil, nil),
			newResource(urnI, urnA, "8", []resource.URN{urnG},
				propertyDependencies{"A": []resource.URN{urnG}}),
			newResource(urnJ, urnE, "9", nil, nil),
			newResource(urnK, urnE, "10", []resource.URN{urnH},
				propertyDependencies{"A": []resource.URN{urnH}}),
			newResource(urnL, urnF, "11", nil, nil),
			newResource(urnM, urnF, "12", nil, nil),
			newResource(urnN, "", "13", []resource.URN{urnH},
				propertyDependencies{"A": []resource.URN{urnH}}),
		},
	}

	programF := deploytest.NewLanguageRuntimeF(
		func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
			register := func(urn, parent resource.URN) resource.ID {
				_, id, _, err := monitor.RegisterResource(
					urn.Type(),
					urn.Name(),
					urn.Type() != resTypeComponent,
					deploytest.ResourceOptions{
						Inputs: nil,
						Parent: parent,
					})
				assert.NoError(t, err)
				return id
			}

			register(urnA, "")
			register(urnB, "")
			register(urnC, "")
			register(urnD, urnA)
			register(urnE, urnB)
			register(urnF, urnB)
			register(urnG, urnD)
			register(urnH, urnD)
			register(urnI, urnA)
			register(urnJ, urnE)
			register(urnK, urnE)
			register(urnL, urnF)
			register(urnM, urnF)
			register(urnN, "")

			return nil
		})

	return urns, old, programF
}

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

	// when deleting 'A' with targetDependents specified we expect A, D, G, H, I, K and N to be deleted.
	destroySpecificTargetsWithChildren(
		t, []string{"A"}, true, /*targetDependents*/
		func(urns []resource.URN, deleted map[resource.URN]bool) {
			names := componentBasedTestDependencyGraphNames
			assert.Equal(t, map[resource.URN]bool{
				pickURN(t, urns, names, "A"): true,
				pickURN(t, urns, names, "D"): true,
				pickURN(t, urns, names, "G"): true,
				pickURN(t, urns, names, "H"): true,
				pickURN(t, urns, names, "I"): true,
				pickURN(t, urns, names, "K"): true,
				pickURN(t, urns, names, "N"): true,
			}, deleted)
		})

	// when deleting 'A' with targetDependents not specified, we expect an error.
	destroySpecificTargetsWithChildren(
		t, []string{"A"}, false, /*targetDependents*/
		func(urns []resource.URN, deleted map[resource.URN]bool) {})

	// when deleting 'B' we expect B, E, F, J, K, L, M to be deleted.
	destroySpecificTargetsWithChildren(
		t, []string{"B"}, false, /*targetDependents*/
		func(urns []resource.URN, deleted map[resource.URN]bool) {
			names := componentBasedTestDependencyGraphNames
			assert.Equal(t, map[resource.URN]bool{
				pickURN(t, urns, names, "B"): true,
				pickURN(t, urns, names, "E"): true,
				pickURN(t, urns, names, "F"): true,
				pickURN(t, urns, names, "J"): true,
				pickURN(t, urns, names, "K"): true,
				pickURN(t, urns, names, "L"): true,
				pickURN(t, urns, names, "M"): true,
			}, deleted)
		})
}

func destroySpecificTargetsWithChildren(
	t *testing.T, targets []string, targetDependents bool,
	validate func(urns []resource.URN, deleted map[resource.URN]bool),
) {
	p := &TestPlan{}

	urns, old, programF := generateParentedTestDependencyGraph(t, p)

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

	p.Options.HostF = deploytest.NewPluginHostF(nil, nil, programF, loaders...)
	p.Options.TargetDependents = targetDependents

	destroyTargets := []resource.URN{}
	for _, target := range targets {
		destroyTargets = append(destroyTargets, pickURN(t, urns, componentBasedTestDependencyGraphNames, target))
	}

	p.Options.Targets = deploy.NewUrnTargetsFromUrns(destroyTargets)
	t.Logf("Destroying targets: %v", destroyTargets)

	// If we're not forcing the targets to be destroyed, then expect to get a failure here as
	// we'll have downstream resources to delete that weren't specified explicitly.
	p.Steps = []TestStep{{
		Op:            Destroy,
		ExpectFailure: !targetDependents,
		Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
			evts []Event, err error,
		) error {
			assert.NoError(t, err)
			assert.True(t, len(entries) > 0)

			deleted := make(map[resource.URN]bool)
			for _, entry := range entries {
				assert.Equal(t, deploy.OpDelete, entry.Step.Op())
				deleted[entry.Step.URN()] = true
			}

			for _, target := range p.Options.Targets.Literals() {
				assert.Contains(t, deleted, target)
			}

			validate(urns, deleted)
			return err
		},
	}}

	p.Run(t, old)
}

func newResource(urn, parent resource.URN, id resource.ID, provider string, dependencies []resource.URN,
	propertyDeps propertyDependencies, outputs resource.PropertyMap, custom bool,
) *resource.State {
	inputs := resource.PropertyMap{}
	for k := range propertyDeps {
		inputs[k] = resource.NewStringProperty("foo")
	}

	return &resource.State{
		Type:                 urn.Type(),
		URN:                  urn,
		Custom:               custom,
		Delete:               false,
		ID:                   id,
		Inputs:               inputs,
		Outputs:              outputs,
		Dependencies:         dependencies,
		PropertyDependencies: propertyDeps,
		Provider:             provider,
		Parent:               parent,
	}
}

// TestTargetedCreateDefaultProvider checks that an update that targets a resource still creates the default
// provider if not targeted.
func TestTargetedCreateDefaultProvider(t *testing.T) {
	t.Parallel()

	loaders := []*deploytest.ProviderLoader{
		deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
			return &deploytest.Provider{}, 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{}

	project := p.GetProject()

	// Check that update succeeds despite the default provider not being targeted.
	options := TestUpdateOptions{
		HostF: hostF,
		UpdateOptions: UpdateOptions{
			Targets: deploy.NewUrnTargets([]string{
				"urn:pulumi:test::test::pkgA:m:typA::resA",
			}),
		},
	}
	snap, err := TestOp(Update).Run(project, p.GetTarget(t, nil), options, false, p.BackendClient, nil)
	assert.NoError(t, err)

	// Check that the default provider was created.
	var foundDefaultProvider bool
	for _, res := range snap.Resources {
		if res.URN == "urn:pulumi:test::test::pulumi:providers:pkgA::default" {
			foundDefaultProvider = true
		}
	}
	assert.True(t, foundDefaultProvider)
}

// Returns the resource with the matching URN, or nil.
func findResourceByURN(rs []*resource.State, urn resource.URN) *resource.State {
	for _, r := range rs {
		if r.URN == urn {
			return r
		}
	}
	return nil
}

// TestEnsureUntargetedSame checks that an untargeted resource retains the prior state after an update when the provider
// alters the inputs. This is a regression test for pulumi/pulumi#12964.
func TestEnsureUntargetedSame(t *testing.T) {
	t.Parallel()

	// Provider that alters inputs during Check.
	loaders := []*deploytest.ProviderLoader{
		deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
			return &deploytest.Provider{
				CheckF: func(urn resource.URN,
					olds, news resource.PropertyMap, _ []byte,
				) (resource.PropertyMap, []plugin.CheckFailure, error) {
					// Pulumi GCP provider alters inputs during Check.
					news["__defaults"] = resource.NewStringProperty("exists")
					return news, nil, nil
				},
			}, nil
		}),
	}

	// Program that creates 2 resources.
	programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
		_, _, _, err := monitor.RegisterResource("pulumi:pulumi:Stack", "test-test", false)
		assert.NoError(t, err)

		_, _, _, err = monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{
			Inputs: resource.PropertyMap{
				"foo": resource.NewStringProperty("foo"),
			},
		})
		assert.NoError(t, err)

		_, _, _, err = monitor.RegisterResource("pkgA:m:typA", "resB", true, deploytest.ResourceOptions{
			Inputs: resource.PropertyMap{
				"foo": resource.NewStringProperty("bar"),
			},
		})
		assert.NoError(t, err)

		return nil
	})

	hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
	p := &TestPlan{}

	project := p.GetProject()

	// Set up stack with initial two resources.
	options := TestUpdateOptions{HostF: hostF}
	origSnap, err := TestOp(Update).Run(project, p.GetTarget(t, nil), options, false, p.BackendClient, nil)
	require.NoError(t, err)

	// Target only `resA` and run a targeted update.
	options = TestUpdateOptions{
		HostF: hostF,
		UpdateOptions: UpdateOptions{
			Targets: deploy.NewUrnTargets([]string{
				"urn:pulumi:test::test::pkgA:m:typA::resA",
			}),
		},
	}
	finalSnap, err := TestOp(Update).Run(project, p.GetTarget(t, origSnap), options, false, p.BackendClient, nil)
	require.NoError(t, err)

	// Check that `resB` (untargeted) is the same between the two snapshots.
	{
		initialState := findResourceByURN(origSnap.Resources, "urn:pulumi:test::test::pkgA:m:typA::resB")
		assert.NotNil(t, initialState, "initial `resB` state not found")

		finalState := findResourceByURN(finalSnap.Resources, "urn:pulumi:test::test::pkgA:m:typA::resB")
		assert.NotNil(t, finalState, "final `resB` state not found")

		assert.Equal(t, initialState, finalState)
	}
}

// TestReplaceSpecificTargetsPlan checks combinations of --target and --replace for expected behavior.
func TestReplaceSpecificTargetsPlan(t *testing.T) {
	t.Parallel()

	p := &TestPlan{}

	loaders := []*deploytest.ProviderLoader{
		deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
			return &deploytest.Provider{}, nil
		}),
	}

	// Initial state
	fooVal := "bar"

	// Don't try to create resB yet.
	createResB := false

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

		_, _, _, err = monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{
			Inputs: resource.PropertyMap{
				"foo": resource.NewStringProperty(fooVal),
			},
			ReplaceOnChanges: []string{"foo"},
		})
		assert.NoError(t, err)

		if createResB {
			// Now try to create resB which is not targeted and should show up in the plan.
			_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resB", true, deploytest.ResourceOptions{
				Inputs: resource.PropertyMap{
					"foo": resource.NewStringProperty(fooVal),
				},
			})
			assert.NoError(t, err)
		}

		err = monitor.RegisterResourceOutputs(stackURN, resource.PropertyMap{
			"foo": resource.NewStringProperty(fooVal),
		})

		assert.NoError(t, err)

		return nil
	})

	p.Options.HostF = deploytest.NewPluginHostF(nil, nil, programF, loaders...)

	project := p.GetProject()

	old, err := TestOp(Update).Run(project, p.GetTarget(t, nil), TestUpdateOptions{
		HostF: p.Options.HostF,
	}, false, p.BackendClient, nil)
	assert.NoError(t, err)

	// Configure next update.
	fooVal = "changed-from-bar" // This triggers a replace

	// Now try to create resB.
	createResB = true

	urnA := resource.URN("urn:pulumi:test::test::pkgA:m:typA::resA")
	urnB := resource.URN("urn:pulumi:test::test::pkgA:m:typA::resB")

	// `--target-replace a`
	t.Run("EnsureUntargetedIsSame", func(t *testing.T) {
		t.Parallel()
		// Create the update plan with only targeted resources.
		plan, err := TestOp(Update).Plan(project, p.GetTarget(t, old), TestUpdateOptions{
			HostF: p.Options.HostF,
			UpdateOptions: UpdateOptions{
				Experimental: true,
				GeneratePlan: true,

				// `--target-replace a` means ReplaceTargets and UpdateTargets are both set for a.
				Targets: deploy.NewUrnTargetsFromUrns([]resource.URN{
					urnA,
				}),
				ReplaceTargets: deploy.NewUrnTargetsFromUrns([]resource.URN{
					urnA,
				}),
			},
		}, p.BackendClient, nil)
		assert.NoError(t, err)
		assert.NotNil(t, plan)

		// Ensure resB is in the plan.
		foundResB := false
		for _, r := range plan.ResourcePlans {
			if r.Goal == nil {
				continue
			}
			switch r.Goal.Name {
			case "resB":
				foundResB = true
				// Ensure resB is created in the plan.
				assert.Equal(t, []display.StepOp{
					deploy.OpSame,
				}, r.Ops)
			}
		}
		assert.True(t, foundResB, "resB should be in the plan")
	})

	// `--replace a`
	t.Run("EnsureReplaceTargetIsReplacedAndNotTargeted", func(t *testing.T) {
		t.Parallel()
		// Create the update plan with only targeted resources.
		plan, err := TestOp(Update).Plan(project, p.GetTarget(t, old), TestUpdateOptions{
			HostF: p.Options.HostF,
			UpdateOptions: UpdateOptions{
				Experimental: true,
				GeneratePlan: true,

				// `--replace a` means ReplaceTargets is set. It is not a targeted update.
				// Both a and b should be changed.
				ReplaceTargets: deploy.NewUrnTargetsFromUrns([]resource.URN{
					urnA,
				}),
			},
		}, p.BackendClient, nil)
		assert.NoError(t, err)
		assert.NotNil(t, plan)

		foundResA := false
		foundResB := false
		for _, r := range plan.ResourcePlans {
			if r.Goal == nil {
				continue
			}
			switch r.Goal.Name {
			case "resA":
				foundResA = true
				assert.Equal(t, []display.StepOp{
					deploy.OpCreateReplacement,
					deploy.OpReplace,
					deploy.OpDeleteReplaced,
				}, r.Ops)
			case "resB":
				foundResB = true
				assert.Equal(t, []display.StepOp{
					deploy.OpCreate,
				}, r.Ops)
			}
		}
		assert.True(t, foundResA, "resA should be in the plan")
		assert.True(t, foundResB, "resB should be in the plan")
	})

	// `--replace a --target b`
	// This is a targeted update where the `--replace a` is irrelevant as a is not targeted.
	t.Run("EnsureUntargetedReplaceTargetIsNotReplaced", func(t *testing.T) {
		t.Parallel()
		// Create the update plan with only targeted resources.
		plan, err := TestOp(Update).Plan(project, p.GetTarget(t, old), TestUpdateOptions{
			HostF: p.Options.HostF,
			UpdateOptions: UpdateOptions{
				Experimental: true,
				GeneratePlan: true,

				Targets: deploy.NewUrnTargetsFromUrns([]resource.URN{
					urnB,
				}),
				ReplaceTargets: deploy.NewUrnTargetsFromUrns([]resource.URN{
					urnA,
				}),
			},
		}, p.BackendClient, nil)
		assert.NoError(t, err)
		assert.NotNil(t, plan)

		foundResA := false
		foundResB := false
		for _, r := range plan.ResourcePlans {
			if r.Goal == nil {
				continue
			}
			switch r.Goal.Name {
			case "resA":
				foundResA = true
				assert.Equal(t, []display.StepOp{
					deploy.OpSame,
				}, r.Ops)
			case "resB":
				foundResB = true
				assert.Equal(t, []display.StepOp{
					deploy.OpCreate,
				}, r.Ops)
			}
		}
		assert.True(t, foundResA, "resA should be in the plan")
		assert.True(t, foundResB, "resB should be in the plan")
	})
}

func TestTargetDependents(t *testing.T) {
	// Regression test for https://github.com/pulumi/pulumi/pull/13560. This test ensures that when
	// --target-dependents is set we don't start creating untargted resources.
	t.Parallel()

	loaders := []*deploytest.ProviderLoader{
		deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
			return &deploytest.Provider{}, nil
		}),
	}

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

		_, _, _, err = monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{})
		assert.NoError(t, err)

		_, _, _, err = monitor.RegisterResource("pkgA:m:typA", "resB", true, deploytest.ResourceOptions{})
		assert.NoError(t, err)

		return nil
	})

	hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
	p := &TestPlan{}

	project := p.GetProject()

	// Target only resA and check only A is created
	snap, err := TestOp(Update).Run(project, p.GetTarget(t, nil), TestUpdateOptions{
		HostF: hostF,
		UpdateOptions: UpdateOptions{
			Targets:          deploy.NewUrnTargets([]string{"urn:pulumi:test::test::pkgA:m:typA::resA"}),
			TargetDependents: false,
		},
	}, false, p.BackendClient, nil)
	require.NoError(t, err)
	// Check we only have three resources, stack, provider, and resA
	require.Equal(t, 3, len(snap.Resources))

	// Run another fresh update (note we're starting from a nil snapshot again), and target only resA and check
	// only A is created but also turn on --target-dependents.
	snap, err = TestOp(Update).Run(project, p.GetTarget(t, nil), TestUpdateOptions{
		HostF: hostF,
		UpdateOptions: UpdateOptions{
			Targets:          deploy.NewUrnTargets([]string{"urn:pulumi:test::test::pkgA:m:typA::resA"}),
			TargetDependents: true,
		},
	}, false, p.BackendClient, nil)
	require.NoError(t, err)
	// Check we still only have three resources, stack, provider, and resA
	require.Equal(t, 3, len(snap.Resources))
}

func TestTargetDependentsExplicitProvider(t *testing.T) {
	// Regression test for https://github.com/pulumi/pulumi/pull/13560. This test ensures that when
	// --target-dependents is set we still target explicit providers resources.
	t.Parallel()

	loaders := []*deploytest.ProviderLoader{
		deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
			return &deploytest.Provider{}, nil
		}),
	}

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

		provURN, provID, _, err := monitor.RegisterResource(
			providers.MakeProviderType("pkgA"), "provider", true, deploytest.ResourceOptions{})
		assert.NoError(t, err)

		if provID == "" {
			provID = providers.UnknownID
		}

		provRef, err := providers.NewReference(provURN, provID)
		assert.NoError(t, err)

		_, _, _, err = monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{
			Provider: provRef.String(),
		})
		assert.NoError(t, err)

		_, _, _, err = monitor.RegisterResource("pkgA:m:typA", "resB", true, deploytest.ResourceOptions{
			Provider: provRef.String(),
		})
		assert.NoError(t, err)

		return nil
	})

	hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
	p := &TestPlan{}

	project := p.GetProject()

	// Target only the explicit provider and check that only the provider is created
	snap, err := TestOp(Update).Run(project, p.GetTarget(t, nil), TestUpdateOptions{
		HostF: hostF,
		UpdateOptions: UpdateOptions{
			Targets:          deploy.NewUrnTargets([]string{"urn:pulumi:test::test::pulumi:providers:pkgA::provider"}),
			TargetDependents: false,
		},
	}, false, p.BackendClient, nil)
	require.NoError(t, err)
	// Check we only have two resources, stack, and provider
	require.Equal(t, 2, len(snap.Resources))

	// Run another fresh update (note we're starting from a nil snapshot again), and target only the provider
	// but turn on  --target-dependents and check the provider, A, and B are created
	snap, err = TestOp(Update).Run(project, p.GetTarget(t, nil), TestUpdateOptions{
		HostF: hostF,
		UpdateOptions: UpdateOptions{
			Targets:          deploy.NewUrnTargets([]string{"urn:pulumi:test::test::pulumi:providers:pkgA::provider"}),
			TargetDependents: true,
		},
	}, false, p.BackendClient, nil)
	require.NoError(t, err)
	// Check we still only have four resources, stack, provider, resA, and resB.
	require.Equal(t, 4, len(snap.Resources))
}

func TestTargetDependentsSiblingResources(t *testing.T) {
	// Regression test for https://github.com/pulumi/pulumi/pull/13591. This test ensures that when
	// --target-dependents is set we don't target sibling resources (that is resources created by the same
	// provider as the one being targeted).
	t.Parallel()

	loaders := []*deploytest.ProviderLoader{
		deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
			return &deploytest.Provider{}, nil
		}),
	}

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

		// We're creating 8 resources here (one the implicit default provider). First we create three
		// pkgA:m:typA resources called "implicitX", "implicitY", and "implicitZ" (which will trigger the
		// creation of the default provider for pkgA). Second we create an explicit provider for pkgA and then
		// create three resources using that ("explicitX", "explicitY", and "explicitZ"). We want to check
		// that if we target the X resources, the Y resources aren't created, but the providers are, and the Z
		// resources are if --target-dependents is on.

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

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

		_, _, _, err = monitor.RegisterResource("pkgA:m:typA", "implicitZ", true, deploytest.ResourceOptions{
			Parent: implicitX,
		})
		assert.NoError(t, err)

		provURN, provID, _, err := monitor.RegisterResource(
			providers.MakeProviderType("pkgA"), "provider", true, deploytest.ResourceOptions{})
		assert.NoError(t, err)

		if provID == "" {
			provID = providers.UnknownID
		}

		provRef, err := providers.NewReference(provURN, provID)
		assert.NoError(t, err)

		explicitX, _, _, err := monitor.RegisterResource("pkgA:m:typA", "explicitX", true, deploytest.ResourceOptions{
			Provider: provRef.String(),
		})
		assert.NoError(t, err)

		_, _, _, err = monitor.RegisterResource("pkgA:m:typA", "explicitY", true, deploytest.ResourceOptions{
			Provider: provRef.String(),
		})
		assert.NoError(t, err)

		_, _, _, err = monitor.RegisterResource("pkgA:m:typA", "explicitZ", true, deploytest.ResourceOptions{
			Parent: explicitX,
		})
		assert.NoError(t, err)

		return nil
	})

	hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
	p := &TestPlan{}

	project := p.GetProject()

	// Target implicitX and explicitX and ensure that those, their children and the providers are created.
	snap, err := TestOp(Update).Run(project, p.GetTarget(t, nil), TestUpdateOptions{
		HostF: hostF,
		UpdateOptions: UpdateOptions{
			Targets: deploy.NewUrnTargets([]string{
				"urn:pulumi:test::test::pkgA:m:typA::implicitX",
				"urn:pulumi:test::test::pkgA:m:typA::explicitX",
			}),
			TargetDependents: false,
		},
	}, false, p.BackendClient, nil)
	require.NoError(t, err)
	// Check we only have the 5 resources expected, the stack, the two providers and the two X resources.
	require.Equal(t, 5, len(snap.Resources))

	// Run another fresh update (note we're starting from a nil snapshot again) but turn on
	// --target-dependents and check we get 7 resources, the same set as above plus the two Z resources.
	snap, err = TestOp(Update).Run(project, p.GetTarget(t, nil), TestUpdateOptions{
		HostF: hostF,
		UpdateOptions: UpdateOptions{
			Targets: deploy.NewUrnTargets([]string{
				"urn:pulumi:test::test::pkgA:m:typA::implicitX",
				"urn:pulumi:test::test::pkgA:m:typA::explicitX",
			}),
			TargetDependents: true,
		},
	}, false, p.BackendClient, nil)
	require.NoError(t, err)
	require.Equal(t, 7, len(snap.Resources))
}