// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1

package deferring

import (
	"testing"

	"github.com/zclconf/go-cty/cty"

	"github.com/hashicorp/terraform/internal/addrs"
	"github.com/hashicorp/terraform/internal/plans"
	"github.com/hashicorp/terraform/internal/providers"
)

func TestDeferred_externalDependency(t *testing.T) {
	deferred := NewDeferred(true)

	// This reports that something outside of the modules runtime knows that
	// everything in this configuration depends on some elsewhere-action
	// that has been deferred, and so the modules runtime must respect that
	// even though it doesn't know the details of why it is so.
	deferred.SetExternalDependencyDeferred()

	// With the above flag set, now ShouldDeferResourceInstanceChanges should
	// return true regardless of any other information.
	got := deferred.ShouldDeferResourceInstanceChanges(addrs.AbsResourceInstance{
		Module: addrs.RootModuleInstance,
		Resource: addrs.ResourceInstance{
			Resource: addrs.Resource{
				Mode: addrs.ManagedResourceMode,
				Type: "anything",
				Name: "really-anything",
			},
		},
	}, nil)
	if !got {
		t.Errorf("did not report that the instance should have its changes deferred; should have")
	}
}

func TestDeferred_absResourceInstanceDeferred(t *testing.T) {
	instAAddr := addrs.AbsResourceInstance{
		Module: addrs.RootModuleInstance.Child("foo", addrs.NoKey),
		Resource: addrs.ResourceInstance{
			Resource: addrs.Resource{
				Mode: addrs.ManagedResourceMode,
				Type: "test",
				Name: "a",
			},
		},
	}
	instBAddr := addrs.AbsResourceInstance{
		Module: addrs.RootModuleInstance,
		Resource: addrs.ResourceInstance{
			Resource: addrs.Resource{
				Mode: addrs.ManagedResourceMode,
				Type: "test",
				Name: "a",
			},
		},
	}
	instCAddr := addrs.AbsResourceInstance{
		Module: addrs.RootModuleInstance,
		Resource: addrs.ResourceInstance{
			Resource: addrs.Resource{
				Mode: addrs.ManagedResourceMode,
				Type: "test",
				Name: "c",
			},
		},
	}

	dependencies := addrs.MakeMap[addrs.ConfigResource, []addrs.ConfigResource](
		addrs.MapElem[addrs.ConfigResource, []addrs.ConfigResource]{
			Key:   instCAddr.ConfigResource(),
			Value: []addrs.ConfigResource{instBAddr.ConfigResource(), instAAddr.ConfigResource()},
		})

	deferred := NewDeferred(true)

	// Before we report anything, all three addresses should indicate that
	// they don't need to have their actions deferred.
	t.Run("without any deferrals yet", func(t *testing.T) {
		for _, instAddr := range []addrs.AbsResourceInstance{instAAddr, instBAddr, instCAddr} {
			if deferred.ShouldDeferResourceInstanceChanges(instAddr, dependencies.Get(instAddr.ConfigResource())) {
				t.Errorf("%s reported as needing deferred; should not be, yet", instAddr)
			}
		}
	})

	// Instance A has its Create action deferred for some reason.
	deferred.ReportResourceInstanceDeferred(instAAddr, providers.DeferredReasonResourceConfigUnknown, &plans.ResourceInstanceChange{
		Addr: instAAddr,
		Change: plans.Change{
			Action: plans.Create,
			After:  cty.DynamicVal,
		},
	})

	t.Run("with one resource instance deferred", func(t *testing.T) {
		if !deferred.ShouldDeferResourceInstanceChanges(instCAddr, dependencies.Get(instCAddr.ConfigResource())) {
			t.Errorf("%s was not reported as needing deferred; should be deferred", instCAddr)
		}
		if deferred.ShouldDeferResourceInstanceChanges(instBAddr, dependencies.Get(instBAddr.ConfigResource())) {
			t.Errorf("%s reported as needing deferred; should not be", instCAddr)
		}
	})
}

