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

package command

import (
	"context"
	"strings"
	"time"

	backendInit "github.com/hashicorp/terraform/internal/backend/init"
	"github.com/hashicorp/terraform/internal/backend/local"
	"github.com/hashicorp/terraform/internal/logging"
	"github.com/hashicorp/terraform/internal/moduletest"
	"github.com/hashicorp/terraform/internal/tfdiags"
)

// TestCleanupCommand is a command that cleans up left-over resources created
// during Terraform test runs. It basically runs the test command in cleanup mode.
type TestCleanupCommand struct {
	Meta
}

func (c *TestCleanupCommand) Help() string {
	helpText := `
Usage: terraform [global options] test cleanup [options]

  Cleans up left-over resources in states that were created during Terraform test runs.

  By default, this command ignores the skip_cleanup attributes in the manifest
  file. Use the -repair flag to override this behavior, which will ensure that
  resources that were intentionally left-over are exempt from cleanup.

Options:

  -repair               Overrides the skip_cleanup attribute in the manifest
                        file and attempts to clean up all resources.

  -no-color             If specified, output won't contain any color.

  -verbose              Print detailed output during the cleanup process.
`
	return strings.TrimSpace(helpText)
}

func (c *TestCleanupCommand) Synopsis() string {
	return "Clean up left-over resources created during Terraform test runs"
}

func (c *TestCleanupCommand) Run(rawArgs []string) int {
	setup, diags := c.setupTestExecution(moduletest.CleanupMode, "test cleanup", rawArgs)
	if diags.HasErrors() {
		return 1
	}

	args := setup.Args
	view := setup.View
	config := setup.Config
	variables := setup.Variables
	testVariables := setup.TestVariables
	opts := setup.Opts

	// We have two levels of interrupt here. A 'stop' and a 'cancel'. A 'stop'
	// is a soft request to stop. We'll finish the current test, do the tidy up,
	// but then skip all remaining tests and run blocks. A 'cancel' is a hard
	// request to stop now. We'll cancel the current operation immediately
	// even if it's a delete operation, and we won't clean up any infrastructure
	// if we're halfway through a test. We'll print details explaining what was
	// stopped so the user can do their best to recover from it.

	runningCtx, done := context.WithCancel(context.Background())
	stopCtx, stop := context.WithCancel(runningCtx)
	cancelCtx, cancel := context.WithCancel(context.Background())

	runner := &local.TestSuiteRunner{
		BackendFactory: backendInit.Backend,
		Config:         config,
		// The GlobalVariables are loaded from the
		// main configuration directory
		// The GlobalTestVariables are loaded from the
		// test directory
		GlobalVariables:     variables,
		GlobalTestVariables: testVariables,
		TestingDirectory:    args.TestDirectory,
		Opts:                opts,
		View:                view,
		Stopped:             false,
		Cancelled:           false,
		StoppedCtx:          stopCtx,
		CancelledCtx:        cancelCtx,
		Filter:              args.Filter,
		Verbose:             args.Verbose,
		Repair:              args.Repair,
		CommandMode:         moduletest.CleanupMode,
	}

	var testDiags tfdiags.Diagnostics

	go func() {
		defer logging.PanicHandler()
		defer done()
		defer stop()
		defer cancel()

		_, testDiags = runner.Test(c.Meta.AllowExperimentalFeatures)
	}()

	// Wait for the operation to complete, or for an interrupt to occur.
	select {
	case <-c.ShutdownCh:
		// Nice request to be cancelled.

		view.Interrupted()
		runner.Stop()
		stop()

		select {
		case <-c.ShutdownCh:
			// The user pressed it again, now we have to get it to stop as
			// fast as possible.

			view.FatalInterrupt()
			runner.Cancel()
			cancel()

			waitTime := 5 * time.Second

			// We'll wait 5 seconds for this operation to finish now, regardless
			// of whether it finishes successfully or not.
			select {
			case <-runningCtx.Done():
			case <-time.After(waitTime):
			}

		case <-runningCtx.Done():
			// The application finished nicely after the request was stopped.
		}
	case <-runningCtx.Done():
		// tests finished normally with no interrupts.
	}

	view.Diagnostics(nil, nil, testDiags)

	return 0
}
