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

package command

import (
	"context"
	"encoding/base64"
	"net"
	"net/http"
	"strings"
	"testing"
	"time"

	log "github.com/hashicorp/go-hclog"
	kv "github.com/hashicorp/vault-plugin-secrets-kv"
	"github.com/hashicorp/vault/api"
	"github.com/hashicorp/vault/audit"
	"github.com/hashicorp/vault/builtin/logical/pki"
	"github.com/hashicorp/vault/builtin/logical/ssh"
	"github.com/hashicorp/vault/builtin/logical/transit"
	"github.com/hashicorp/vault/helper/benchhelpers"
	"github.com/hashicorp/vault/helper/builtinplugins"
	"github.com/hashicorp/vault/sdk/helper/logging"
	"github.com/hashicorp/vault/sdk/logical"
	"github.com/hashicorp/vault/sdk/physical/inmem"
	"github.com/hashicorp/vault/vault"
	"github.com/hashicorp/vault/vault/seal"
	"github.com/mitchellh/cli"

	auditFile "github.com/hashicorp/vault/builtin/audit/file"
	credUserpass "github.com/hashicorp/vault/builtin/credential/userpass"
	vaulthttp "github.com/hashicorp/vault/http"
)

var (
	defaultVaultLogger = log.NewNullLogger()

	defaultVaultCredentialBackends = map[string]logical.Factory{
		"userpass": credUserpass.Factory,
	}

	defaultVaultAuditBackends = map[string]audit.Factory{
		"file": auditFile.Factory,
	}

	defaultVaultLogicalBackends = map[string]logical.Factory{
		"generic-leased": vault.LeasedPassthroughBackendFactory,
		"pki":            pki.Factory,
		"ssh":            ssh.Factory,
		"transit":        transit.Factory,
		"kv":             kv.Factory,
	}
)

// assertNoTabs asserts the CLI help has no tab characters.
func assertNoTabs(tb testing.TB, c cli.Command) {
	tb.Helper()

	if strings.ContainsRune(c.Help(), '\t') {
		tb.Errorf("%#v help output contains tabs", c)
	}
}

// testVaultServer creates a test vault cluster and returns a configured API
// client and closer function.
func testVaultServer(tb testing.TB) (*api.Client, func()) {
	tb.Helper()

	client, _, closer := testVaultServerUnseal(tb)
	return client, closer
}

func testVaultServerWithSecrets(ctx context.Context, tb testing.TB) (*api.Client, func()) {
	tb.Helper()

	client, _, closer := testVaultServerUnseal(tb)

	// enable kv-v1 backend
	if err := client.Sys().Mount("kv-v1/", &api.MountInput{
		Type: "kv-v1",
	}); err != nil {
		tb.Fatal(err)
	}

	// enable kv-v2 backend
	if err := client.Sys().Mount("kv-v2/", &api.MountInput{
		Type: "kv-v2",
	}); err != nil {
		tb.Fatal(err)
	}

	// populate dummy secrets
	for _, path := range []string{
		"foo",
		"app-1/foo",
		"app-1/bar",
		"app-1/nested/baz",
	} {
		if err := client.KVv1("kv-v1").Put(ctx, path, map[string]interface{}{
			"user":     "test",
			"password": "Hashi123",
		}); err != nil {
			tb.Fatal(err)
		}

		if _, err := client.KVv2("kv-v2").Put(ctx, path, map[string]interface{}{
			"user":     "test",
			"password": "Hashi123",
		}); err != nil {
			tb.Fatal(err)
		}
	}

	return client, closer
}

func testVaultServerWithKVVersion(tb testing.TB, kvVersion string) (*api.Client, func()) {
	tb.Helper()

	client, _, closer := testVaultServerUnsealWithKVVersionWithSeal(tb, kvVersion, nil)
	return client, closer
}

