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

package testing

import (
	"fmt"
	"runtime/debug"
	"testing"

	"github.com/hashicorp/go-uuid"
	"github.com/zclconf/go-cty/cty"
	ctyjson "github.com/zclconf/go-cty/cty/json"

	"github.com/hashicorp/terraform/internal/configs/configschema"
	"github.com/hashicorp/terraform/internal/providers"
	testing_provider "github.com/hashicorp/terraform/internal/providers/testing"
	"github.com/hashicorp/terraform/internal/tfdiags"
)

var (
	TestingResourceSchema = providers.Schema{
		Body: &configschema.Block{
			Attributes: map[string]*configschema.Attribute{
				"id":    {Type: cty.String, Optional: true, Computed: true},
				"value": {Type: cty.String, Optional: true},
			},
		},
	}

	TestingEphemeralResourceSchema = providers.Schema{
		Body: &configschema.Block{
			Attributes: map[string]*configschema.Attribute{
				"value": {
					Type:     cty.String,
					Computed: true,
				},
			},
		},
	}

	DeferredResourceSchema = providers.Schema{
		Body: &configschema.Block{
			Attributes: map[string]*configschema.Attribute{
				"id":       {Type: cty.String, Optional: true, Computed: true},
				"value":    {Type: cty.String, Optional: true},
				"deferred": {Type: cty.Bool, Required: true},
			},
		},
	}

	FailedResourceSchema = providers.Schema{
		Body: &configschema.Block{
			Attributes: map[string]*configschema.Attribute{
				"id":         {Type: cty.String, Optional: true, Computed: true},
				"value":      {Type: cty.String, Optional: true},
				"fail_plan":  {Type: cty.Bool, Optional: true, Computed: true},
				"fail_apply": {Type: cty.Bool, Optional: true, Computed: true},
			},
		},
	}

	BlockedResourceSchema = providers.Schema{
		Body: &configschema.Block{
			Attributes: map[string]*configschema.Attribute{
				"id":                 {Type: cty.String, Optional: true, Computed: true},
				"value":              {Type: cty.String, Optional: true},
				"required_resources": {Type: cty.Set(cty.String), Optional: true},
			},
		},
	}

	WriteOnlyResourceSchema = providers.Schema{
		Body: &configschema.Block{
			Attributes: map[string]*configschema.Attribute{
				"id":         {Type: cty.String, Optional: true, Computed: true},
				"value":      {Type: cty.String, Optional: true},
				"write_only": {Type: cty.String, WriteOnly: true, Optional: true},
			},
		},
	}

	TestingDataSourceSchema = providers.Schema{
		Body: &configschema.Block{
			Attributes: map[string]*configschema.Attribute{
				"id":    {Type: cty.String, Required: true},
				"value": {Type: cty.String, Computed: true},
			},
		},
	}

	WriteOnlyDataSourceSchema = providers.Schema{
		Body: &configschema.Block{
			Attributes: map[string]*configschema.Attribute{
				"id":         {Type: cty.String, Required: true},
				"value":      {Type: cty.String, Computed: true},
				"write_only": {Type: cty.String, WriteOnly: true, Optional: true},
			},
		},
	}

	TestingResourceWithIdentitySchema = providers.Schema{
		Body: &configschema.Block{
			Attributes: map[string]*configschema.Attribute{
				"id":    {Type: cty.String, Optional: true, Computed: true},
				"value": {Type: cty.String, Optional: true},
			},
		},
		Identity: &configschema.Object{
			Attributes: map[string]*configschema.Attribute{
				"id": {Type: cty.String, Required: true},
			},
			Nesting: configschema.NestingSingle,
		},
	}

	TestingActionSchema = providers.ActionSchema{
		ConfigSchema: &configschema.Block{
			Attributes: map[string]*configschema.Attribute{
				"message": {Type: cty.String, Optional: true},
			},
		},
	}
)

// MockProvider wraps the standard MockProvider with a simple in-memory
// data store for resources and data sources.
type MockProvider struct {
	*testing_provider.MockProvider

	ResourceStore *ResourceStore

	// If set, authentication means the configuration must provide a value
	// that matches the value here otherwise the Configure function will
	// fail.
	Authentication string
}

// NewProvider returns a new MockProvider with an empty data store.
func NewProvider(t *testing.T) *MockProvider {
	provider := NewProviderWithData(t, NewResourceStore())
	return provider
}

