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

package edit

import (
	"testing"
	"time"

	"github.com/pulumi/pulumi/pkg/v3/secrets/b64"

	"github.com/pulumi/pulumi/pkg/v3/resource/deploy"
	"github.com/pulumi/pulumi/pkg/v3/resource/deploy/providers"
	"github.com/pulumi/pulumi/pkg/v3/version"
	"github.com/pulumi/pulumi/sdk/v3/go/common/apitype"
	"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
	"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func NewResource(name string, provider *resource.State, deps ...resource.URN) *resource.State {
	prov := ""
	if provider != nil {
		p, err := providers.NewReference(provider.URN, provider.ID)
		if err != nil {
			panic(err)
		}
		prov = p.String()
	}

	t := tokens.Type("a:b:c")
	return &resource.State{
		Type:         t,
		URN:          resource.NewURN("test", "test", "", t, name),
		Inputs:       resource.PropertyMap{},
		Outputs:      resource.PropertyMap{},
		Dependencies: deps,
		Provider:     prov,
	}
}

func NewProviderResource(pkg, name, id string, deps ...resource.URN) *resource.State {
	t := providers.MakeProviderType(tokens.Package(pkg))
	return &resource.State{
		Type:         t,
		URN:          resource.NewURN("test", "test", "", t, name),
		ID:           resource.ID(id),
		Inputs:       resource.PropertyMap{},
		Outputs:      resource.PropertyMap{},
		Dependencies: deps,
	}
}

