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

package command

import (
	"encoding/json"
	"fmt"
	"path"
	"regexp"
	"slices"
	"strings"
	"testing"

	"github.com/google/go-cmp/cmp"
	"github.com/hashicorp/terraform/internal/command/views"
	"github.com/hashicorp/terraform/internal/configs/configschema"
	"github.com/hashicorp/terraform/internal/providers"
	testing_provider "github.com/hashicorp/terraform/internal/providers/testing"
	tfversion "github.com/hashicorp/terraform/version"
	"github.com/zclconf/go-cty/cty"
)

func TestQuery(t *testing.T) {
	tests := []struct {
		name        string
		directory   string
		expectedOut string
		expectedErr []string
		initCode    int
		args        []string
	}{
		{
			name:      "basic query",
			directory: "basic",
			expectedOut: `list.test_instance.example   id=test-instance-1   Test Instance 1
list.test_instance.example   id=test-instance-2   Test Instance 2

`,
		},
		{
			name:      "query referencing local variable",
			directory: "with-locals",
			expectedOut: `list.test_instance.example   id=test-instance-1   Test Instance 1
list.test_instance.example   id=test-instance-2   Test Instance 2

`,
		},
		{
			name:        "config with no query block",
			directory:   "no-list-block",
			expectedOut: "",
			expectedErr: []string{`
Error: No resources to query

The configuration does not contain any resources that can be queried.
`},
		},
		{
			name:        "missing query file",
			directory:   "missing-query-file",
			expectedOut: "",
			expectedErr: []string{`
Error: No resources to query

The configuration does not contain any resources that can be queried.
`},
		},
		{
			name:        "missing configuration",
			directory:   "missing-configuration",
			expectedOut: "",
			expectedErr: []string{`
Error: No configuration files

Query requires a query configuration to be present. Create a Terraform query
configuration file (.tfquery.hcl file) and try again.
`},
		},
		{
			name:        "invalid query syntax",
			directory:   "invalid-syntax",
			expectedOut: "",
			initCode:    0,
			expectedErr: []string{`
Error: Unsupported block type

  on query.tfquery.hcl line 11:
  11: resource "test_instance" "example" {

Blocks of type "resource" are not expected here.
`},
		},
		{
			name:      "empty result",
			directory: "empty-result",
			expectedOut: `list.test_instance.example   id=test-instance-1   Test Instance 1
list.test_instance.example   id=test-instance-2   Test Instance 2

Warning: list block(s) [list.test_instance.example2] returned 0 results.`,
		},
		{
			name:      "error - extra variables",
			directory: "basic",
			args:      []string{"-var", "instance_name=test-instance"},
			expectedErr: []string{`
Error: Value for undeclared variable

A variable named "instance_name" was assigned on the command line, but the
root module does not declare a variable of that name. To use this value, add
a "variable" block to the configuration.
				`},
		},
		{
			name:      "query with variables",
			directory: "with-variables",
			args:      []string{"-var", "instance_name=test-instance"},
			expectedOut: `list.test_instance.example   id=test-instance-1   Test Instance 1
list.test_instance.example   id=test-instance-2   Test Instance 2

`,
		},
		{
			name:      "query with variables defined in tf file",
			directory: "with-variables-in-tf",
			args:      []string{"-var", "instance_name=test-instance"},
			expectedOut: `list.test_instance.example   id=test-instance-1   Test Instance 1
list.test_instance.example   id=test-instance-2   Test Instance 2

`,
		},
		{
			name:      "query with variable files",
			directory: "with-variables-file",
			args:      []string{"-var-file=custom.tfvars"},
			expectedOut: `list.test_instance.example   id=test-instance-1   Test Instance 1
list.test_instance.example   id=test-instance-2   Test Instance 2

`,
		},
		{
			name:        "error - query with invalid variable value",
			directory:   "with-invalid-variables",
			args:        []string{"-var", "target_ami=ami-123"},
			expectedErr: []string{`AMI ID must be longer than 10 characters.`},
		},
		{
			name:        "error - query with missing required variable",
			directory:   "with-variables",
			expectedOut: "",
			expectedErr: []string{`
Error: No value for required variable

  on query.tfquery.hcl line 7:
   7: variable "instance_name" {

The root module input variable "instance_name" is not set, and has no default
value. Use a -var or -var-file command line argument to provide a value for
this variable.
`},
		},
		{
			name:        "error - query with missing required variable in tf file",
			directory:   "with-variables-in-tf",
			expectedOut: "",
			expectedErr: []string{`
Error: No value for required variable

  on main.tf line 15:
  15: variable "instance_name" {

The root module input variable "instance_name" is not set, and has no default
value. Use a -var or -var-file command line argument to provide a value for
this variable.
`},
		},
	}

	for _, ts := range tests {
		t.Run(ts.name, func(t *testing.T) {
			td := t.TempDir()
			testCopyDir(t, testFixturePath(path.Join("query", ts.directory)), td)
			t.Chdir(td)
			providerSource, close := newMockProviderSource(t, map[string][]string{
				"hashicorp/test": {"1.0.0"},
			})
			defer close()

			p := queryFixtureProvider()
			view, done := testView(t)
			meta := Meta{
				testingOverrides:          metaOverridesForProvider(p),
				View:                      view,
				AllowExperimentalFeatures: true,
				ProviderSource:            providerSource,
			}

			// helper for asserting against the expected error(s)
			assertErr := func(actual string) (errored bool) {
				for _, expected := range ts.expectedErr {
					expected := strings.TrimSpace(expected)
					if !strings.Contains(actual, expected) {
						errored = true
						t.Errorf("expected error message to contain '%s', \ngot: %s: diff: %s", expected, actual, cmp.Diff(expected, actual))
					}
				}
				return
			}

			init := &InitCommand{Meta: meta}
			code := init.Run(nil)
			output := done(t)
			if code != ts.initCode {
				t.Fatalf("expected status code %d but got %d: %s", ts.initCode, code, output.All())
			}

			// If we expect an init error, perhaps we want to assert the error message
			if ts.initCode != 0 {
				actual := output.All()
				if errored := assertErr(actual); errored {
					t.FailNow()
					return
				}
			}

			view, done = testView(t)
			meta.View = view

			c := &QueryCommand{Meta: meta}
			code = c.Run(append([]string{"-no-color"}, ts.args...))
			output = done(t)
			if len(ts.expectedErr) == 0 {
				if code != 0 {
					t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
				}
				actual := strings.TrimSpace(output.Stdout())

				// Check that we have query output
				expected := strings.TrimSpace(ts.expectedOut)
				if diff := cmp.Diff(expected, actual); diff != "" {
					t.Errorf("expected query output to contain \n%q, \ngot: \n%q, \ndiff: %s", expected, actual, diff)
				}

			} else {
				actual := strings.TrimSpace(output.Stderr())
				assertErr(actual)
			}
		})
	}
}