func TestDeferred_absDataSourceInstanceDeferred(t *testing.T) {
	instAAddr := addrs.AbsResourceInstance{
		Module: addrs.RootModuleInstance.Child("foo", addrs.NoKey),
		Resource: addrs.ResourceInstance{
			Resource: addrs.Resource{
				Mode: addrs.DataResourceMode,
				Type: "test",
				Name: "a",
			},
		},
	}
	instBAddr := addrs.AbsResourceInstance{
		Module: addrs.RootModuleInstance,
		Resource: addrs.ResourceInstance{
			Resource: addrs.Resource{
				Mode: addrs.DataResourceMode,
				Type: "test",
				Name: "b",
			},
		},
	}
	instCAddr := addrs.AbsResourceInstance{
		Module: addrs.RootModuleInstance,
		Resource: addrs.ResourceInstance{
			Resource: addrs.Resource{
				Mode: addrs.ManagedResourceMode,
				Type: "test",
				Name: "c",
			},
		},
	}

	dependencies := addrs.MakeMap[addrs.ConfigResource, []addrs.ConfigResource](
		addrs.MapElem[addrs.ConfigResource, []addrs.ConfigResource]{
			Key:   instCAddr.ConfigResource(),
			Value: []addrs.ConfigResource{instBAddr.ConfigResource(), instAAddr.ConfigResource()},
		})
	deferred := NewDeferred(true)

	// Before we report anything, all three addresses should indicate that
	// they don't need to have their actions deferred.
	t.Run("without any deferrals yet", func(t *testing.T) {
		for _, instAddr := range []addrs.AbsResourceInstance{instAAddr, instBAddr, instCAddr} {
			if deferred.ShouldDeferResourceInstanceChanges(instAddr, dependencies.Get(instAddr.ConfigResource())) {
				t.Errorf("%s reported as needing deferred; should not be, yet", instAddr)
			}
		}
	})

	// Instance A has its Read action deferred for some reason.
	deferred.ReportDataSourceInstanceDeferred(instAAddr, providers.DeferredReasonProviderConfigUnknown, &plans.ResourceInstanceChange{
		Addr:        instAAddr,
		PrevRunAddr: instAAddr,
		Change: plans.Change{
			Action: plans.Read,
			After:  cty.DynamicVal,
		},
		ActionReason: plans.ResourceInstanceReadBecauseDependencyPending,
	})

	t.Run("with one resource instance deferred", func(t *testing.T) {
		if !deferred.ShouldDeferResourceInstanceChanges(instCAddr, dependencies.Get(instCAddr.ConfigResource())) {
			t.Errorf("%s was not reported as needing deferred; should be deferred", instCAddr)
		}
		if deferred.ShouldDeferResourceInstanceChanges(instBAddr, dependencies.Get(instBAddr.ConfigResource())) {
			t.Errorf("%s reported as needing deferred; should not be", instCAddr)
		}
	})
}

func TestDeferred_absEphemeralResourceInstanceDeferred(t *testing.T) {
	instAAddr := addrs.AbsResourceInstance{
		Module: addrs.RootModuleInstance.Child("foo", addrs.NoKey),
		Resource: addrs.ResourceInstance{
			Resource: addrs.Resource{
				Mode: addrs.EphemeralResourceMode,
				Type: "test",
				Name: "a",
			},
		},
	}
	instBAddr := addrs.AbsResourceInstance{
		Module: addrs.RootModuleInstance,
		Resource: addrs.ResourceInstance{
			Resource: addrs.Resource{
				Mode: addrs.EphemeralResourceMode,
				Type: "test",
				Name: "b",
			},
		},
	}
	instCAddr := addrs.AbsResourceInstance{
		Module: addrs.RootModuleInstance,
		Resource: addrs.ResourceInstance{
			Resource: addrs.Resource{
				Mode: addrs.ManagedResourceMode,
				Type: "test",
				Name: "c",
			},
		},
	}

	dependencies := addrs.MakeMap[addrs.ConfigResource, []addrs.ConfigResource](
		addrs.MapElem[addrs.ConfigResource, []addrs.ConfigResource]{
			Key:   instCAddr.ConfigResource(),
			Value: []addrs.ConfigResource{instBAddr.ConfigResource(), instAAddr.ConfigResource()},
		})
	deferred := NewDeferred(true)

	// Before we report anything, all three addresses should indicate that
	// they don't need to have their actions deferred.
	t.Run("without any deferrals yet", func(t *testing.T) {
		for _, instAddr := range []addrs.AbsResourceInstance{instAAddr, instBAddr, instCAddr} {
			if deferred.ShouldDeferResourceInstanceChanges(instAddr, dependencies.Get(instAddr.ConfigResource())) {
				t.Errorf("%s reported as needing deferred; should not be, yet", instAddr)
			}
		}
	})

	// Instance A has e.g. the open action deferred
	deferred.ReportEphemeralResourceInstanceDeferred(instAAddr, providers.DeferredReasonProviderConfigUnknown)

	t.Run("with one resource instance deferred", func(t *testing.T) {
		if !deferred.ShouldDeferResourceInstanceChanges(instCAddr, dependencies.Get(instCAddr.ConfigResource())) {
			t.Errorf("%s was not reported as needing deferred; should be deferred", instCAddr)
		}
		if deferred.ShouldDeferResourceInstanceChanges(instBAddr, dependencies.Get(instBAddr.ConfigResource())) {
			t.Errorf("%s reported as needing deferred; should not be", instCAddr)
		}
	})
}