// NewProviderWithData returns a new MockProvider with the given data store.
func NewProviderWithData(t *testing.T, store *ResourceStore) *MockProvider {
	if store == nil {
		store = NewResourceStore()
	}

	// grab the current stack trace so we know where the provider was created
	// in case it isn't being cleaned up properly
	currentStackTrace := debug.Stack()

	provider := &MockProvider{
		MockProvider: &testing_provider.MockProvider{
			GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{
				Provider: providers.Schema{
					Body: &configschema.Block{
						Attributes: map[string]*configschema.Attribute{
							// if the configuration sets require_auth then it
							// must also provide the correct value for
							// authentication
							"authentication": {
								Type:      cty.String,
								Sensitive: true,
								Optional:  true,
							},
							"require_auth": {
								Type:     cty.Bool,
								Optional: true,
							},

							// If this value is provider, the Configure
							// function call will fail and return the value
							// here as part of the error.
							"configure_error": {
								Type:     cty.String,
								Optional: true,
							},

							// ignored allows the configuration to create
							// dependencies from this provider to component
							// blocks and inputs without affecting behaviour.
							"ignored": {
								Type:     cty.String,
								Optional: true,
							},
						},
					},
				},
				ResourceTypes: map[string]providers.Schema{
					"testing_resource": {
						Body: TestingResourceSchema.Body,
					},
					"testing_deferred_resource": {
						Body: DeferredResourceSchema.Body,
					},
					"testing_failed_resource": {
						Body: FailedResourceSchema.Body,
					},
					"testing_blocked_resource": {
						Body: BlockedResourceSchema.Body,
					},
					"testing_resource_with_identity": {
						Body:     TestingResourceSchema.Body,
						Identity: TestingResourceWithIdentitySchema.Identity,
					},
					"testing_write_only_resource": {
						Body: WriteOnlyResourceSchema.Body,
					},
				},
				DataSources: map[string]providers.Schema{
					"testing_data_source": {
						Body: TestingDataSourceSchema.Body,
					},
					"testing_write_only_data_source": {
						Body: WriteOnlyDataSourceSchema.Body,
					},
				},
				EphemeralResourceTypes: map[string]providers.Schema{
					"testing_resource": {
						Body: TestingEphemeralResourceSchema.Body,
					},
				},
				Functions: map[string]providers.FunctionDecl{
					"echo": {
						Parameters: []providers.FunctionParam{
							{Name: "value", Type: cty.DynamicPseudoType},
						},
						ReturnType: cty.DynamicPseudoType,
					},
				},
				Actions: map[string]providers.ActionSchema{
					"testing_action": TestingActionSchema,
				},
				ServerCapabilities: providers.ServerCapabilities{
					MoveResourceState: true,
				},
			},
			PlanResourceChangeFn: func(request providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
				return getResource(request.TypeName).Plan(request, store)
			},
			ApplyResourceChangeFn: func(request providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse {
				return getResource(request.TypeName).Apply(request, store)
			},
			ReadResourceFn: func(request providers.ReadResourceRequest) providers.ReadResourceResponse {
				return getResource(request.TypeName).Read(request, store)
			},
			ReadDataSourceFn: func(request providers.ReadDataSourceRequest) providers.ReadDataSourceResponse {
				var diags tfdiags.Diagnostics

				id := request.Config.GetAttr("id")
				if id.IsNull() {
					diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "missing id", "id is required"))
					return providers.ReadDataSourceResponse{
						Diagnostics: diags,
					}
				}

				value, exists := store.Get(id.AsString())
				if !exists {
					diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "not found", fmt.Sprintf("%q not found", id)))
				}
				return providers.ReadDataSourceResponse{
					State:       value,
					Diagnostics: diags,
				}
			},
			ImportResourceStateFn: func(request providers.ImportResourceStateRequest) providers.ImportResourceStateResponse {
				id := request.ID
				value, exists := store.Get(id)
				if !exists {
					return providers.ImportResourceStateResponse{
						Diagnostics: tfdiags.Diagnostics{
							tfdiags.Sourceless(tfdiags.Error, "not found", fmt.Sprintf("%q not found", id)),
						},
					}
				}
				return providers.ImportResourceStateResponse{
					ImportedResources: []providers.ImportedResource{
						{
							TypeName: request.TypeName,
							State:    value,
						},
					},
				}
			},
			MoveResourceStateFn: func(request providers.MoveResourceStateRequest) providers.MoveResourceStateResponse {
				if request.SourceTypeName != "testing_resource" && request.TargetTypeName != "testing_deferred_resource" {
					return providers.MoveResourceStateResponse{
						Diagnostics: tfdiags.Diagnostics{
							tfdiags.Sourceless(tfdiags.Error, "unsupported", "unsupported move"),
						},
					}
				}
				// So, we know we're moving from `testing_resource` to
				// `testing_deferred_resource`.

				source, err := ctyjson.Unmarshal(request.SourceStateJSON, cty.Object(map[string]cty.Type{
					"id":    cty.String,
					"value": cty.String,
				}))
				if err != nil {
					return providers.MoveResourceStateResponse{
						Diagnostics: tfdiags.Diagnostics{
							tfdiags.Sourceless(tfdiags.Error, "invalid source state", err.Error()),
						},
					}
				}

				target := cty.ObjectVal(map[string]cty.Value{
					"id":       source.GetAttr("id"),
					"value":    source.GetAttr("value"),
					"deferred": cty.False,
				})
				store.Set(source.GetAttr("id").AsString(), target)

				return providers.MoveResourceStateResponse{
					TargetState: target,
				}
			},
			CallFunctionFn: func(request providers.CallFunctionRequest) providers.CallFunctionResponse {
				// Just echo the first argument back as the result.
				return providers.CallFunctionResponse{
					Result: request.Arguments[0],
				}
			},
			OpenEphemeralResourceFn: func(request providers.OpenEphemeralResourceRequest) providers.OpenEphemeralResourceResponse {
				return providers.OpenEphemeralResourceResponse{
					Result: cty.ObjectVal(map[string]cty.Value{
						"value": cty.StringVal("secret"),
					}),
				}
			},
			PlanActionFn: func(request providers.PlanActionRequest) providers.PlanActionResponse {
				// Simple action planning - no drift, just validation
				return providers.PlanActionResponse{
					Diagnostics: tfdiags.Diagnostics{},
				}
			},
			InvokeActionFn: func(request providers.InvokeActionRequest) providers.InvokeActionResponse {
				// Simple action invocation - just emit a completed event
				return providers.InvokeActionResponse{
					Events: func(yield func(providers.InvokeActionEvent) bool) {
						yield(providers.InvokeActionEvent_Completed{
							Diagnostics: tfdiags.Diagnostics{},
						})
					},
					Diagnostics: tfdiags.Diagnostics{},
				}
			},
			ValidateActionConfigFn: func(request providers.ValidateActionConfigRequest) providers.ValidateActionConfigResponse {
				// No validation errors for testing actions
				return providers.ValidateActionConfigResponse{
					Diagnostics: tfdiags.Diagnostics{},
				}
			},
		},
		ResourceStore: store,
	}

	// We want to use internal fields in this function so we have to set it
	// like this.
	provider.ConfigureProviderFn = provider.configure

	t.Cleanup(func() {
		// Fail the test if this provider is not closed.
		if !provider.CloseCalled {
			t.Log(string(currentStackTrace))
			t.Fatalf("provider.Close was not called")
		}
	})

	return provider
}

