// 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 (
	"context"
	"fmt"
	"os"
	"testing"
	"time"

	expect "github.com/Netflix/go-expect"
	tfe "github.com/hashicorp/go-tfe"
	"github.com/hashicorp/go-uuid"
	goversion "github.com/hashicorp/go-version"
	tfversion "github.com/opentofu/opentofu/version"
)

const (
	// We need to give the console enough time to hear back.
	// 1 minute was too short in some cases, so this gives it ample time.
	expectConsoleTimeout = 3 * time.Minute
)

type tfCommand struct {
	command           []string
	expectedCmdOutput string
	expectError       bool
	userInput         []string
	postInputOutput   []string
}

type operationSets struct {
	commands []tfCommand
	prep     func(t *testing.T, orgName, dir string)
}

type testCases map[string]struct {
	operations  []operationSets
	validations func(t *testing.T, orgName string)
}

func defaultOpts() []expect.ConsoleOpt {
	opts := []expect.ConsoleOpt{
		expect.WithDefaultTimeout(expectConsoleTimeout),
	}
	if verboseMode {
		opts = append(opts, expect.WithStdout(os.Stdout))
	}
	return opts
}

func createOrganization(t *testing.T) (*tfe.Organization, func()) {
	ctx := context.Background()
	org, err := tfeClient.Organizations.Create(ctx, tfe.OrganizationCreateOptions{
		Name:                  tfe.String("tst-" + randomString(t)),
		Email:                 tfe.String(fmt.Sprintf("%s@tfe.local", randomString(t))),
		CostEstimationEnabled: tfe.Bool(false),
	})
	if err != nil {
		t.Fatal(err)
	}

	_, err = tfeClient.Admin.Organizations.Update(ctx, org.Name, tfe.AdminOrganizationUpdateOptions{
		AccessBetaTools: tfe.Bool(true),
	})
	if err != nil {
		t.Fatal(err)
	}

	return org, func() {
		if err := tfeClient.Organizations.Delete(ctx, org.Name); err != nil {
			t.Errorf("Error destroying organization! WARNING: Dangling resources\n"+
				"may exist! The full error is shown below.\n\n"+
				"Organization: %s\nError: %s", org.Name, err)
		}
	}
}

func createWorkspace(t *testing.T, orgName string, wOpts tfe.WorkspaceCreateOptions) *tfe.Workspace {
	ctx := context.Background()
	w, err := tfeClient.Workspaces.Create(ctx, orgName, wOpts)
	if err != nil {
		t.Fatal(err)
	}

	return w
}

func getWorkspace(workspaces []*tfe.Workspace, workspace string) (*tfe.Workspace, bool) {
	for _, ws := range workspaces {
		if ws.Name == workspace {
			return ws, false
		}
	}
	return nil, true
}

func randomString(t *testing.T) string {
	v, err := uuid.GenerateUUID()
	if err != nil {
		t.Fatal(err)
	}
	return v
}

func terraformConfigLocalBackend() string {
	return `
terraform {
  backend "local" {
  }
}

output "val" {
  value = "${terraform.workspace}"
}
`
}

func terraformConfigRemoteBackendName(org, name string) string {
	return fmt.Sprintf(`
terraform {
  backend "remote" {
    hostname = "%s"
    organization = "%s"

    workspaces {
      name = "%s"
    }
  }
}

output "val" {
  value = "${terraform.workspace}"
}
`, tfeHostname, org, name)
}

func terraformConfigRemoteBackendPrefix(org, prefix string) string {
	return fmt.Sprintf(`
terraform {
  backend "remote" {
    hostname = "%s"
    organization = "%s"

    workspaces {
      prefix = "%s"
    }
  }
}

output "val" {
  value = "${terraform.workspace}"
}
`, tfeHostname, org, prefix)
}

func terraformConfigCloudBackendTags(org, tag string) string {
	return fmt.Sprintf(`
terraform {
  cloud {
    hostname = "%s"
    organization = "%s"

    workspaces {
      tags = ["%s"]
    }
  }
}

output "tag_val" {
  value = "%s"
}
`, tfeHostname, org, tag, tag)
}

func terraformConfigCloudBackendName(org, name string) string {
	return fmt.Sprintf(`
terraform {
  cloud {
    hostname = "%s"
    organization = "%s"

    workspaces {
      name = "%s"
    }
  }
}

output "val" {
  value = "${terraform.workspace}"
}
`, tfeHostname, org, name)
}

func terraformConfigCloudBackendOmitOrg(workspaceName string) string {
	return fmt.Sprintf(`
terraform {
  cloud {
    hostname = "%s"

	workspaces {
	  name = "%s"
	}
  }
}

output "val" {
  value = "${terraform.workspace}"
}
`, tfeHostname, workspaceName)
}

func terraformConfigCloudBackendOmitWorkspaces(orgName string) string {
	return fmt.Sprintf(`
terraform {
  cloud {
    hostname = "%s"
	organization = "%s"
  }
}

output "val" {
  value = "${terraform.workspace}"
}
`, tfeHostname, orgName)
}

func terraformConfigCloudBackendOmitConfig() string {
	return `
terraform {
  cloud {}
}

output "val" {
  value = "${terraform.workspace}"
}
`
}

func writeMainTF(t *testing.T, block string, dir string) {
	f, err := os.Create(fmt.Sprintf("%s/main.tf", dir))
	if err != nil {
		t.Fatal(err)
	}
	_, err = f.WriteString(block)
	if err != nil {
		t.Fatal(err)
	}
	f.Close()
}

// The e2e tests rely on the fact that the terraform version in TFC/E is able to
// run the `cloud` configuration block, which is available in 1.1 and will
// continue to be available in later versions. So this function checks that
// there is a version that is >= 1.1.
func skipWithoutRemoteTerraformVersion(t *testing.T) {
	version := tfversion.Version
	baseVersion, err := goversion.NewVersion(version)
	if err != nil {
		t.Fatalf(fmt.Sprintf("Error instantiating go-version for %s", version))
	}
	opts := &tfe.AdminTerraformVersionsListOptions{
		ListOptions: tfe.ListOptions{
			PageNumber: 1,
			PageSize:   100,
		},
	}
	hasVersion := false

findTfVersion:
	for {
		// TODO: update go-tfe Read() to retrieve a tofu version by name.
		// Currently you can only retrieve by ID.
		tfVersionList, err := tfeClient.Admin.TerraformVersions.List(context.Background(), opts)
		if err != nil {
			t.Fatalf("Could not retrieve list of terraform versions: %v", err)
		}
		for _, item := range tfVersionList.Items {
			availableVersion, err := goversion.NewVersion(item.Version)
			if err != nil {
				t.Logf("Error instantiating go-version for %s", item.Version)
				continue
			}
			if availableVersion.Core().GreaterThanOrEqual(baseVersion.Core()) {
				hasVersion = true
				break findTfVersion
			}
		}

		// Exit the loop when we've seen all pages.
		if tfVersionList.CurrentPage >= tfVersionList.TotalPages {
			break
		}

		// Update the page number to get the next page.
		opts.PageNumber = tfVersionList.NextPage
	}

	if !hasVersion {
		t.Skipf("Skipping test because TFC/E does not have current Terraform version to test with (%s)", version)
	}
}