func testVaultServerAllBackends(tb testing.TB) (*api.Client, func()) {
	tb.Helper()

	client, _, closer := testVaultServerCoreConfig(tb, &vault.CoreConfig{
		DisableMlock:       true,
		DisableCache:       true,
		Logger:             defaultVaultLogger,
		CredentialBackends: credentialBackends,
		AuditBackends:      auditBackends,
		LogicalBackends:    logicalBackends,
		BuiltinRegistry:    builtinplugins.Registry,
	})
	return client, closer
}

// testVaultServerAutoUnseal creates a test vault cluster and sets it up with auto unseal
// the function returns a client, the recovery keys, and a closer function
func testVaultServerAutoUnseal(tb testing.TB) (*api.Client, []string, func()) {
	testSeal, _ := seal.NewTestSeal(nil)
	autoSeal, err := vault.NewAutoSeal(testSeal)
	if err != nil {
		tb.Fatal("unable to create autoseal", err)
	}
	return testVaultServerUnsealWithKVVersionWithSeal(tb, "1", autoSeal)
}

// testVaultServerUnseal creates a test vault cluster and returns a configured
// API client, list of unseal keys (as strings), and a closer function.
func testVaultServerUnseal(tb testing.TB) (*api.Client, []string, func()) {
	return testVaultServerUnsealWithKVVersionWithSeal(tb, "1", nil)
}

func testVaultServerUnsealWithKVVersionWithSeal(tb testing.TB, kvVersion string, seal vault.Seal) (*api.Client, []string, func()) {
	tb.Helper()
	logger := log.NewInterceptLogger(&log.LoggerOptions{
		Output:     log.DefaultOutput,
		Level:      log.Debug,
		JSONFormat: logging.ParseEnvLogFormat() == logging.JSONFormat,
	})

	return testVaultServerCoreConfigWithOpts(tb, &vault.CoreConfig{
		DisableMlock:       true,
		DisableCache:       true,
		Logger:             logger,
		CredentialBackends: defaultVaultCredentialBackends,
		AuditBackends:      defaultVaultAuditBackends,
		LogicalBackends:    defaultVaultLogicalBackends,
		BuiltinRegistry:    builtinplugins.Registry,
		Seal:               seal,
	}, &vault.TestClusterOptions{
		HandlerFunc: vaulthttp.Handler,
		NumCores:    1,
		KVVersion:   kvVersion,
	})
}

// testVaultServerUnseal creates a test vault cluster and returns a configured
// API client, list of unseal keys (as strings), and a closer function
// configured with the given plugin directory.
func testVaultServerPluginDir(tb testing.TB, pluginDir string) (*api.Client, []string, func()) {
	tb.Helper()

	return testVaultServerCoreConfig(tb, &vault.CoreConfig{
		DisableMlock:       true,
		DisableCache:       true,
		Logger:             defaultVaultLogger,
		CredentialBackends: defaultVaultCredentialBackends,
		AuditBackends:      defaultVaultAuditBackends,
		LogicalBackends:    defaultVaultLogicalBackends,
		PluginDirectory:    pluginDir,
		BuiltinRegistry:    builtinplugins.Registry,
	})
}

func testVaultServerCoreConfig(tb testing.TB, coreConfig *vault.CoreConfig) (*api.Client, []string, func()) {
	return testVaultServerCoreConfigWithOpts(tb, coreConfig, &vault.TestClusterOptions{
		HandlerFunc: vaulthttp.Handler,
		NumCores:    1, // Default is 3, but we don't need that many
	})
}