func NewSnapshot(resources []*resource.State) *deploy.Snapshot {
	return deploy.NewSnapshot(deploy.Manifest{
		Time:    time.Now(),
		Version: version.Version,
		Plugins: nil,
	}, b64.NewBase64SecretsManager(), resources, nil)
}

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

	pA := NewProviderResource("a", "p1", "0")
	a := NewResource("a", pA)
	b := NewResource("b", pA)
	c := NewResource("c", pA)
	snap := NewSnapshot([]*resource.State{
		pA,
		a,
		b,
		c,
	})

	err := DeleteResource(snap, b, nil, false)
	assert.NoError(t, err)
	assert.Len(t, snap.Resources, 3)
	assert.Equal(t, []*resource.State{pA, a, c}, snap.Resources)
}

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

	pA := NewProviderResource("a", "p1", "0")
	a := NewResource("a", pA)

	// Create duplicate resources.
	b1 := NewResource("b", pA)
	b2 := NewResource("b", pA)
	b3 := NewResource("b", pA)

	// ensure b1, b2, and b3 must have the same URN.
	bURN := b1.URN
	assert.Equal(t, bURN, b1.URN)
	assert.Equal(t, bURN, b2.URN)
	assert.Equal(t, bURN, b3.URN)

	// c exists to check behavior on b's dependents.
	c := NewResource("c", pA, bURN)

	// This test ensures that when targeting dependent resources, deleting a
	// resource with a redundant URN will not delete dependent resources in
	// state as it's ambiguous since another URN can satisfy the dependency.
	t.Run("do-target-dependents", func(t *testing.T) {
		t.Parallel()
		snap := NewSnapshot([]*resource.State{
			pA, a, b1, b2, b3, c,
		})

		err := DeleteResource(snap, b1, nil, true /* targetDependents */)
		require.NoError(t, err)

		assert.Equal(t, []*resource.State{
			pA, a, b2, b3, c,
		}, snap.Resources)

		// Ensure that a pointer to b1 is not in the list.
		for _, s := range snap.Resources {
			assert.False(t, s == b1)
		}
	})

	// This test ensures that when targeting a resource with a redundant URN,
	// dependency checks should not block the resource from being deleted from state.
	t.Run("do-not-target-dependents", func(t *testing.T) {
		t.Parallel()
		snap := NewSnapshot([]*resource.State{
			pA, a, b1, b2, b3, c,
		})

		err := DeleteResource(snap, b1, nil, false /* targetDependents */)
		require.NoError(t, err)

		assert.Equal(t, []*resource.State{
			pA, a, b2, b3, c,
		}, snap.Resources)

		// Ensure that a pointer to b1 is not in the list.
		for _, s := range snap.Resources {
			assert.False(t, s == b1)
		}
	})
}

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

	// Create duplicate provider resources
	pA0 := NewProviderResource("a", "p1", "0")
	pA1 := NewProviderResource("a", "p1", "1")

	// Create a resource that depends on the duplicate Provider.
	b0 := NewResource("b", pA0)
	b1 := NewResource("b", pA1)
	assert.Equal(t, b0.URN, b1.URN)

	c := NewResource("c", pA1, b0.URN)

	t.Run("do-target-dependents", func(t *testing.T) {
		t.Parallel()
		snap := NewSnapshot([]*resource.State{
			pA0, pA1, b0, b1, c,
		})

		err := DeleteResource(snap, pA0, nil, true /* targetDependents */)
		require.NoError(t, err)

		assert.Equal(t, []*resource.State{
			pA1, b1, c,
		}, snap.Resources)
	})

	t.Run("do-not-target-dependents", func(t *testing.T) {
		t.Parallel()
		snap := NewSnapshot([]*resource.State{
			pA0, pA1, b0, b1, c,
		})

		err := DeleteResource(snap, pA0, nil, false /* targetDependents */)
		require.ErrorContains(t, err,
			"Can't delete resource \"urn:pulumi:test::test::pulumi:providers:a::p1\" due to dependent resources")
	})

	t.Run("do-target-dependents-one-intermediate", func(t *testing.T) {
		t.Parallel()
		snap := NewSnapshot([]*resource.State{
			pA0, pA1, b0, c,
		})

		err := DeleteResource(snap, pA0, nil, true /* targetDependents */)
		require.NoError(t, err)
		assert.Equal(t, []*resource.State{
			pA1,
		}, snap.Resources)
	})

	t.Run("do-target-dependents-one-intermediate", func(t *testing.T) {
		t.Parallel()
		snap := NewSnapshot([]*resource.State{
			pA0, pA1, b0, c,
		})

		err := DeleteResource(snap, pA0, nil, false /* targetDependents */)
		require.ErrorContains(t, err,
			"Can't delete resource \"urn:pulumi:test::test::pulumi:providers:a::p1\" due to dependent resources")
	})
}

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

	// Create duplicate provider resources
	pA0 := NewProviderResource("a", "p1", "0")
	pA1 := NewProviderResource("a", "p1", "1")

	// Create a resource that depends on the duplicate Provider.
	b0 := NewResource("b", pA0)

	c0 := NewProviderResource("c", "p1", "0", b0.URN)
	c1 := NewProviderResource("c", "p1", "1")

	d0 := NewResource("d", c0)
	d1 := NewResource("d", c1)

	t.Run("do-target-dependents", func(t *testing.T) {
		t.Parallel()
		snap := NewSnapshot([]*resource.State{
			pA0, pA1, b0, c0, c1, d0, d1,
		})

		err := DeleteResource(snap, pA0, nil, true /* targetDependents */)
		require.NoError(t, err)

		assert.Equal(t, []*resource.State{
			pA1, c1, d1,
		}, snap.Resources)
	})

	t.Run("do-not-target-dependents", func(t *testing.T) {
		t.Parallel()
		snap := NewSnapshot([]*resource.State{
			pA0, pA1, b0, c0, c1, d0, d1,
		})

		err := DeleteResource(snap, pA0, nil, false /* targetDependents */)
		require.ErrorContains(t, err,
			"Can't delete resource \"urn:pulumi:test::test::pulumi:providers:a::p1\" due to dependent resources")
	})
}

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

	pA := NewProviderResource("a", "p1", "0")
	a := NewResource("a", pA)
	b := NewResource("b", pA)
	c := NewResource("c", pA, a.URN)
	d := NewResource("d", pA, c.URN)
	snap := NewSnapshot([]*resource.State{
		pA, a, b, c, d,
	})

	err := DeleteResource(snap, a, nil, true)
	require.NoError(t, err)

	assert.Equal(t, snap.Resources, []*resource.State{pA, b})
}

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

	pA := NewProviderResource("a", "p1", "0")
	a := NewResource("a", pA)
	b := NewResource("b", pA)
	c := NewResource("c", pA)
	snap := NewSnapshot([]*resource.State{
		pA,
		a,
		b,
		c,
	})

	err := DeleteResource(snap, pA, nil, false)
	assert.Error(t, err)
	depErr, ok := err.(ResourceHasDependenciesError)
	if !assert.True(t, ok) {
		t.FailNow()
	}

	assert.Contains(t, depErr.Dependencies, a)
	assert.Contains(t, depErr.Dependencies, b)
	assert.Contains(t, depErr.Dependencies, c)
	assert.Len(t, snap.Resources, 4)
	assert.Equal(t, []*resource.State{pA, a, b, c}, snap.Resources)
}

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

	pA := NewProviderResource("a", "p1", "0")
	a := NewResource("a", pA)
	b := NewResource("b", pA, a.URN)
	c := NewResource("c", pA)
	snap := NewSnapshot([]*resource.State{
		pA,
		a,
		b,
		c,
	})

	err := DeleteResource(snap, a, nil, false)
	assert.Error(t, err)
	depErr, ok := err.(ResourceHasDependenciesError)
	if !assert.True(t, ok) {
		t.FailNow()
	}

	assert.NotContains(t, depErr.Dependencies, pA)
	assert.NotContains(t, depErr.Dependencies, a)
	assert.Contains(t, depErr.Dependencies, b)
	assert.NotContains(t, depErr.Dependencies, c)
	assert.Len(t, snap.Resources, 4)
	assert.Equal(t, []*resource.State{pA, a, b, c}, snap.Resources)
}

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

	pA := NewProviderResource("a", "p1", "0")
	a := NewResource("a", pA)
	a.Protect = true
	snap := NewSnapshot([]*resource.State{
		pA,
		a,
	})

	err := DeleteResource(snap, a, nil, false)
	assert.Error(t, err)
	_, ok := err.(ResourceProtectedError)
	assert.True(t, ok)
}

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

	tests := []struct {
		name string
		test func(t *testing.T, pA, a, b, c *resource.State, snap *deploy.Snapshot)
	}{
		{
			"root-protected",
			func(t *testing.T, pA, a, b, c *resource.State, snap *deploy.Snapshot) {
				a.Protect = true
				protectedCount := 0
				err := DeleteResource(snap, a, func(s *resource.State) error {
					s.Protect = false
					protectedCount++
					return nil
				}, false)
				assert.NoError(t, err)
				assert.Equal(t, protectedCount, 1)
				assert.Equal(t, snap.Resources, []*resource.State{pA, b, c})
			},
		},
		{
			"root-and-branch",
			func(t *testing.T, pA, a, b, c *resource.State, snap *deploy.Snapshot) {
				a.Protect = true
				b.Protect = true
				c.Protect = true
				protectedCount := 0
				err := DeleteResource(snap, b, func(s *resource.State) error {
					s.Protect = false
					protectedCount++
					return nil
				}, true)
				assert.NoError(t, err)
				// 2 because we only plan to delete b and c. a is protected but not
				// scheduled for deletion, so we don't call the onProtect handler.
				assert.Equal(t, protectedCount, 2)
				assert.Equal(t, snap.Resources, []*resource.State{pA, a})
			},
		},
		{
			"branch",
			func(t *testing.T, pA, a, b, c *resource.State, snap *deploy.Snapshot) {
				b.Protect = true
				c.Protect = true
				protectedCount := 0
				err := DeleteResource(snap, c, func(s *resource.State) error {
					s.Protect = false
					protectedCount++
					return nil
				}, false)
				assert.NoError(t, err)
				assert.Equal(t, protectedCount, 1)
				assert.Equal(t, snap.Resources, []*resource.State{pA, a, b})
			},
		},
		{
			"no-permission-root",
			func(t *testing.T, pA, a, b, c *resource.State, snap *deploy.Snapshot) {
				c.Protect = true
				err := DeleteResource(snap, c, nil, false).(ResourceProtectedError)
				assert.Equal(t, ResourceProtectedError{
					Condemned: c,
				}, err)
			},
		},
		{
			"no-permission-branch",
			func(t *testing.T, pA, a, b, c *resource.State, snap *deploy.Snapshot) {
				c.Protect = true
				err := DeleteResource(snap, b, nil, true).(ResourceProtectedError)
				assert.Equal(t, ResourceProtectedError{
					Condemned: c,
				}, err)
			},
		},
	}
	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			pA := NewProviderResource("a", "p1", "0")
			a := NewResource("a", pA)
			b := NewResource("b", pA)
			c := NewResource("c", pA, b.URN)
			snap := NewSnapshot([]*resource.State{
				pA,
				a,
				b,
				c,
			})

			tt.test(t, pA, a, b, c, snap)
		})
	}
}

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

	pA := NewProviderResource("a", "p1", "0")
	a := NewResource("a", pA)
	b := NewResource("b", pA)
	b.Parent = a.URN
	c := NewResource("c", pA)
	c.Parent = a.URN
	snap := NewSnapshot([]*resource.State{
		pA,
		a,
		b,
		c,
	})

	err := DeleteResource(snap, a, nil, false)
	assert.Error(t, err)
	depErr, ok := err.(ResourceHasDependenciesError)
	if !assert.True(t, ok) {
		t.FailNow()
	}

	assert.NotContains(t, depErr.Dependencies, pA)
	assert.NotContains(t, depErr.Dependencies, a)
	assert.Contains(t, depErr.Dependencies, b)
	assert.Contains(t, depErr.Dependencies, c)
	assert.Len(t, snap.Resources, 4)
	assert.Equal(t, []*resource.State{pA, a, b, c}, snap.Resources)
}

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

	pA := NewProviderResource("a", "p1", "0")
	a := NewResource("a", pA)
	a.Protect = true
	b := NewResource("b", pA)
	c := NewResource("c", pA)
	snap := NewSnapshot([]*resource.State{
		pA,
		a,
		b,
		c,
	})

	err := UnprotectResource(snap, a)
	assert.NoError(t, err)
	assert.Len(t, snap.Resources, 4)
	assert.Equal(t, []*resource.State{pA, a, b, c}, snap.Resources)
	assert.False(t, a.Protect)
}

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

	pA := NewProviderResource("a", "p1", "0")
	a := NewResource("a", pA)
	b := NewResource("b", pA)
	c := NewResource("c", pA)
	snap := NewSnapshot([]*resource.State{
		pA,
		a,
		b,
		c,
	})

	ty := tokens.Type("a:b:c")
	urn := resource.NewURN("test", "test", "", ty, "not-present")
	resList := LocateResource(snap, urn)
	assert.Nil(t, resList)
}

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

	pA := NewProviderResource("a", "p1", "0")
	a := NewResource("a", pA)
	b := NewResource("b", pA)
	aPending := NewResource("a", pA)
	aPending.Delete = true
	snap := NewSnapshot([]*resource.State{
		pA,
		a,
		b,
		aPending,
	})

	resList := LocateResource(snap, a.URN)
	assert.Len(t, resList, 2)
	assert.Contains(t, resList, a)
	assert.Contains(t, resList, aPending)
	assert.NotContains(t, resList, pA)
	assert.NotContains(t, resList, b)
}

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

	pA := NewProviderResource("a", "p1", "0")
	a := NewResource("a", pA)
	b := NewResource("b", pA)
	c := NewResource("c", pA)
	snap := NewSnapshot([]*resource.State{
		pA,
		a,
		b,
		c,
	})

	resList := LocateResource(snap, a.URN)
	assert.Len(t, resList, 1)
	assert.Contains(t, resList, a)
}

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

	locateResource := func(deployment *apitype.DeploymentV3, urn resource.URN) []apitype.ResourceV3 {
		if deployment == nil {
			return nil
		}

		var resources []apitype.ResourceV3
		for _, res := range deployment.Resources {
			if res.URN == urn {
				resources = append(resources, res)
			}
		}

		return resources
	}

	newResource := func(name string, provider *apitype.ResourceV3, deps ...resource.URN) apitype.ResourceV3 {
		prov := ""
		if provider != nil {
			p, err := providers.NewReference(provider.URN, provider.ID)
			if err != nil {
				panic(err)
			}
			prov = p.String()
		}

		t := tokens.Type("a:b:c")
		return apitype.ResourceV3{
			Type:         t,
			URN:          resource.NewURN("test", "test", "", t, name),
			Inputs:       map[string]interface{}{},
			Outputs:      map[string]interface{}{},
			Dependencies: deps,
			Provider:     prov,
		}
	}

	newProviderResource := func(pkg, name, id string, deps ...resource.URN) apitype.ResourceV3 {
		t := providers.MakeProviderType(tokens.Package(pkg))
		return apitype.ResourceV3{
			Type:         t,
			URN:          resource.NewURN("test", "test", "", t, name),
			ID:           resource.ID(id),
			Inputs:       map[string]interface{}{},
			Outputs:      map[string]interface{}{},
			Dependencies: deps,
		}
	}

	newDeployment := func(resources []apitype.ResourceV3) *apitype.DeploymentV3 {
		return &apitype.DeploymentV3{
			Manifest: apitype.ManifestV1{
				Time:    time.Now(),
				Version: version.Version,
				Plugins: nil,
			},
			Resources: resources,
		}
	}

	pA := newProviderResource("a", "p1", "0")
	a := newResource("a", &pA)
	b := newResource("b", &pA)
	c := newResource("c", &pA)
	deployment := newDeployment([]apitype.ResourceV3{
		pA,
		a,
		b,
		c,
	})

	// Baseline. Can locate resource A.
	resList := locateResource(deployment, a.URN)
	assert.Len(t, resList, 1)
	assert.Contains(t, resList, a)
	if t.Failed() {
		t.Fatal("Unable to find expected resource in initial checkpoint.")
	}
	baselineResourceURN := resList[0].URN

	// The stack name and project are hard-coded in NewResource(...)
	assert.EqualValues(t, "test", baselineResourceURN.Stack())
	assert.EqualValues(t, "test", baselineResourceURN.Project())

	// Rename just the stack.
	//nolint:paralleltest // uses shared stack
	t.Run("JustTheStack", func(t *testing.T) {
		err := RenameStack(deployment, tokens.MustParseStackName("new-stack"), tokens.PackageName(""))
		if err != nil {
			t.Fatalf("Error renaming stack: %v", err)
		}

		// Confirm the previous resource by URN isn't found.
		assert.Len(t, locateResource(deployment, baselineResourceURN), 0)

		// Confirm the resource has been renamed.
		updatedResourceURN := resource.NewURN(
			tokens.QName("new-stack"),
			"test", // project name stayed the same
			"" /*parent type*/, baselineResourceURN.Type(),
			baselineResourceURN.Name())
		assert.Len(t, locateResource(deployment, updatedResourceURN), 1)
	})

	// Rename the stack and project.
	//nolint:paralleltest // uses shared stack
	t.Run("StackAndProject", func(t *testing.T) {
		err := RenameStack(deployment, tokens.MustParseStackName("new-stack2"), tokens.PackageName("new-project"))
		if err != nil {
			t.Fatalf("Error renaming stack: %v", err)
		}

		// Lookup the resource by URN, with both stack and project updated.
		updatedResourceURN := resource.NewURN(
			tokens.QName("new-stack2"),
			"new-project",
			"" /*parent type*/, baselineResourceURN.Type(),
			baselineResourceURN.Name())
		assert.Len(t, locateResource(deployment, updatedResourceURN), 1)
	})
}