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

package cloud

import (
	"context"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"github.com/google/go-cmp/cmp"
	"github.com/hashicorp/cli"
	"github.com/hashicorp/terraform/internal/addrs"
	"github.com/hashicorp/terraform/internal/backend/backendrun"
	"github.com/hashicorp/terraform/internal/command/arguments"
	"github.com/hashicorp/terraform/internal/command/clistate"
	"github.com/hashicorp/terraform/internal/command/jsonformat"
	"github.com/hashicorp/terraform/internal/command/views"
	"github.com/hashicorp/terraform/internal/depsfile"
	"github.com/hashicorp/terraform/internal/states/statemgr"
	"github.com/hashicorp/terraform/internal/terminal"
	tftesting "github.com/hashicorp/terraform/internal/terraform/testing"
)

func testOperationQuery(t *testing.T, configDir string) (*backendrun.Operation, func(), func(*testing.T) *terminal.TestOutput) {
	t.Helper()

	return testOperationQueryWithTimeout(t, configDir, 0)
}

func testOperationQueryWithTimeout(t *testing.T, configDir string, timeout time.Duration) (*backendrun.Operation, func(), func(*testing.T) *terminal.TestOutput) {
	t.Helper()

	_, configLoader, configCleanup := tftesting.MustLoadConfigForTests(t, configDir, "tests")

	streams, done := terminal.StreamsForTesting(t)
	view := views.NewView(streams)
	stateLockerView := views.NewStateLocker(arguments.ViewHuman, view)
	operationView := views.NewQueryOperation(arguments.ViewHuman, false, view)

	// Many of our tests use an overridden "null" provider that's just in-memory
	// inside the test process, not a separate plugin on disk.
	depLocks := depsfile.NewLocks()
	depLocks.SetProviderOverridden(addrs.MustParseProviderSourceString("registry.terraform.io/hashicorp/null"))

	return &backendrun.Operation{
		ConfigDir:       configDir,
		ConfigLoader:    configLoader,
		StateLocker:     clistate.NewLocker(timeout, stateLockerView),
		Type:            backendrun.OperationTypePlan,
		View:            operationView,
		DependencyLocks: depLocks,
		Query:           true,
	}, configCleanup, done
}

func TestCloud_queryBasic(t *testing.T) {
	b, bCleanup := testBackendWithName(t)
	defer bCleanup()

	op, configCleanup, done := testOperationQuery(t, "./testdata/query")
	defer configCleanup()
	defer done(t)

	op.Workspace = testBackendSingleWorkspaceName

	run, err := b.Operation(context.Background(), op)
	if err != nil {
		t.Fatalf("error starting operation: %v", err)
	}

	<-run.Done()
	if run.Result != backendrun.OperationSuccess {
		t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
	}

	output := b.CLI.(*cli.MockUi).OutputWriter.String()
	if !strings.Contains(output, "Running query in HCP Terraform") {
		t.Fatalf("expected HCP Terraform header in output: %s", output)
	}
	if !strings.Contains(output, "list.concept_pet.pets   id=") {
		t.Fatalf("expected query results in output: %s", output)
	}

	stateMgr, _ := b.StateMgr(testBackendSingleWorkspaceName)
	// An error suggests that the state was not unlocked after the operation finished
	if _, err := stateMgr.Lock(statemgr.NewLockInfo()); err != nil {
		t.Fatalf("unexpected error locking state after successful plan: %s", err.Error())
	}
}