func queryFixtureProvider() *testing_provider.MockProvider {
	p := testProvider()
	instanceListSchema := &configschema.Block{
		Attributes: map[string]*configschema.Attribute{
			"data": {
				Type:     cty.DynamicPseudoType,
				Computed: true,
			},
		},
		BlockTypes: map[string]*configschema.NestedBlock{
			"config": {
				Block: configschema.Block{
					Attributes: map[string]*configschema.Attribute{
						"ami": {
							Type:     cty.String,
							Required: true,
						},
						"foo": {
							Type:     cty.String,
							Optional: true,
						},
					},
				},
				Nesting:  configschema.NestingSingle,
				MinItems: 1,
				MaxItems: 1,
			},
		},
	}
	databaseListSchema := &configschema.Block{
		Attributes: map[string]*configschema.Attribute{
			"data": {
				Type:     cty.DynamicPseudoType,
				Computed: true,
			},
		},
		BlockTypes: map[string]*configschema.NestedBlock{
			"config": {
				Block: configschema.Block{
					Attributes: map[string]*configschema.Attribute{
						"engine": {
							Type:     cty.String,
							Optional: true,
						},
					},
				},
				Nesting:  configschema.NestingSingle,
				MinItems: 1,
				MaxItems: 1,
			},
		},
	}
	p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
		ResourceTypes: map[string]providers.Schema{
			"test_instance": {
				Body: &configschema.Block{
					Attributes: map[string]*configschema.Attribute{
						"id": {
							Type:     cty.String,
							Computed: true,
						},
						"ami": {
							Type:     cty.String,
							Optional: true,
						},
					},
				},
				Identity: &configschema.Object{
					Attributes: map[string]*configschema.Attribute{
						"id": {
							Type:     cty.String,
							Computed: true,
						},
					},
					Nesting: configschema.NestingSingle,
				},
				IdentityVersion: 1,
			},
			"test_database": {
				Body: &configschema.Block{
					Attributes: map[string]*configschema.Attribute{
						"id": {
							Type:     cty.String,
							Computed: true,
						},
						"engine": {
							Type:     cty.String,
							Optional: true,
						},
					},
				},
				Identity: &configschema.Object{
					Attributes: map[string]*configschema.Attribute{
						"id": {
							Type:     cty.String,
							Computed: true,
						},
					},
					Nesting: configschema.NestingSingle,
				},
			},
		},
		ListResourceTypes: map[string]providers.Schema{
			"test_instance": {Body: instanceListSchema},
			"test_database": {Body: databaseListSchema},
		},
	}

	// Mock the ListResources method for query operations
	p.ListResourceFn = func(request providers.ListResourceRequest) providers.ListResourceResponse {
		// Check the config to determine what kind of response to return
		wholeConfigMap := request.Config.AsValueMap()

		configMap := wholeConfigMap["config"]

		// For empty results test case
		ami, ok := configMap.AsValueMap()["ami"]
		if ok && ami.AsString() == "ami-nonexistent" {
			return providers.ListResourceResponse{
				Result: cty.ObjectVal(map[string]cty.Value{
					"data":   cty.ListValEmpty(cty.DynamicPseudoType),
					"config": configMap,
				}),
			}
		}

		switch request.TypeName {
		case "test_instance":
			return providers.ListResourceResponse{
				Result: cty.ObjectVal(map[string]cty.Value{
					"data": cty.ListVal([]cty.Value{
						cty.ObjectVal(map[string]cty.Value{
							"identity": cty.ObjectVal(map[string]cty.Value{
								"id": cty.StringVal("test-instance-1"),
							}),
							"state": cty.ObjectVal(map[string]cty.Value{
								"id":  cty.StringVal("test-instance-1"),
								"ami": cty.StringVal("ami-12345"),
							}),
							"display_name": cty.StringVal("Test Instance 1"),
						}),
						cty.ObjectVal(map[string]cty.Value{
							"identity": cty.ObjectVal(map[string]cty.Value{
								"id": cty.StringVal("test-instance-2"),
							}),
							"state": cty.ObjectVal(map[string]cty.Value{
								"id":  cty.StringVal("test-instance-2"),
								"ami": cty.StringVal("ami-67890"),
							}),
							"display_name": cty.StringVal("Test Instance 2"),
						}),
					}),
					"config": configMap,
				}),
			}
		case "test_database":
			return providers.ListResourceResponse{
				Result: cty.ObjectVal(map[string]cty.Value{
					"data": cty.ListVal([]cty.Value{
						cty.ObjectVal(map[string]cty.Value{
							"identity": cty.ObjectVal(map[string]cty.Value{
								"id": cty.StringVal("test-db-1"),
							}),
							"state": cty.ObjectVal(map[string]cty.Value{
								"id":     cty.StringVal("test-db-1"),
								"engine": cty.StringVal("mysql"),
							}),
							"display_name": cty.StringVal("Test Database 1"),
						}),
					}),
					"config": configMap,
				}),
			}
		default:
			return providers.ListResourceResponse{
				Result: cty.ObjectVal(map[string]cty.Value{
					"data":   cty.ListVal([]cty.Value{}),
					"config": configMap,
				}),
			}
		}
	}

	return p
}