func TestDeferred_partialExpandedDatasource(t *testing.T) {
	instAAddr := addrs.AbsResourceInstance{
		Module: addrs.RootModuleInstance.Child("foo", addrs.NoKey),
		Resource: addrs.ResourceInstance{
			Resource: addrs.Resource{
				Mode: addrs.DataResourceMode,
				Type: "test",
				Name: "a",
			},
		},
	}
	instBAddr := addrs.AbsResourceInstance{
		Module: addrs.RootModuleInstance,
		Resource: addrs.ResourceInstance{
			Resource: addrs.Resource{
				Mode: addrs.ManagedResourceMode,
				Type: "test",
				Name: "a",
			},
		},
	}
	instCAddr := addrs.AbsResourceInstance{
		Module: addrs.RootModuleInstance,
		Resource: addrs.ResourceInstance{
			Resource: addrs.Resource{
				Mode: addrs.DataResourceMode,
				Type: "test",
				Name: "c",
			},
		},
	}
	instAPartial := addrs.RootModuleInstance.
		UnexpandedChild(addrs.ModuleCall{Name: "foo"}).
		Resource(instAAddr.Resource.Resource)

	dependencies := addrs.MakeMap[addrs.ConfigResource, []addrs.ConfigResource](
		addrs.MapElem[addrs.ConfigResource, []addrs.ConfigResource]{
			Key:   instCAddr.ConfigResource(),
			Value: []addrs.ConfigResource{instBAddr.ConfigResource(), instAAddr.ConfigResource()},
		})
	deferred := NewDeferred(true)

	// Before we report anything, all three addresses should indicate that
	// they don't need to have their actions deferred.
	t.Run("without any deferrals yet", func(t *testing.T) {
		for _, instAddr := range []addrs.AbsResourceInstance{instAAddr, instBAddr, instCAddr} {
			if deferred.ShouldDeferResourceInstanceChanges(instAddr, dependencies.Get(instAddr.ConfigResource())) {
				t.Errorf("%s reported as needing deferred; should not be, yet", instAddr)
			}
		}
	})

	// Resource A hasn't been expanded fully, so is deferred.
	deferred.ReportDataSourceExpansionDeferred(instAPartial, &plans.ResourceInstanceChange{
		Addr: instAAddr,
		Change: plans.Change{
			Action: plans.Read,
			After:  cty.DynamicVal,
		},
	})

	t.Run("with one resource instance deferred", func(t *testing.T) {
		if !deferred.ShouldDeferResourceInstanceChanges(instCAddr, dependencies.Get(instCAddr.ConfigResource())) {
			t.Errorf("%s was not reported as needing deferred; should be deferred", instCAddr)
		}
		if deferred.ShouldDeferResourceInstanceChanges(instBAddr, dependencies.Get(instBAddr.ConfigResource())) {
			t.Errorf("%s reported as needing deferred; should not be", instCAddr)
		}
	})

}

func TestDeferred_partialExpandedResource(t *testing.T) {
	instAAddr := addrs.AbsResourceInstance{
		Module: addrs.RootModuleInstance.Child("foo", addrs.NoKey),
		Resource: addrs.ResourceInstance{
			Resource: addrs.Resource{
				Mode: addrs.ManagedResourceMode,
				Type: "test",
				Name: "a",
			},
		},
	}
	instBAddr := addrs.AbsResourceInstance{
		Module: addrs.RootModuleInstance,
		Resource: addrs.ResourceInstance{
			Resource: addrs.Resource{
				Mode: addrs.ManagedResourceMode,
				Type: "test",
				Name: "a",
			},
		},
	}
	instCAddr := addrs.AbsResourceInstance{
		Module: addrs.RootModuleInstance,
		Resource: addrs.ResourceInstance{
			Resource: addrs.Resource{
				Mode: addrs.ManagedResourceMode,
				Type: "test",
				Name: "c",
			},
		},
	}
	instAPartial := addrs.RootModuleInstance.
		UnexpandedChild(addrs.ModuleCall{Name: "foo"}).
		Resource(instAAddr.Resource.Resource)

	dependencies := addrs.MakeMap[addrs.ConfigResource, []addrs.ConfigResource](
		addrs.MapElem[addrs.ConfigResource, []addrs.ConfigResource]{
			Key:   instCAddr.ConfigResource(),
			Value: []addrs.ConfigResource{instBAddr.ConfigResource(), instAAddr.ConfigResource()},
		})
	deferred := NewDeferred(true)

	// Before we report anything, all three addresses should indicate that
	// they don't need to have their actions deferred.
	t.Run("without any deferrals yet", func(t *testing.T) {
		for _, instAddr := range []addrs.AbsResourceInstance{instAAddr, instBAddr, instCAddr} {
			if deferred.ShouldDeferResourceInstanceChanges(instAddr, dependencies.Get(instAddr.ConfigResource())) {
				t.Errorf("%s reported as needing deferred; should not be, yet", instAddr)
			}
		}
	})

	// Resource A hasn't been expanded fully, so is deferred.
	deferred.ReportResourceExpansionDeferred(instAPartial, &plans.ResourceInstanceChange{
		Addr: instAAddr,
		Change: plans.Change{
			Action: plans.Create,
			After:  cty.DynamicVal,
		},
	})

	t.Run("with one resource instance deferred", func(t *testing.T) {
		if !deferred.ShouldDeferResourceInstanceChanges(instCAddr, dependencies.Get(instCAddr.ConfigResource())) {
			t.Errorf("%s was not reported as needing deferred; should be deferred", instCAddr)
		}
		if deferred.ShouldDeferResourceInstanceChanges(instBAddr, dependencies.Get(instBAddr.ConfigResource())) {
			t.Errorf("%s reported as needing deferred; should not be", instCAddr)
		}
	})
}
