// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package tf

import (
	"context"
	"fmt"
	"log"
	"testing"

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

	"github.com/opentofu/opentofu/internal/addrs"
	"github.com/opentofu/opentofu/internal/backend"
	"github.com/opentofu/opentofu/internal/configs/configschema"
	"github.com/opentofu/opentofu/internal/encryption"
	"github.com/opentofu/opentofu/internal/lang/marks"
	"github.com/opentofu/opentofu/internal/states/statemgr"
	"github.com/opentofu/opentofu/internal/tfdiags"
)

func TestResource(t *testing.T) {
	if err := dataSourceRemoteStateGetSchema().Block.InternalValidate(); err != nil {
		t.Fatalf("err: %s", err)
	}
}

func TestState_basic(t *testing.T) {
	var tests = map[string]struct {
		Config cty.Value
		Want   cty.Value
		Err    bool
	}{
		"basic": {
			cty.ObjectVal(map[string]cty.Value{
				"backend": cty.StringVal("local"),
				"config": cty.ObjectVal(map[string]cty.Value{
					"path": cty.StringVal("./testdata/basic.tfstate"),
				}),
			}),
			cty.ObjectVal(map[string]cty.Value{
				"backend": cty.StringVal("local"),
				"config": cty.ObjectVal(map[string]cty.Value{
					"path": cty.StringVal("./testdata/basic.tfstate"),
				}),
				"outputs": cty.ObjectVal(map[string]cty.Value{
					"foo": cty.StringVal("bar"),
				}),
				"defaults":  cty.NullVal(cty.DynamicPseudoType),
				"workspace": cty.NullVal(cty.String),
			}),
			false,
		},
		"workspace": {
			cty.ObjectVal(map[string]cty.Value{
				"backend":   cty.StringVal("local"),
				"workspace": cty.StringVal(backend.DefaultStateName),
				"config": cty.ObjectVal(map[string]cty.Value{
					"path": cty.StringVal("./testdata/basic.tfstate"),
				}),
			}),
			cty.ObjectVal(map[string]cty.Value{
				"backend":   cty.StringVal("local"),
				"workspace": cty.StringVal(backend.DefaultStateName),
				"config": cty.ObjectVal(map[string]cty.Value{
					"path": cty.StringVal("./testdata/basic.tfstate"),
				}),
				"outputs": cty.ObjectVal(map[string]cty.Value{
					"foo": cty.StringVal("bar"),
				}),
				"defaults": cty.NullVal(cty.DynamicPseudoType),
			}),
			false,
		},
		"_local": {
			cty.ObjectVal(map[string]cty.Value{
				"backend": cty.StringVal("_local"),
				"config": cty.ObjectVal(map[string]cty.Value{
					"path": cty.StringVal("./testdata/basic.tfstate"),
				}),
			}),
			cty.ObjectVal(map[string]cty.Value{
				"backend": cty.StringVal("_local"),
				"config": cty.ObjectVal(map[string]cty.Value{
					"path": cty.StringVal("./testdata/basic.tfstate"),
				}),
				"outputs": cty.ObjectVal(map[string]cty.Value{
					"foo": cty.StringVal("bar"),
				}),
				"defaults":  cty.NullVal(cty.DynamicPseudoType),
				"workspace": cty.NullVal(cty.String),
			}),
			false,
		},
		"complex outputs": {
			cty.ObjectVal(map[string]cty.Value{
				"backend": cty.StringVal("local"),
				"config": cty.ObjectVal(map[string]cty.Value{
					"path": cty.StringVal("./testdata/complex_outputs.tfstate"),
				}),
			}),
			cty.ObjectVal(map[string]cty.Value{
				"backend": cty.StringVal("local"),
				"config": cty.ObjectVal(map[string]cty.Value{
					"path": cty.StringVal("./testdata/complex_outputs.tfstate"),
				}),
				"outputs": cty.ObjectVal(map[string]cty.Value{
					"computed_map": cty.MapVal(map[string]cty.Value{
						"key1": cty.StringVal("value1"),
					}),
					"computed_set": cty.ListVal([]cty.Value{
						cty.StringVal("setval1"),
						cty.StringVal("setval2"),
					}),
					"map": cty.MapVal(map[string]cty.Value{
						"key":  cty.StringVal("test"),
						"test": cty.StringVal("test"),
					}),
					"set": cty.ListVal([]cty.Value{
						cty.StringVal("test1"),
						cty.StringVal("test2"),
					}),
				}),
				"defaults":  cty.NullVal(cty.DynamicPseudoType),
				"workspace": cty.NullVal(cty.String),
			}),
			false,
		},
		"null outputs": {
			cty.ObjectVal(map[string]cty.Value{
				"backend": cty.StringVal("local"),
				"config": cty.ObjectVal(map[string]cty.Value{
					"path": cty.StringVal("./testdata/null_outputs.tfstate"),
				}),
			}),
			cty.ObjectVal(map[string]cty.Value{
				"backend": cty.StringVal("local"),
				"config": cty.ObjectVal(map[string]cty.Value{
					"path": cty.StringVal("./testdata/null_outputs.tfstate"),
				}),
				"outputs": cty.ObjectVal(map[string]cty.Value{
					"map":  cty.NullVal(cty.Map(cty.String)),
					"list": cty.NullVal(cty.List(cty.String)),
				}),
				"defaults":  cty.NullVal(cty.DynamicPseudoType),
				"workspace": cty.NullVal(cty.String),
			}),
			false,
		},
		"defaults": {
			cty.ObjectVal(map[string]cty.Value{
				"backend": cty.StringVal("local"),
				"config": cty.ObjectVal(map[string]cty.Value{
					"path": cty.StringVal("./testdata/empty.tfstate"),
				}),
				"defaults": cty.ObjectVal(map[string]cty.Value{
					"foo": cty.StringVal("bar"),
				}),
			}),
			cty.ObjectVal(map[string]cty.Value{
				"backend": cty.StringVal("local"),
				"config": cty.ObjectVal(map[string]cty.Value{
					"path": cty.StringVal("./testdata/empty.tfstate"),
				}),
				"defaults": cty.ObjectVal(map[string]cty.Value{
					"foo": cty.StringVal("bar"),
				}),
				"outputs": cty.ObjectVal(map[string]cty.Value{
					"foo": cty.StringVal("bar"),
				}),
				"workspace": cty.NullVal(cty.String),
			}),
			false,
		},
		"missing": {
			cty.ObjectVal(map[string]cty.Value{
				"backend": cty.StringVal("local"),
				"config": cty.ObjectVal(map[string]cty.Value{
					"path": cty.StringVal("./testdata/missing.tfstate"), // intentionally not present on disk
				}),
			}),
			cty.ObjectVal(map[string]cty.Value{
				"backend": cty.StringVal("local"),
				"config": cty.ObjectVal(map[string]cty.Value{
					"path": cty.StringVal("./testdata/missing.tfstate"),
				}),
				"defaults":  cty.NullVal(cty.DynamicPseudoType),
				"outputs":   cty.EmptyObjectVal,
				"workspace": cty.NullVal(cty.String),
			}),
			true,
		},
		"wrong type for config": {
			cty.ObjectVal(map[string]cty.Value{
				"backend": cty.StringVal("local"),
				"config":  cty.StringVal("nope"),
			}),
			cty.NilVal,
			true,
		},
		"wrong type for config with unknown backend": {
			cty.ObjectVal(map[string]cty.Value{
				"backend": cty.UnknownVal(cty.String),
				"config":  cty.StringVal("nope"),
			}),
			cty.NilVal,
			true,
		},
		"wrong type for config with unknown config": {
			cty.ObjectVal(map[string]cty.Value{
				"backend": cty.StringVal("local"),
				"config":  cty.UnknownVal(cty.String),
			}),
			cty.NilVal,
			true,
		},
		"wrong type for defaults": {
			cty.ObjectVal(map[string]cty.Value{
				"backend": cty.StringVal("local"),
				"config": cty.ObjectVal(map[string]cty.Value{
					"path": cty.StringVal("./testdata/basic.tfstate"),
				}),
				"defaults": cty.StringVal("nope"),
			}),
			cty.NilVal,
			true,
		},
		"config as map": {
			cty.ObjectVal(map[string]cty.Value{
				"backend": cty.StringVal("local"),
				"config": cty.MapVal(map[string]cty.Value{
					"path": cty.StringVal("./testdata/empty.tfstate"),
				}),
			}),
			cty.ObjectVal(map[string]cty.Value{
				"backend": cty.StringVal("local"),
				"config": cty.MapVal(map[string]cty.Value{
					"path": cty.StringVal("./testdata/empty.tfstate"),
				}),
				"defaults":  cty.NullVal(cty.DynamicPseudoType),
				"outputs":   cty.EmptyObjectVal,
				"workspace": cty.NullVal(cty.String),
			}),
			false,
		},
		"defaults as map": {
			cty.ObjectVal(map[string]cty.Value{
				"backend": cty.StringVal("local"),
				"config": cty.ObjectVal(map[string]cty.Value{
					"path": cty.StringVal("./testdata/basic.tfstate"),
				}),
				"defaults": cty.MapValEmpty(cty.String),
			}),
			cty.ObjectVal(map[string]cty.Value{
				"backend": cty.StringVal("local"),
				"config": cty.ObjectVal(map[string]cty.Value{
					"path": cty.StringVal("./testdata/basic.tfstate"),
				}),
				"defaults": cty.MapValEmpty(cty.String),
				"outputs": cty.ObjectVal(map[string]cty.Value{
					"foo": cty.StringVal("bar"),
				}),
				"workspace": cty.NullVal(cty.String),
			}),
			false,
		},
		"nonexistent backend": {
			cty.ObjectVal(map[string]cty.Value{
				"backend": cty.StringVal("nonexistent"),
				"config": cty.ObjectVal(map[string]cty.Value{
					"path": cty.StringVal("./testdata/basic.tfstate"),
				}),
			}),
			cty.NilVal,
			true,
		},
		"null config": {
			cty.ObjectVal(map[string]cty.Value{
				"backend": cty.StringVal("local"),
				"config":  cty.NullVal(cty.DynamicPseudoType),
			}),
			cty.NilVal,
			true,
		},
		"deprecation marks": {
			cty.ObjectVal(map[string]cty.Value{
				"backend": cty.StringVal("local"),
				"config": cty.ObjectVal(map[string]cty.Value{
					"path": cty.StringVal("./testdata/deprecation_warnings.tfstate"),
				}),
			}),
			cty.ObjectVal(map[string]cty.Value{
				"backend": cty.StringVal("local"),
				"config": cty.ObjectVal(map[string]cty.Value{
					"path": cty.StringVal("./testdata/deprecation_warnings.tfstate"),
				}),
				"outputs": cty.ObjectVal(map[string]cty.Value{
					"foo": marks.Deprecated(cty.StringVal("bar"), marks.DeprecationCause{
						By: addrs.ResourceInstance{
							Resource: addrs.Resource{
								Mode: addrs.DataResourceMode,
								Type: "terraform_remote_state",
								Name: "test",
							},
						}.String() + ".foo",
						Message: "I am deprecated",
					}),
				}),
				"defaults":  cty.NullVal(cty.DynamicPseudoType),
				"workspace": cty.NullVal(cty.String),
			}),
			false,
		},
	}
	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			schema := dataSourceRemoteStateGetSchema().Block
			config, err := schema.CoerceValue(test.Config)
			if err != nil {
				t.Fatalf("unexpected error: %v", err)
			}

			diags := dataSourceRemoteStateValidate(config)

			var got cty.Value
			if !diags.HasErrors() && config.IsWhollyKnown() {
				var moreDiags tfdiags.Diagnostics
				got, moreDiags = dataSourceRemoteStateRead(t.Context(), config, encryption.StateEncryptionDisabled(), addrs.AbsResourceInstance{
					Resource: addrs.ResourceInstance{
						Resource: addrs.Resource{
							Mode: addrs.DataResourceMode,
							Type: "terraform_remote_state",
							Name: "test",
						},
					},
				})
				diags = diags.Append(moreDiags)
			}

			if test.Err {
				if !diags.HasErrors() {
					t.Fatal("succeeded; want error")
				}
			} else if diags.HasErrors() {
				t.Fatalf("unexpected errors: %v", diags.Err())
			}

			if test.Want != cty.NilVal && !test.Want.RawEquals(got) {
				t.Errorf("wrong result\nconfig: %sgot:    %swant:   %s", ctydebug.ValueString(config), ctydebug.ValueString(got), ctydebug.ValueString(test.Want))
			}
		})
	}
}