// testVaultServerCoreConfig creates a new vault cluster with the given core
// configuration. This is a lower-level test helper. If the seal config supports recovery keys, then
// recovery keys are returned. Otherwise, unseal keys are returned
func testVaultServerCoreConfigWithOpts(tb testing.TB, coreConfig *vault.CoreConfig, opts *vault.TestClusterOptions) (*api.Client, []string, func()) {
	tb.Helper()

	cluster := vault.NewTestCluster(benchhelpers.TBtoT(tb), coreConfig, opts)
	cluster.Start()

	// Make it easy to get access to the active
	core := cluster.Cores[0].Core
	vault.TestWaitActive(benchhelpers.TBtoT(tb), core)

	// Get the client already setup for us!
	client := cluster.Cores[0].Client
	client.SetToken(cluster.RootToken)

	var keys [][]byte
	if coreConfig.Seal != nil && coreConfig.Seal.RecoveryKeySupported() {
		keys = cluster.RecoveryKeys
	} else {
		keys = cluster.BarrierKeys
	}

	return client, encodeKeys(keys), cluster.Cleanup
}

// Convert the unseal keys to base64 encoded, since these are how the user
// will get them.
func encodeKeys(rawKeys [][]byte) []string {
	keys := make([]string, len(rawKeys))
	for i := range rawKeys {
		keys[i] = base64.StdEncoding.EncodeToString(rawKeys[i])
	}
	return keys
}

// testVaultServerUninit creates an uninitialized server.
func testVaultServerUninit(tb testing.TB) (*api.Client, func()) {
	tb.Helper()

	inm, err := inmem.NewInmem(nil, defaultVaultLogger)
	if err != nil {
		tb.Fatal(err)
	}

	core, err := vault.NewCore(&vault.CoreConfig{
		DisableMlock:       true,
		DisableCache:       true,
		Logger:             defaultVaultLogger,
		Physical:           inm,
		CredentialBackends: defaultVaultCredentialBackends,
		AuditBackends:      defaultVaultAuditBackends,
		LogicalBackends:    defaultVaultLogicalBackends,
		BuiltinRegistry:    builtinplugins.Registry,
	})
	if err != nil {
		tb.Fatal(err)
	}

	ln, addr := vaulthttp.TestServer(tb, core)

	client, err := api.NewClient(&api.Config{
		Address: addr,
	})
	if err != nil {
		tb.Fatal(err)
	}

	closer := func() {
		core.Shutdown()
		ln.Close()
	}

	return client, closer
}

// testVaultServerBad creates an http server that returns a 500 on each request
// to simulate failures.
func testVaultServerBad(tb testing.TB) (*api.Client, func()) {
	tb.Helper()

	listener, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		tb.Fatal(err)
	}

	server := &http.Server{
		Addr: "127.0.0.1:0",
		Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			http.Error(w, "500 internal server error", http.StatusInternalServerError)
		}),
		ReadTimeout:       1 * time.Second,
		ReadHeaderTimeout: 1 * time.Second,
		WriteTimeout:      1 * time.Second,
		IdleTimeout:       1 * time.Second,
	}

	go func() {
		if err := server.Serve(listener); err != nil && err != http.ErrServerClosed {
			tb.Fatal(err)
		}
	}()

	client, err := api.NewClient(&api.Config{
		Address: "http://" + listener.Addr().String(),
	})
	if err != nil {
		tb.Fatal(err)
	}

	return client, func() {
		ctx, done := context.WithTimeout(context.Background(), 5*time.Second)
		defer done()

		server.Shutdown(ctx)
	}
}

// testTokenAndAccessor creates a new authentication token capable of being renewed with
// the default policy attached. It returns the token and it's accessor.
func testTokenAndAccessor(tb testing.TB, client *api.Client) (string, string) {
	tb.Helper()

	secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{
		Policies: []string{"default"},
		TTL:      "30m",
	})
	if err != nil {
		tb.Fatal(err)
	}
	if secret == nil || secret.Auth == nil || secret.Auth.ClientToken == "" {
		tb.Fatalf("missing auth data: %#v", secret)
	}
	return secret.Auth.ClientToken, secret.Auth.Accessor
}

func testClient(tb testing.TB, addr string, token string) *api.Client {
	tb.Helper()
	config := api.DefaultConfig()
	config.Address = addr
	client, err := api.NewClient(config)
	if err != nil {
		tb.Fatal(err)
	}
	client.SetToken(token)

	return client
}