func (provider *MockProvider) configure(request providers.ConfigureProviderRequest) providers.ConfigureProviderResponse {
	// If configure_error is set, return an error.
	err := request.Config.GetAttr("configure_error")
	if err.IsKnown() && !err.IsNull() {
		return providers.ConfigureProviderResponse{
			Diagnostics: tfdiags.Diagnostics{
				tfdiags.AttributeValue(tfdiags.Error, err.AsString(), "configure_error attribute was set", cty.GetAttrPath("configure_error")),
			},
		}
	}

	// We deliberately only check the authentication if the configuration
	// is providing it. It's entirely up to the config to opt into the
	// authentication which would be crazy for a real provider but just
	// makes things so much simpler for us in testing world.
	requireAuth := request.Config.GetAttr("require_auth")
	if requireAuth.True() {
		authn := request.Config.GetAttr("authentication")
		if authn.IsNull() || !authn.IsKnown() {
			return providers.ConfigureProviderResponse{
				Diagnostics: tfdiags.Diagnostics{
					tfdiags.AttributeValue(tfdiags.Error, "Authentication failed", "authentication field is required", cty.GetAttrPath("authentication")),
				},
			}
		}
		if authn.AsString() != provider.Authentication {
			return providers.ConfigureProviderResponse{
				Diagnostics: tfdiags.Diagnostics{
					tfdiags.AttributeValue(tfdiags.Error, "Authentication failed", "authentication field did not match expected", cty.GetAttrPath("authentication")),
				},
			}
		}
	}

	return providers.ConfigureProviderResponse{}
}

// mustGenerateUUID is a helper to generate a UUID and panic if it fails.
func mustGenerateUUID() string {
	val, err := uuid.GenerateUUID()
	if err != nil {
		panic(err)
	}
	return val
}
