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

package main

import (
	"flag"
	"fmt"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"testing"

	expect "github.com/Netflix/go-expect"
	tfe "github.com/hashicorp/go-tfe"
	"github.com/opentofu/opentofu/internal/e2e"
	tfversion "github.com/opentofu/opentofu/version"
)

var tofuBin string
var cliConfigFileEnv string

var tfeClient *tfe.Client
var tfeHostname string
var tfeToken string
var verboseMode bool

func TestMain(m *testing.M) {
	teardown := setup()
	code := m.Run()
	teardown()

	os.Exit(code)
}

func accTest() bool {
	// TF_ACC is set when we want to run acceptance tests, meaning it relies on
	// network access.
	return os.Getenv("TF_ACC") != ""
}

func hasHostname() bool {
	return os.Getenv("TFE_HOSTNAME") != ""
}

func hasToken() bool {
	return os.Getenv("TFE_TOKEN") != ""
}

func hasRequiredEnvVars() bool {
	return accTest() && hasHostname() && hasToken()
}

func skipIfMissingEnvVar(t *testing.T) {
	if !hasRequiredEnvVars() {
		t.Skip("Skipping test, required environment variables missing. Use `TF_ACC`, `TFE_HOSTNAME`, `TFE_TOKEN`")
	}
}

func setup() func() {
	tfOutput := flag.Bool("tfoutput", false, "This flag produces the terraform output from tests.")
	flag.Parse()
	verboseMode = *tfOutput

	setTfeClient()
	teardown := setupBinary()

	return func() {
		teardown()
	}
}
func testRunner(t *testing.T, cases testCases, orgCount int, tfEnvFlags ...string) {
	for name, tc := range cases {
		tc := tc // rebind tc into this lexical scope
		t.Run(name, func(subtest *testing.T) {
			subtest.Parallel()

			orgNames := []string{}
			for i := 0; i < orgCount; i++ {
				organization, cleanup := createOrganization(t)
				t.Cleanup(cleanup)
				orgNames = append(orgNames, organization.Name)
			}

			exp, err := expect.NewConsole(defaultOpts()...)
			if err != nil {
				subtest.Fatal(err)
			}
			defer exp.Close()

			tmpDir := t.TempDir()

			tf := e2e.NewBinary(t, tofuBin, tmpDir)
			tfEnvFlags = append(tfEnvFlags, "TF_LOG=INFO")
			tfEnvFlags = append(tfEnvFlags, cliConfigFileEnv)
			for _, env := range tfEnvFlags {
				tf.AddEnv(env)
			}

			var orgName string
			for index, op := range tc.operations {
				switch orgCount {
				case 0:
					orgName = ""
				case 1:
					orgName = orgNames[0]
				default:
					orgName = orgNames[index]
				}

				op.prep(t, orgName, tf.WorkDir())
				for _, tfCmd := range op.commands {
					cmd := tf.Cmd(tfCmd.command...)
					cmd.Stdin = exp.Tty()
					cmd.Stdout = exp.Tty()
					cmd.Stderr = exp.Tty()

					err = cmd.Start()
					if err != nil {
						subtest.Fatal(err)
					}

					if tfCmd.expectedCmdOutput != "" {
						got, err := exp.ExpectString(tfCmd.expectedCmdOutput)
						if err != nil {
							subtest.Fatalf("error while waiting for output\nwant: %s\nerror: %s\noutput\n%s", tfCmd.expectedCmdOutput, err, got)
						}
					}

					lenInput := len(tfCmd.userInput)
					lenInputOutput := len(tfCmd.postInputOutput)
					if lenInput > 0 {
						for i := 0; i < lenInput; i++ {
							input := tfCmd.userInput[i]
							_, err := exp.SendLine(input)
							if err != nil {
								subtest.Fatal(err)
							}
							// use the index to find the corresponding
							// output that matches the input.
							if lenInputOutput-1 >= i {
								output := tfCmd.postInputOutput[i]
								_, err := exp.ExpectString(output)
								if err != nil {
									subtest.Fatal(err)
								}
							}
						}
					}

					err = cmd.Wait()
					if err != nil && !tfCmd.expectError {
						subtest.Fatal(err)
					}
				}
			}

			if tc.validations != nil {
				tc.validations(t, orgName)
			}
		})
	}
}

func setTfeClient() {
	tfeHostname = os.Getenv("TFE_HOSTNAME")
	tfeToken = os.Getenv("TFE_TOKEN")

	cfg := &tfe.Config{
		Address: fmt.Sprintf("https://%s", tfeHostname),
		Token:   tfeToken,
	}

	if tfeHostname != "" && tfeToken != "" {
		// Create a new TFE client.
		client, err := tfe.NewClient(cfg)
		if err != nil {
			fmt.Printf("Could not create new tfe client: %v\n", err)
			os.Exit(1)
		}
		tfeClient = client
	}
}

func chdirOCF(dir string) {
	if err := os.Chdir(dir); err != nil {
		fmt.Printf("Could not change directories: %v\n", err)
		os.Exit(1)
	}
}

func setupBinary() func() {
	log.Println("Setting up terraform binary")
	tmpTerraformBinaryDir, err := os.MkdirTemp("", "terraform-test")
	if err != nil {
		fmt.Printf("Could not create temp directory: %v\n", err)
		os.Exit(1)
	}
	log.Println(tmpTerraformBinaryDir)
	currentDir, err := os.Getwd()
	defer chdirOCF(currentDir)
	if err != nil {
		fmt.Printf("Could not get current directory: %v\n", err)
		os.Exit(1)
	}
	// Getting top level dir
	newDir := filepath.ToSlash(currentDir)
	dirPaths := strings.Split(newDir, "/")
	log.Println(currentDir)
	topLevel := len(dirPaths) - 3
	topDir := strings.Join(dirPaths[0:topLevel], "/")

	chdirOCF(topDir)

	cmd := exec.Command(
		"go",
		"build",
		"-o", tmpTerraformBinaryDir,
		"-ldflags", fmt.Sprintf("-X \"github.com/opentofu/opentofu/version.Prerelease=%s\"", tfversion.Prerelease),
		"./cmd/tofu",
	)
	err = cmd.Run()
	if err != nil {
		fmt.Printf("Could not run exec command: %v\n", err)
		os.Exit(1)
	}

	credFile := fmt.Sprintf("%s/dev.tfrc", tmpTerraformBinaryDir)
	writeCredRC(credFile)

	tofuBin = fmt.Sprintf("%s/terraform", tmpTerraformBinaryDir)
	cliConfigFileEnv = fmt.Sprintf("TF_CLI_CONFIG_FILE=%s", credFile)

	return func() {
		os.RemoveAll(tmpTerraformBinaryDir)
	}
}

func writeCredRC(file string) {
	creds := credentialBlock()
	f, err := os.Create(file)
	if err != nil {
		fmt.Printf("Could not create file: %v\n", err)
		os.Exit(1)
	}
	_, err = f.WriteString(creds)
	if err != nil {
		fmt.Printf("Could not write credentials: %v\n", err)
		os.Exit(1)
	}
	f.Close()
}

func credentialBlock() string {
	return fmt.Sprintf(`
credentials "%s" {
  token = "%s"
}`, tfeHostname, tfeToken)
}