func TestState_validation(t *testing.T) {
	// The main test TestState_basic covers both validation and reading of
	// state snapshots, so this additional test is here only to verify that
	// the validation step in isolation does not attempt to configure
	// the backend.
	overrideBackendFactories = map[string]backend.InitFn{
		"failsconfigure": func(enc encryption.StateEncryption) backend.Backend {
			return backendFailsConfigure{}
		},
	}
	defer func() {
		// undo our overrides so we won't affect other tests
		overrideBackendFactories = nil
	}()

	schema := dataSourceRemoteStateGetSchema().Block
	config, err := schema.CoerceValue(cty.ObjectVal(map[string]cty.Value{
		"backend": cty.StringVal("failsconfigure"),
		"config":  cty.EmptyObjectVal,
	}))
	if err != nil {
		t.Fatalf("unexpected error: %s", err)
	}

	diags := dataSourceRemoteStateValidate(config)
	if diags.HasErrors() {
		t.Fatalf("unexpected errors\n%s", diags.Err().Error())
	}
}

type backendFailsConfigure struct{}

func (b backendFailsConfigure) ConfigSchema() *configschema.Block {
	log.Printf("[TRACE] backendFailsConfigure.ConfigSchema")
	return &configschema.Block{} // intentionally empty configuration schema
}

func (b backendFailsConfigure) PrepareConfig(given cty.Value) (cty.Value, tfdiags.Diagnostics) {
	// No special actions to take here
	return given, nil
}

func (b backendFailsConfigure) Configure(ctx context.Context, config cty.Value) tfdiags.Diagnostics {
	log.Printf("[TRACE] backendFailsConfigure.Configure(%#v)", config)
	var diags tfdiags.Diagnostics
	diags = diags.Append(fmt.Errorf("Configure should never be called"))
	return diags
}

func (b backendFailsConfigure) StateMgr(_ context.Context, workspace string) (statemgr.Full, error) {
	return nil, fmt.Errorf("StateMgr not implemented")
}

func (b backendFailsConfigure) DeleteWorkspace(_ context.Context, name string, _ bool) error {
	return fmt.Errorf("DeleteWorkspace not implemented")
}

func (b backendFailsConfigure) Workspaces(context.Context) ([]string, error) {
	return nil, fmt.Errorf("Workspaces not implemented")
}