func TestQuery_JSON(t *testing.T) {
	tmp := t.TempDir()
	tests := []struct {
		name           string
		directory      string
		expectedRes    []map[string]any
		initCode       int
		opts           []string
		commandErrMsg  string   // non-empty when the query command fails after init
		selectResource []string // only these resources will be selected in the result if given
	}{
		{
			name:      "basic query",
			directory: "basic",
			expectedRes: []map[string]any{
				{
					"@level":   "info",
					"@message": "list.test_instance.example: Starting query...",
					"list_start": map[string]any{
						"address":       "list.test_instance.example",
						"resource_type": "test_instance",
						"input_config":  map[string]any{"ami": "ami-12345", "foo": nil},
					},
					"type": "list_start",
				},
				{
					"@level":   "info",
					"@message": "list.test_instance.example: Result found",
					"list_resource_found": map[string]any{
						"address":      "list.test_instance.example",
						"display_name": "Test Instance 1",
						"identity": map[string]any{
							"id": "test-instance-1",
						},
						"identity_version": float64(1),
						"resource_type":    "test_instance",
						"resource_object": map[string]any{
							"ami": "ami-12345",
							"id":  "test-instance-1",
						},
					},
					"type": "list_resource_found",
				},
				{
					"@level":   "info",
					"@message": "list.test_instance.example: Result found",
					"list_resource_found": map[string]any{
						"address":      "list.test_instance.example",
						"display_name": "Test Instance 2",
						"identity": map[string]any{
							"id": "test-instance-2",
						},
						"identity_version": float64(1),
						"resource_type":    "test_instance",
						"resource_object": map[string]any{
							"ami": "ami-67890",
							"id":  "test-instance-2",
						},
					},
					"type": "list_resource_found",
				},
				{
					"@level":   "info",
					"@message": "list.test_instance.example: List complete",
					"list_complete": map[string]any{
						"address":       "list.test_instance.example",
						"resource_type": "test_instance",
						"total":         float64(2),
					},
					"type": "list_complete",
				},
			},
		},
		{
			name:      "basic query - generate config",
			directory: "basic",
			opts:      []string{fmt.Sprintf("-generate-config-out=%s/new.tf", tmp)},
			expectedRes: []map[string]any{
				{
					"@level":   "info",
					"@message": "list.test_instance.example: Starting query...",
					"list_start": map[string]any{
						"address":       "list.test_instance.example",
						"resource_type": "test_instance",
						"input_config":  map[string]any{"ami": "ami-12345", "foo": nil},
					},
					"type": "list_start",
				},
				{"@level": "info",
					"@message": "list.test_instance.example: Result found",
					"list_resource_found": map[string]any{
						"address":      "list.test_instance.example",
						"display_name": "Test Instance 1",
						"identity": map[string]any{
							"id": "test-instance-1",
						},
						"identity_version": float64(1),
						"resource_type":    "test_instance",
						"resource_object": map[string]any{
							"ami": "ami-12345",
							"id":  "test-instance-1",
						},
						"config":        "resource \"test_instance\" \"example_0\" {\n  provider = test\n  ami      = \"ami-12345\"\n}",
						"import_config": "import {\n  to       = test_instance.example_0\n  provider = test\n  identity = {\n    id = \"test-instance-1\"\n  }\n}",
					},
					"type": "list_resource_found",
				},
				{
					"@level":   "info",
					"@message": "list.test_instance.example: Result found",
					"list_resource_found": map[string]any{
						"address":      "list.test_instance.example",
						"display_name": "Test Instance 2",
						"identity": map[string]any{
							"id": "test-instance-2",
						},
						"identity_version": float64(1),
						"resource_type":    "test_instance",
						"resource_object": map[string]any{
							"ami": "ami-67890",
							"id":  "test-instance-2",
						},
						"config":        "resource \"test_instance\" \"example_1\" {\n  provider = test\n  ami      = \"ami-67890\"\n}",
						"import_config": "import {\n  to       = test_instance.example_1\n  provider = test\n  identity = {\n    id = \"test-instance-2\"\n  }\n}",
					},
					"type": "list_resource_found",
				},
				{
					"@level":   "info",
					"@message": "list.test_instance.example: List complete",
					"list_complete": map[string]any{
						"address":       "list.test_instance.example",
						"resource_type": "test_instance",
						"total":         float64(2),
					},
					"type": "list_complete",
				},
			},
		},
		{
			name:      "list resource with an empty result",
			directory: "empty-result",
			expectedRes: []map[string]any{
				{
					"@level":   "info",
					"@message": "list.test_instance.example2: List complete",
					"list_complete": map[string]any{
						"address":       "list.test_instance.example2",
						"resource_type": "test_instance",
						"total":         float64(0),
					},
					"type": "list_complete",
				},
			},
			selectResource: []string{"list.test_instance.example2"},
		},
		{
			name:      "error - list resource with an unknown config",
			directory: "unknown-config",
			expectedRes: []map[string]any{
				{
					"@level":   "error",
					"@message": "Error: config is not known",
					"diagnostic": map[string]any{
						"severity": "error",
						"summary":  "config is not known",
						"detail":   "",
					},
					"type": "diagnostic",
				},
			},
		},
		{
			name:      "error - generate-config-path already exists",
			directory: "basic",
			opts:      []string{fmt.Sprintf("-generate-config-out=%s", t.TempDir())},
			expectedRes: []map[string]any{
				{
					"@level":   "error",
					"@message": "Error: Target generated file already exists",
					"diagnostic": map[string]any{
						"detail":   "Terraform can only write generated config into a new file. Either choose a different target location or move all existing configuration out of the target file, delete it and try again.",
						"severity": "error",
						"summary":  "Target generated file already exists",
					},
					"type": "diagnostic",
				},
			},
		},
		{
			name:      "success with variables",
			directory: "with-variables",
			opts:      []string{"-var", "instance_name=test-instance"},
			expectedRes: []map[string]any{
				{
					"@level":   "info",
					"@message": "list.test_instance.example: Starting query...",
					"list_start": map[string]any{
						"address":       "list.test_instance.example",
						"resource_type": "test_instance",
						"input_config":  map[string]any{"ami": "ami-12345", "foo": "test-instance"},
					},
					"type": "list_start",
				},
				{
					"@level":   "info",
					"@message": "list.test_instance.example: Result found",
					"list_resource_found": map[string]any{
						"address":      "list.test_instance.example",
						"display_name": "Test Instance 1",
						"identity": map[string]any{
							"id": "test-instance-1",
						},
						"identity_version": float64(1),
						"resource_type":    "test_instance",
						"resource_object": map[string]any{
							"ami": "ami-12345",
							"id":  "test-instance-1",
						},
					},
					"type": "list_resource_found",
				},
				{
					"@level":   "info",
					"@message": "list.test_instance.example: Result found",
					"list_resource_found": map[string]any{
						"address":      "list.test_instance.example",
						"display_name": "Test Instance 2",
						"identity": map[string]any{
							"id": "test-instance-2",
						},
						"identity_version": float64(1),
						"resource_type":    "test_instance",
						"resource_object": map[string]any{
							"ami": "ami-67890",
							"id":  "test-instance-2",
						},
					},
					"type": "list_resource_found",
				},
				{
					"@level":   "info",
					"@message": "list.test_instance.example: List complete",
					"list_complete": map[string]any{
						"address":       "list.test_instance.example",
						"resource_type": "test_instance",
						"total":         float64(2),
					},
					"type": "list_complete",
				},
			},
		},
		{
			name:      "success with sensitive variables",
			directory: "with-sensitive-variables",
			opts:      []string{"-var", "sensitive_foo=test-instance"},
			expectedRes: []map[string]any{
				{
					"@level":   "info",
					"@message": "list.test_instance.example: Starting query...",
					"list_start": map[string]any{
						"address":                   "list.test_instance.example",
						"resource_type":             "test_instance",
						"input_config":              map[string]any{"ami": "ami-12345", "foo": "test-instance"},
						"sensitive_attribute_paths": []any{string(".foo")},
					},
					"type": "list_start",
				},
				{
					"@level":   "info",
					"@message": "list.test_instance.example: Result found",
					"list_resource_found": map[string]any{
						"address":      "list.test_instance.example",
						"display_name": "Test Instance 1",
						"identity": map[string]any{
							"id": "test-instance-1",
						},
						"identity_version": float64(1),
						"resource_type":    "test_instance",
						"resource_object": map[string]any{
							"ami": "ami-12345",
							"id":  "test-instance-1",
						},
					},
					"type": "list_resource_found",
				},
				{
					"@level":   "info",
					"@message": "list.test_instance.example: Result found",
					"list_resource_found": map[string]any{
						"address":      "list.test_instance.example",
						"display_name": "Test Instance 2",
						"identity": map[string]any{
							"id": "test-instance-2",
						},
						"identity_version": float64(1),
						"resource_type":    "test_instance",
						"resource_object": map[string]any{
							"ami": "ami-67890",
							"id":  "test-instance-2",
						},
					},
					"type": "list_resource_found",
				},
				{
					"@level":   "info",
					"@message": "list.test_instance.example: List complete",
					"list_complete": map[string]any{
						"address":       "list.test_instance.example",
						"resource_type": "test_instance",
						"total":         float64(2),
					},
					"type": "list_complete",
				},
			},
		},
	}

	for _, ts := range tests {
		t.Run(ts.name, func(t *testing.T) {
			td := t.TempDir()
			testCopyDir(t, testFixturePath(path.Join("query", ts.directory)), td)
			t.Chdir(td)
			providerSource, close := newMockProviderSource(t, map[string][]string{
				"hashicorp/test": {"1.0.0"},
			})
			defer close()

			p := queryFixtureProvider()
			view, done := testView(t)
			meta := Meta{
				testingOverrides:          metaOverridesForProvider(p),
				View:                      view,
				AllowExperimentalFeatures: true,
				ProviderSource:            providerSource,
			}

			init := &InitCommand{Meta: meta}
			code := init.Run(nil)
			output := done(t)
			if code != ts.initCode {
				t.Fatalf("expected status code %d but got %d: %s", ts.initCode, code, output.All())
			}

			view, done = testView(t)
			meta.View = view

			c := &QueryCommand{Meta: meta}
			args := []string{"-no-color", "-json"}
			code = c.Run(append(args, ts.opts...))
			output = done(t)
			if code != 0 {
				t.Logf("query command returned non-zero code '%d' and an error: \n\n%s", code, output.All())
			}

			// convert output to JSON array
			actual := strings.TrimSpace(output.Stdout())
			conc := fmt.Sprintf("[%s]", strings.Join(strings.Split(actual, "\n"), ","))
			rawRes := make([]map[string]any, 0)
			err := json.NewDecoder(strings.NewReader(conc)).Decode(&rawRes)
			if err != nil {
				t.Fatalf("failed to unmarshal: %s", err)
			}

			// remove unnecessary fields before comparison
			actualRes := make([]map[string]any, 0, len(rawRes))
			for _, item := range rawRes {
				delete(item, "@module")
				delete(item, "@timestamp")
				delete(item, "ui")

				// Clean up diagnostic fields that we don't want to compare
				if diagnostic, ok := item["diagnostic"].(map[string]any); ok {
					delete(diagnostic, "range")
					delete(diagnostic, "snippet")
				}

				// if we have a select list of resource addresses, we only check those addresses
				if len(ts.selectResource) > 0 {
					for _, addr := range ts.selectResource {
						if strings.Contains(item["@message"].(string), addr) {
							actualRes = append(actualRes, item)
						}
					}
				} else {
					actualRes = append(actualRes, item)
				}
			}

			// remove the version entry. Not relevant for testing
			actualRes = slices.Delete(actualRes, 0, 1)

			if diff := cmp.Diff(ts.expectedRes, actualRes); diff != "" {
				// Check that the output matches the expected results
				t.Errorf("expected query output to contain \n%q, \ngot: \n%q, \ndiff: %s", ts.expectedRes, actualRes, diff)
			}
		})
	}
}