func TestCloud_queryJSONBasic(t *testing.T) {
	b, bCleanup := testBackendWithName(t)
	defer bCleanup()

	stream, close := terminal.StreamsForTesting(t)

	b.renderer = &jsonformat.Renderer{
		Streams:  stream,
		Colorize: mockColorize(),
	}

	op, configCleanup, done := testOperationQuery(t, "./testdata/query-json-basic")
	defer configCleanup()
	defer done(t)

	op.Workspace = testBackendSingleWorkspaceName

	mockSROWorkspace(t, b, op.Workspace)

	run, err := b.Operation(context.Background(), op)
	if err != nil {
		t.Fatalf("error starting operation: %v", err)
	}

	<-run.Done()
	if run.Result != backendrun.OperationSuccess {
		t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
	}

	outp := close(t)
	gotOut := outp.Stdout()

	expectedOut := `list.concept_pet.pets   id=large-roughy,legs=2      This is a large-roughy
list.concept_pet.pets   id=able-werewolf,legs=5     This is a able-werewolf
list.concept_pet.pets   id=complete-gannet,legs=6   This is a complete-gannet
list.concept_pet.pets   id=charming-beagle,legs=3   This is a charming-beagle
list.concept_pet.pets   id=legal-lamprey,legs=2     This is a legal-lamprey

`
	if diff := cmp.Diff(expectedOut, gotOut); diff != "" {
		t.Fatalf("expected query results output to be %s, got %s: diff: %s", expectedOut, gotOut, diff)
	}

	stateMgr, _ := b.StateMgr(testBackendSingleWorkspaceName)
	// An error suggests that the state was not unlocked after the operation finished
	if _, err := stateMgr.Lock(statemgr.NewLockInfo()); err != nil {
		t.Fatalf("unexpected error locking state after successful plan: %s", err.Error())
	}
}

func TestCloud_queryJSONWithDiags(t *testing.T) {
	b, bCleanup := testBackendWithName(t)
	defer bCleanup()

	stream, close := terminal.StreamsForTesting(t)

	b.renderer = &jsonformat.Renderer{
		Streams:  stream,
		Colorize: mockColorize(),
	}

	op, configCleanup, done := testOperationQuery(t, "./testdata/query-json-diag")
	defer configCleanup()
	defer done(t)

	op.Workspace = testBackendSingleWorkspaceName

	mockSROWorkspace(t, b, op.Workspace)

	run, err := b.Operation(context.Background(), op)
	if err != nil {
		t.Fatalf("error starting operation: %v", err)
	}

	<-run.Done()
	if run.Result != backendrun.OperationSuccess {
		t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
	}

	testOut := close(t)
	output := testOut.Stdout()

	// Warning diagnostic message
	testString := "Warning: Something went wrong"
	if !strings.Contains(output, testString) {
		t.Fatalf("Expected %q to contain %q but it did not", output, testString)
	}
}

func TestCloud_queryGenerateConfigOut(t *testing.T) {
	tests := []struct {
		name          string
		setConfigOut  bool
		wantGenConfig bool
	}{
		{
			name:          "GenerateConfigOut set",
			setConfigOut:  true,
			wantGenConfig: true,
		},
		{
			name:          "GenerateConfigOut not set",
			setConfigOut:  false,
			wantGenConfig: false,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			b, mc, bCleanup := testBackendAndMocksWithName(t)
			defer bCleanup()

			op, configCleanup, done := testOperationQuery(t, "./testdata/query")
			defer configCleanup()
			defer done(t)

			if tc.setConfigOut {
				// ValidateTargetFile errors if the file already exists, so
				// use a path inside a temp dir that does not yet exist.
				op.GenerateConfigOut = filepath.Join(t.TempDir(), "generated.tf")
			}

			op.Workspace = testBackendSingleWorkspaceName

			run, err := b.Operation(context.Background(), op)
			if err != nil {
				t.Fatalf("error starting operation: %v", err)
			}

			<-run.Done()
			if run.Result != backendrun.OperationSuccess {
				t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
			}

			got := mc.QueryRuns.CreateOptions.GenerateConfigOut
			if got == nil {
				t.Fatal("expected GenerateConfigOut to be non-nil in QueryRunCreateOptions")
			}
			if *got != tc.wantGenConfig {
				t.Errorf("GenerateConfigOut: got %v, want %v", *got, tc.wantGenConfig)
			}
		})
	}
}