func TestQuery_JSON_Raw(t *testing.T) {

	tfVer := tfversion.String()
	tests := []struct {
		name        string
		directory   string
		expectedOut string
		expectedErr []string
		initCode    int
		args        []string
	}{
		{
			name:      "basic query",
			directory: "basic",
			expectedOut: `{"@level":"info","@message":"Terraform ` + tfVer + `","@module":"terraform.ui","@timestamp":"2025-09-12T16:52:57.596469+02:00","terraform":"1.14.0-dev","type":"version","ui":"` + views.JSON_UI_VERSION + `"}
{"@level":"info","@message":"list.test_instance.example: Starting query...","@module":"terraform.ui","@timestamp":"2025-09-12T16:52:57.600609+02:00","list_start":{"address":"list.test_instance.example","resource_type":"test_instance","input_config":{"ami":"ami-12345","foo":null}},"type":"list_start"}
{"@level":"info","@message":"list.test_instance.example: Result found","@module":"terraform.ui","@timestamp":"2025-09-12T16:52:57.600729+02:00","list_resource_found":{"address":"list.test_instance.example","display_name":"Test Instance 1","identity":{"id":"test-instance-1"},"identity_version":1,"resource_type":"test_instance","resource_object":{"ami":"ami-12345","id":"test-instance-1"}},"type":"list_resource_found"}
{"@level":"info","@message":"list.test_instance.example: Result found","@module":"terraform.ui","@timestamp":"2025-09-12T16:52:57.600759+02:00","list_resource_found":{"address":"list.test_instance.example","display_name":"Test Instance 2","identity":{"id":"test-instance-2"},"identity_version":1,"resource_type":"test_instance","resource_object":{"ami":"ami-67890","id":"test-instance-2"}},"type":"list_resource_found"}
{"@level":"info","@message":"list.test_instance.example: List complete","@module":"terraform.ui","@timestamp":"2025-09-12T16:52:57.600770+02:00","list_complete":{"address":"list.test_instance.example","resource_type":"test_instance","total":2},"type":"list_complete"}
`,
		},
		{
			name:      "empty result",
			directory: "empty-result",
			expectedOut: `{"@level":"info","@message":"Terraform ` + tfVer + `","@module":"terraform.ui","@timestamp":"2025-09-12T16:52:57.596469+02:00","terraform":"1.14.0-dev","type":"version","ui":"` + views.JSON_UI_VERSION + `"}
{"@level":"info","@message":"list.test_instance.example: Starting query...","@module":"terraform.ui","@timestamp":"2025-09-12T16:52:57.600609+02:00","list_start":{"address":"list.test_instance.example","resource_type":"test_instance","input_config":{"ami":"ami-12345","foo":null}},"type":"list_start"}
{"@level":"info","@message":"list.test_instance.example: Result found","@module":"terraform.ui","@timestamp":"2025-09-12T16:52:57.600729+02:00","list_resource_found":{"address":"list.test_instance.example","display_name":"Test Instance 1","identity":{"id":"test-instance-1"},"identity_version":1,"resource_type":"test_instance","resource_object":{"ami":"ami-12345","id":"test-instance-1"}},"type":"list_resource_found"}
{"@level":"info","@message":"list.test_instance.example: Result found","@module":"terraform.ui","@timestamp":"2025-09-12T16:52:57.600759+02:00","list_resource_found":{"address":"list.test_instance.example","display_name":"Test Instance 2","identity":{"id":"test-instance-2"},"identity_version":1,"resource_type":"test_instance","resource_object":{"ami":"ami-67890","id":"test-instance-2"}},"type":"list_resource_found"}
{"@level":"info","@message":"list.test_instance.example: List complete","@module":"terraform.ui","@timestamp":"2025-09-12T16:52:57.600770+02:00","list_complete":{"address":"list.test_instance.example","resource_type":"test_instance","total":2},"type":"list_complete"}
{"@level":"info","@message":"list.test_instance.example2: Starting query...","@module":"terraform.ui","@timestamp":"2025-09-12T16:52:57.600609+02:00","list_start":{"address":"list.test_instance.example2","resource_type":"test_instance","input_config":{"ami":"ami-nonexistent","foo":"test-instance-1"}},"type":"list_start"}
{"@level":"info","@message":"list.test_instance.example2: List complete","@module":"terraform.ui","@timestamp":"2025-09-12T16:52:57.600770+02:00","list_complete":{"address":"list.test_instance.example2","resource_type":"test_instance","total":0},"type":"list_complete"}
`,
		},
	}

	for _, ts := range tests {
		t.Run(ts.name, func(t *testing.T) {
			td := t.TempDir()
			testCopyDir(t, testFixturePath(path.Join("query", ts.directory)), td)
			t.Chdir(td)
			providerSource, close := newMockProviderSource(t, map[string][]string{
				"hashicorp/test": {"1.0.0"},
			})
			defer close()

			p := queryFixtureProvider()
			view, done := testView(t)
			meta := Meta{
				testingOverrides:          metaOverridesForProvider(p),
				View:                      view,
				AllowExperimentalFeatures: true,
				ProviderSource:            providerSource,
			}

			init := &InitCommand{Meta: meta}
			code := init.Run(nil)
			output := done(t)
			if code != 0 {
				t.Fatalf("expected status code %d but got %d: %s", 0, code, output.All())
			}

			view, done = testView(t)
			meta.View = view

			c := &QueryCommand{Meta: meta}
			args := []string{"-no-color", "-json"}
			code = c.Run(args)
			output = done(t)
			if code != 0 {
				t.Logf("query command returned non-zero code '%d' and an error: \n\n%s", code, output.All())
			}

			// Use regex to normalize timestamps and version numbers for comparison
			timestampRegex := regexp.MustCompile(`"@timestamp":"[^"]*"`)
			versionRegex := regexp.MustCompile(`"terraform":"[^"]*"`)

			actualOutput := output.Stdout()
			expectedOutput := ts.expectedOut

			// Replace timestamps and version numbers with placeholders
			actualNormalized := timestampRegex.ReplaceAllString(actualOutput, `"@timestamp":"TIMESTAMP"`)
			actualNormalized = versionRegex.ReplaceAllString(actualNormalized, `"terraform":"VERSION"`)

			expectedNormalized := timestampRegex.ReplaceAllString(expectedOutput, `"@timestamp":"TIMESTAMP"`)
			expectedNormalized = versionRegex.ReplaceAllString(expectedNormalized, `"terraform":"VERSION"`)
			if diff := cmp.Diff(expectedNormalized, actualNormalized); diff != "" {
				t.Errorf("expected query output to match, diff: %s", diff)
			}
		})
	}
}
