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

package vault

import (
	"context"
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/pem"
	"fmt"
	"math/big"
	mrand "math/rand"
	"net"
	"net/http"
	"path"
	"strings"
	"sync/atomic"
	"testing"
	"time"

	"github.com/hashicorp/boundary/globals"
	"github.com/hashicorp/boundary/internal/db"
	"github.com/hashicorp/boundary/internal/db/common"
	"github.com/hashicorp/boundary/internal/errors"
	"github.com/hashicorp/boundary/internal/kms"
	"github.com/hashicorp/boundary/internal/scheduler"
	wrapping "github.com/hashicorp/go-kms-wrapping/v2"
	vault "github.com/hashicorp/vault/api"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// TestCredentialStore creates a vault credential store in the provided DB with
// the provided project, vault address, token, and accessor and any values passed
// in through the Options vargs.  If any errors are encountered during the
// creation of the store, the test will fail.
func TestCredentialStore(t testing.TB, conn *db.DB, wrapper wrapping.Wrapper, projectId, vaultAddr, vaultToken, accessor string, opts ...Option) *CredentialStore {
	t.Helper()
	ctx := context.Background()
	kmsCache := kms.TestKms(t, conn, wrapper)
	w := db.New(conn)

	databaseWrapper, err := kmsCache.GetWrapper(ctx, projectId, kms.KeyPurposeDatabase)
	assert.NoError(t, err)
	require.NotNil(t, databaseWrapper)

	cs, err := NewCredentialStore(projectId, vaultAddr, []byte(vaultToken), opts...)
	assert.NoError(t, err)
	require.NotNil(t, cs)
	id, err := newCredentialStoreId(ctx)
	assert.NoError(t, err)
	require.NotEmpty(t, id)
	cs.PublicId = id

	_, err2 := w.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{},
		func(_ db.Reader, iw db.Writer) error {
			require.NoError(t, iw.Create(ctx, cs))
			cert := cs.clientCert
			if cert != nil {
				cert.StoreId = cs.GetPublicId()
				require.NoError(t, cert.encrypt(ctx, databaseWrapper))
				require.NoError(t, iw.Create(ctx, cert))
			}
			return nil
		},
	)
	require.NoError(t, err2)

	cs.outputToken = createTestToken(t, conn, wrapper, projectId, id, vaultToken, accessor)

	return cs
}

// TestCredentialStores creates count number of vault credential stores in
// the provided DB with the provided project id. If any errors are
// encountered during the creation of the credential stores, the test will
// fail.
func TestCredentialStores(t testing.TB, conn *db.DB, wrapper wrapping.Wrapper, projectId string, count int) []*CredentialStore {
	t.Helper()
	ctx := context.Background()
	kmsCache := kms.TestKms(t, conn, wrapper)
	w := db.New(conn)

	databaseWrapper, err := kmsCache.GetWrapper(ctx, projectId, kms.KeyPurposeDatabase)
	assert.NoError(t, err)
	require.NotNil(t, databaseWrapper)

	var css []*CredentialStore
	for i := 0; i < count; i++ {
		cs := TestCredentialStore(t, conn, wrapper, projectId, fmt.Sprintf("http://vault%d", i), fmt.Sprintf("vault-token-%s-%d", projectId, i), fmt.Sprintf("accessor-%s-%d", projectId, i))

		inCert := testClientCert(t, testCaCert(t))
		clientCert, err := NewClientCertificate(ctx, inCert.Cert.Cert, inCert.Cert.Key)
		require.NoError(t, err)
		require.NotEmpty(t, clientCert)
		clientCert.StoreId = cs.GetPublicId()
		require.NoError(t, clientCert.encrypt(ctx, databaseWrapper))

		_, err3 := w.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{},
			func(_ db.Reader, iw db.Writer) error {
				return iw.Create(ctx, clientCert)
			},
		)
		require.NoError(t, err3)

		cs.clientCert = clientCert
		css = append(css, cs)
	}
	return css
}

// TestCredentialLibraries creates count number of vault credential
// libraries in the provided DB with the provided store id. If any errors
// are encountered during the creation of the credential libraries, the
// test will fail.
func TestCredentialLibraries(t testing.TB, conn *db.DB, _ wrapping.Wrapper, storeId string, credType globals.CredentialType, count int) []*CredentialLibrary {
	t.Helper()
	ctx := context.Background()
	assert, require := assert.New(t), require.New(t)
	w := db.New(conn)
	var libs []*CredentialLibrary

	for i := 0; i < count; i++ {
		opts := []Option{WithMethod(MethodGet)}
		if credType != "" {
			opts = append(opts, WithCredentialType(credType))
		}
		lib, err := NewCredentialLibrary(storeId, fmt.Sprintf("vault/path%d", i), opts...)
		assert.NoError(err)
		require.NotNil(lib)
		id, err := newCredentialLibraryId(ctx)
		assert.NoError(err)
		require.NotEmpty(id)
		lib.PublicId = id

		_, err2 := w.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{},
			func(_ db.Reader, iw db.Writer) error {
				return iw.Create(ctx, lib)
			},
		)

		require.NoError(err2)
		libs = append(libs, lib)
	}
	return libs
}

// TestSSHCertificateCredentialLibraries creates count number of vault ssh
// certificate credential libraries in the provided DB with the provided store
// id. If any errors are encountered during the creation of the credential
// libraries, the test will fail.
func TestSSHCertificateCredentialLibraries(t testing.TB, conn *db.DB, _ wrapping.Wrapper, storeId string, count int) []*SSHCertificateCredentialLibrary {
	t.Helper()
	ctx := context.Background()
	assert, require := assert.New(t), require.New(t)
	w := db.New(conn)
	var libs []*SSHCertificateCredentialLibrary

	for i := 0; i < count; i++ {
		lib, err := NewSSHCertificateCredentialLibrary(storeId, fmt.Sprintf("ssh/sign/role-%d", i), "username", WithKeyType(KeyTypeEd25519))
		assert.NoError(err)
		require.NotNil(lib)
		id, err := newSSHCertificateCredentialLibraryId(ctx)
		assert.NoError(err)
		require.NotEmpty(id)
		lib.PublicId = id

		_, err2 := w.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{},
			func(_ db.Reader, iw db.Writer) error {
				return iw.Create(ctx, lib)
			},
		)

		require.NoError(err2)
		libs = append(libs, lib)
	}
	return libs
}

// TestLdapCredentialLibraries creates `count` number of Vault LDAP credential
// libraries in the provided db with the provided credential store. If any
// errors are encountered, the test will fail.
func TestLdapCredentialLibraries(t testing.TB, conn *db.DB, _ wrapping.Wrapper, storeId string, count int) []*LdapCredentialLibrary {
	t.Helper()
	w := db.New(conn)

	var libs []*LdapCredentialLibrary
	for i := range count {
		var staticOrDynamic string
		switch mrand.Intn(2) {
		case 0:
			staticOrDynamic = "static-cred"
		case 1:
			staticOrDynamic = "creds"
		}

		lib, err := NewLdapCredentialLibrary(storeId, fmt.Sprintf("ldap/%s/role-%d", staticOrDynamic, i))
		require.NoError(t, err)
		require.NotNil(t, lib)

		libId, err := newLdapCredentialLibraryId(t.Context())
		require.NoError(t, err)
		require.NotEmpty(t, libId)

		lib.PublicId = libId
		_, err = w.DoTx(t.Context(), db.StdRetryCnt, db.ExpBackoff{},
			func(_ db.Reader, w db.Writer) error {
				return w.Create(t.Context(), lib)
			},
		)
		require.NoError(t, err)
		libs = append(libs, lib)
	}

	return libs
}

// TestCredentials creates count number of vault credentials in the provided DB with
// the provided library id and session id. If any errors are encountered
// during the creation of the credentials, the test will fail.
func TestCredentials(t testing.TB, conn *db.DB, wrapper wrapping.Wrapper, libraryId, sessionId string, count int) []*Credential {
	t.Helper()
	assert, require := assert.New(t), require.New(t)
	rw := db.New(conn)

	ctx := context.Background()
	kms := kms.TestKms(t, conn, wrapper)
	sche := scheduler.TestScheduler(t, conn, wrapper)
	repo, err := NewRepository(ctx, rw, rw, kms, sche)
	assert.NoError(err)
	require.NotNil(repo)

	lib, err := repo.LookupCredentialLibrary(ctx, libraryId)
	assert.NoError(err)
	require.NotNil(lib)

	store, err := repo.LookupCredentialStore(ctx, lib.GetStoreId())
	assert.NoError(err)
	require.NotNil(store)

	token := store.Token()
	require.NotNil(token)

	var credentials []*Credential
	for i := 0; i < count; i++ {
		credential, err := newCredential(ctx, lib.GetPublicId(), fmt.Sprintf("vault/credential/%d", i), token.GetTokenHmac(), 5*time.Minute)
		assert.NoError(err)
		require.NotNil(credential)

		id, err := newCredentialId(ctx)
		assert.NoError(err)
		require.NotNil(id)
		credential.PublicId = id

		query, queryValues := insertQuery(credential, sessionId)
		rows, err2 := rw.Exec(ctx, query, queryValues)
		assert.Equal(1, rows)
		assert.NoError(err2)

		credentials = append(credentials, credential)
	}
	return credentials
}

func createTestToken(t testing.TB, conn *db.DB, wrapper wrapping.Wrapper, projectId, storeId, token, accessor string) *Token {
	t.Helper()
	w := db.New(conn)
	ctx := context.Background()
	kkms := kms.TestKms(t, conn, wrapper)
	databaseWrapper, err := kkms.GetWrapper(ctx, projectId, kms.KeyPurposeDatabase)
	require.NoError(t, err)

	inToken, err := newToken(ctx, storeId, []byte(token), []byte(accessor), 5*time.Minute)
	assert.NoError(t, err)
	require.NotNil(t, inToken)

	require.NoError(t, inToken.encrypt(ctx, databaseWrapper))
	query, queryValues := inToken.insertQuery()

	rows, err2 := w.Exec(ctx, query, queryValues)
	assert.Equal(t, 1, rows)
	require.NoError(t, err2)

	return inToken
}

func testTokens(t testing.TB, conn *db.DB, wrapper wrapping.Wrapper, projectId, storeId string, count int) []*Token {
	t.Helper()
	assert, require := assert.New(t), require.New(t)
	w := db.New(conn)

	ctx := context.Background()
	kkms := kms.TestKms(t, conn, wrapper)
	databaseWrapper, err := kkms.GetWrapper(ctx, projectId, kms.KeyPurposeDatabase)
	assert.NoError(err)
	require.NotNil(databaseWrapper)

	r := mrand.New(mrand.NewSource(time.Now().UnixNano()))
	var tokens []*Token
	for i := 0; i < count; i++ {
		num := r.Int31()
		inToken := createTestToken(t, conn, wrapper, projectId, storeId, fmt.Sprintf("vault-token-%s-%d-%v", storeId, i, num), fmt.Sprintf("accessor-%s-%d-%v", storeId, i, num))
		outToken := allocToken()
		require.NoError(w.LookupWhere(ctx, &outToken, "token_hmac = ?", []any{inToken.TokenHmac}))
		require.NoError(outToken.decrypt(ctx, databaseWrapper))

		tokens = append(tokens, outToken)
	}
	return tokens
}

// TestVaultTLS represents the TLS configuration level of a
// TestVaultServer.
type TestVaultTLS int

const (
	// TestNoTLS disables TLS. The test server Addr begins with http://.
	TestNoTLS TestVaultTLS = iota // no TLS

	// TestServerTLS configures the Vault test server listener to use TLS.
	// A CA certificate is generated and a server certificate is issued
	// from the CA certificate. The CA certificate is available in the
	// CaCert field of the TestVaultServer. The test server Addr begins
	// with https://.
	TestServerTLS

	// TestClientTLS configures the Vault test server listener to require a
	// client certificate for mTLS and includes all of the settings from
	// TestServerTLS. A second CA certificate is generated and a client
	// certificate is issued from this CA certificate. The client
	// certificate and the client certificate key are available in the in
	// the ClientCert and ClientKey fields of the TestVaultServer
	// respectively.
	TestClientTLS
)

type testCert struct {
	Cert []byte
	Key  []byte

	certificate *x509.Certificate
	priv        *ecdsa.PrivateKey
}

func (tc *testCert) ClientCertificate(t testing.TB) *ClientCertificate {
	t.Helper()
	c, err := NewClientCertificate(context.Background(), tc.Cert, tc.Key)
	require.NoError(t, err)
	return c
}

type testCertBundle struct {
	CA   *testCert
	Cert *testCert
}

// testCaCert will generate a test x509 CA cert.
func testCaCert(t testing.TB) *testCert {
	t.Helper()
	require := require.New(t)

	priv, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
	require.NoError(err)

	// ECDSA, ED25519 and RSA subject keys should have the DigitalSignature
	// KeyUsage bits set in the x509.Certificate template
	keyUsage := x509.KeyUsageDigitalSignature

	notBefore, notAfter := time.Now(), time.Now().Add(24*time.Hour)
	if t, ok := t.(*testing.T); ok {
		if deadline, ok := t.Deadline(); ok {
			notAfter = deadline
		}
	}

	serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
	serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
	require.NoError(err)

	template := x509.Certificate{
		SerialNumber: serialNumber,
		Subject: pkix.Name{
			Organization: []string{"Acme CA"},
		},
		NotBefore: notBefore,
		NotAfter:  notAfter,

		KeyUsage:              keyUsage | x509.KeyUsageCertSign,
		IsCA:                  true,
		BasicConstraintsValid: true,
	}

	derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
	require.NoError(err)

	c, err := x509.ParseCertificate(derBytes)
	require.NoError(err)

	k, err := x509.MarshalECPrivateKey(priv)
	require.NoError(err)

	return &testCert{
		Cert:        pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}),
		Key:         pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: k}),
		certificate: c,
		priv:        priv,
	}
}

// testServerCert will generate a test x509 server cert.
func testServerCert(t testing.TB, ca *testCert, opt ...TestOption) *testCertBundle {
	t.Helper()
	require := require.New(t)

	priv, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
	require.NoError(err)

	// ECDSA, ED25519 and RSA subject keys should have the DigitalSignature
	// KeyUsage bits set in the x509.Certificate template
	keyUsage := x509.KeyUsageDigitalSignature

	notBefore, notAfter := time.Now(), time.Now().Add(24*time.Hour)
	if t, ok := t.(*testing.T); ok {
		if deadline, ok := t.Deadline(); ok {
			notAfter = deadline
		}
	}

	serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
	serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
	require.NoError(err)

	template := x509.Certificate{
		SerialNumber: serialNumber,
		Subject: pkix.Name{
			Organization: []string{"Acme Servers Inc"},
		},
		NotBefore: notBefore,
		NotAfter:  notAfter,

		KeyUsage:              keyUsage,
		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
		IsCA:                  false,
		BasicConstraintsValid: true,
	}

	opts := getTestOpts(t, opt...)
	for _, h := range opts.serverCertHostNames {
		if ip := net.ParseIP(h); ip != nil {
			template.IPAddresses = append(template.IPAddresses, ip)
		} else {
			template.DNSNames = append(template.DNSNames, h)
		}
	}

	derBytes, err := x509.CreateCertificate(rand.Reader, &template, ca.certificate, &priv.PublicKey, ca.priv)
	require.NoError(err)

	c, err := x509.ParseCertificate(derBytes)
	require.NoError(err)

	k, err := x509.MarshalECPrivateKey(priv)
	require.NoError(err)

	serverCert := &testCert{
		Cert:        pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}),
		Key:         pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: k}),
		certificate: c,
		priv:        priv,
	}

	return &testCertBundle{
		CA:   ca,
		Cert: serverCert,
	}
}

// testClientCert will generate a test x509 client cert.
func testClientCert(t testing.TB, ca *testCert, opt ...TestOption) *testCertBundle {
	t.Helper()
	require := require.New(t)

	var err error
	opts := getTestOpts(t, opt...)
	priv := opts.clientKey
	if priv == nil {
		priv, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
		require.NoError(err)
	}

	// ECDSA, ED25519 and RSA subject keys should have the DigitalSignature
	// KeyUsage bits set in the x509.Certificate template
	keyUsage := x509.KeyUsageDigitalSignature

	notBefore, notAfter := time.Now(), time.Now().Add(24*time.Hour)
	if t, ok := t.(*testing.T); ok {
		if deadline, ok := t.Deadline(); ok {
			notAfter = deadline
		}
	}

	serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
	serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
	require.NoError(err)

	template := x509.Certificate{
		SerialNumber: serialNumber,
		Subject: pkix.Name{
			Organization: []string{"Acme Clients Inc"},
		},
		NotBefore: notBefore,
		NotAfter:  notAfter,

		KeyUsage:              keyUsage,
		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
		IsCA:                  false,
		BasicConstraintsValid: true,
	}

	derBytes, err := x509.CreateCertificate(rand.Reader, &template, ca.certificate, &priv.PublicKey, ca.priv)
	require.NoError(err)

	c, err := x509.ParseCertificate(derBytes)
	require.NoError(err)

	k, err := x509.MarshalECPrivateKey(priv)
	require.NoError(err)

	clientCert := &testCert{
		Cert:        pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}),
		Key:         pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: k}),
		certificate: c,
		priv:        priv,
	}

	return &testCertBundle{
		CA:   ca,
		Cert: clientCert,
	}
}

func getTestOpts(t testing.TB, opt ...TestOption) testOptions {
	t.Helper()
	opts := getDefaultTestOptions(t)
	for _, o := range opt {
		o(t, &opts)
	}
	return opts
}

// TestOption - how Options are passed as arguments.
type TestOption func(testing.TB, *testOptions)

// options = how options are represented
type testOptions struct {
	orphan              bool
	periodic            bool
	renewable           bool
	policies            []string
	allowedExtensions   []string
	mountPath           string
	roleName            string
	vaultTLS            TestVaultTLS
	dockerNetwork       bool
	skipCleanup         bool
	tokenPeriod         time.Duration
	clientKey           *ecdsa.PrivateKey
	vaultVersion        string
	serverCertHostNames []string
	bindDn              string
	bindPass            string
	ldapUrl             string
	ldapRootDomain      string
}

func getDefaultTestOptions(t testing.TB) testOptions {
	t.Helper()
	defaultPeriod := 24 * time.Hour
	if t, ok := t.(*testing.T); ok {
		if deadline, ok := t.Deadline(); ok {
			defaultPeriod = time.Until(deadline)
		}
	}

	return testOptions{
		orphan:              true,
		periodic:            true,
		renewable:           true,
		policies:            []string{"default", "boundary-controller"},
		mountPath:           "",
		roleName:            "boundary",
		vaultTLS:            TestNoTLS,
		dockerNetwork:       false,
		tokenPeriod:         defaultPeriod,
		serverCertHostNames: []string{"localhost", "127.0.0.1", "::1"},
	}
}

// WithTokenPeriod sets the period value in a vault.TokenCreateRequest when
// the token being requested is a periodic token. The default token period
// is the value of t.Deadline() or 24 hours if
// t.Deadline() is nil.
func WithTokenPeriod(d time.Duration) TestOption {
	return func(t testing.TB, o *testOptions) {
		t.Helper()
		o.tokenPeriod = d
	}
}

// WithPolicies sets the polices to attach to a token. The default policy
// attached to tokens is 'default'.
func WithPolicies(p []string) TestOption {
	return func(t testing.TB, o *testOptions) {
		t.Helper()
		o.policies = p
	}
}

// WithDockerNetwork sets the option to create docker network when creating
// a Vault test server. The default is to not create a docker network.
func WithDockerNetwork(b bool) TestOption {
	return func(t testing.TB, o *testOptions) {
		t.Helper()
		o.dockerNetwork = b
	}
}

// WithTestVaultTLS sets the Vault TLS option.
// TestNoTLS is the default TLS option.
func WithTestVaultTLS(s TestVaultTLS) TestOption {
	return func(t testing.TB, o *testOptions) {
		t.Helper()
		o.vaultTLS = s
	}
}

// WithServerCertHostNames sets the host names or IP address to attach to
// the test server's TLS certificate. The default host name attached to the
// test server's TLS certificate is 'localhost'.
func WithServerCertHostNames(h []string) TestOption {
	return func(t testing.TB, o *testOptions) {
		t.Helper()
		o.serverCertHostNames = h
	}
}

// WithClientKey sets the private key that will be used to generate the
// client certificate.  The option is only valid when used together with TestClientTLS.
func WithClientKey(k *ecdsa.PrivateKey) TestOption {
	return func(t testing.TB, o *testOptions) {
		t.Helper()
		o.clientKey = k
	}
}

// WithTestMountPath sets the mount path option to p.
func WithTestMountPath(p string) TestOption {
	return func(t testing.TB, o *testOptions) {
		t.Helper()
		p = strings.TrimSpace(p)
		if p != "" {
			p = strings.TrimRight(p, "/") + "/"
		}
		o.mountPath = p
	}
}

// WithTestRoleName sets the roleName name to n.
// The default role name is boundary.
func WithTestRoleName(n string) TestOption {
	return func(t testing.TB, o *testOptions) {
		t.Helper()
		n = strings.TrimSpace(n)
		if n == "" {
			n = "boundary"
		}
		o.roleName = n
	}
}

// WithDontCleanUp causes the resource created to not
// be automaticaly cleaned up at the end of the test run.
func WithDontCleanUp() TestOption {
	return func(t testing.TB, o *testOptions) {
		t.Helper()
		o.skipCleanup = true
	}
}

// WithVaultVersion sets the version of vault that will be started.
// defaults to the value stored in vault.supported.DefaultVaultVersion
func WithVaultVersion(s string) TestOption {
	return func(t testing.TB, o *testOptions) {
		t.Helper()
		o.vaultVersion = s
	}
}

// WithAllowedExtension tells vault to allow a specific SSH extension
// to be used by vault's ssh secrets engine
func WithAllowedExtension(e string) TestOption {
	return func(t testing.TB, o *testOptions) {
		t.Helper()
		o.allowedExtensions = append(o.allowedExtensions, e)
	}
}

// WithBindDn sets the distinguished name to be used by the Vault LDAP secrets
// engine.
func WithBindDn(dn string) TestOption {
	return func(t testing.TB, o *testOptions) {
		t.Helper()
		o.bindDn = dn
	}
}

// WithBindPass sets the password to be used by the Vault LDAP secrets engine.
// It is also used to set the admin user password in the LDAP server.
func WithBindPass(p string) TestOption {
	return func(t testing.TB, o *testOptions) {
		t.Helper()
		o.bindPass = p
	}
}

// WithLdapUrl sets the URL to be used by the Vault LDAP secrets engine.
func WithLdapUrl(u string) TestOption {
	return func(t testing.TB, o *testOptions) {
		t.Helper()
		o.ldapUrl = u
	}
}

// WithLdapRootDomain controls the LDAP server's root domain.
func WithLdapRootDomain(d string) TestOption {
	return func(t testing.TB, o *testOptions) {
		t.Helper()
		o.ldapRootDomain = d
	}
}

// TestOrphanToken sets the token orphan option to b.
// The orphan option is true by default.
func TestOrphanToken(b bool) TestOption {
	return func(t testing.TB, o *testOptions) {
		t.Helper()
		o.orphan = b
	}
}

// TestPeriodicToken sets the token periodic option to b.
// The periodic option is true by default.
func TestPeriodicToken(b bool) TestOption {
	return func(t testing.TB, o *testOptions) {
		t.Helper()
		o.periodic = b
	}
}

// TestRenewableToken sets the token renewable option to b.
// The renewable option is true by default.
func TestRenewableToken(b bool) TestOption {
	return func(t testing.TB, o *testOptions) {
		t.Helper()
		o.renewable = b
	}
}

// TestClientConfig returns a client config, using the
// provided Vault Server and token
func TestClientConfig(v *TestVaultServer, token string) *clientConfig {
	clientConfig := &clientConfig{
		Addr:       v.Addr,
		Token:      TokenSecret(token),
		CaCert:     v.CaCert,
		ClientCert: v.ClientCert,
		ClientKey:  v.ClientKey,
	}

	return clientConfig
}

func (v *TestVaultServer) client(t testing.TB) *client {
	t.Helper()
	return v.ClientUsingToken(t, v.RootToken)
}

func (v *TestVaultServer) ClientUsingToken(t testing.TB, token string) *client {
	t.Helper()
	ctx := context.Background()
	require := require.New(t)
	conf := &clientConfig{
		Addr:          v.Addr,
		Token:         TokenSecret(token),
		CaCert:        v.CaCert,
		ClientCert:    v.ClientCert,
		ClientKey:     v.ClientKey,
		TlsServerName: v.TlsServerName,
		TlsSkipVerify: v.TlsSkipVerify,
	}

	client, err := newClient(ctx, conf)
	require.NoError(err)
	require.NotNil(client)
	require.NoError(client.ping(ctx))
	return client
}

// CreateToken creates a new Vault token by calling /auth/token/create on v
// using v.RootToken. It returns the vault secret containing the token and
// the token itself. See
// https://www.vaultproject.io/api-docs/auth/token#create-token.
func (v *TestVaultServer) CreateToken(t testing.TB, opt ...TestOption) (*vault.Secret, string) {
	t.Helper()
	require := require.New(t)
	opts := getTestOpts(t, opt...)

	var period string
	if opts.periodic {
		period = opts.tokenPeriod.String()
	}
	req := &vault.TokenCreateRequest{
		DisplayName: t.Name(),
		NoParent:    opts.orphan,
		Renewable:   &opts.renewable,
		Period:      period,
		Policies:    opts.policies,
	}
	vc := v.client(t).cl
	secret, err := vc.Auth().Token().Create(req)
	require.NoError(err)
	require.NotNil(secret)
	token, err := secret.TokenID()
	require.NoError(err)
	require.NotNil(token)

	return secret, token
}

// RevokeToken calls /auth/token/revoke-self on v for the token. See
// https://www.vaultproject.io/api-docs/auth/token#revoke-a-token-self.
func (v *TestVaultServer) RevokeToken(t testing.TB, token string) {
	t.Helper()
	require := require.New(t)
	vc := v.client(t).cl
	vc.SetToken(token)
	err := vc.Auth().Token().RevokeSelf("")
	require.NoError(err)
}

// LookupToken calls /auth/token/lookup on v for the token. See
// https://www.vaultproject.io/api-docs/auth/token#lookup-a-token.
func (v *TestVaultServer) LookupToken(t testing.TB, token string) *vault.Secret {
	t.Helper()
	require := require.New(t)
	vc := v.client(t).cl
	secret, err := vc.Auth().Token().Lookup(token)
	require.NoError(err)
	require.NotNil(secret)
	return secret
}

// VerifyTokenInvalid calls /auth/token/lookup on v for the token. It expects the lookup to
// fail with a StatusForbidden.  See
// https://www.vaultproject.io/api-docs/auth/token#lookup-a-token.
func (v *TestVaultServer) VerifyTokenInvalid(t testing.TB, token string) {
	t.Helper()
	require, assert := require.New(t), assert.New(t)
	vc := v.client(t).cl
	secret, err := vc.Auth().Token().Lookup(token)
	require.Error(err)
	require.Nil(secret)

	// Verify error was forbidden
	var respErr *vault.ResponseError
	require.True(errors.As(err, &respErr))
	assert.Equal(http.StatusForbidden, respErr.StatusCode)
}

// LookupLease calls the /sys/leases/lookup Vault endpoint and returns
// the vault.Secret response.  See
// https://www.vaultproject.io/api-docs/system/leases#read-lease.
func (v *TestVaultServer) LookupLease(t testing.TB, leaseId string) *vault.Secret {
	t.Helper()
	require := require.New(t)
	vc := v.client(t).cl
	credData := map[string]any{"lease_id": leaseId}
	secret, err := vc.Logical().Write("sys/leases/lookup", credData)
	require.NoError(err)
	require.NotNil(secret)
	return secret
}

func (v *TestVaultServer) addPolicy(t testing.TB, name string, pc pathCapabilities) {
	t.Helper()
	require := require.New(t)
	policy := pc.vaultPolicy()
	require.NotEmpty(policy)
	vc := v.client(t).cl
	require.NoError(vc.Sys().PutPolicy(name, policy))
}

// MountSSH mounts the Vault SSH secret engine and initializes it by
// generating a root certificate authority and creating a default role on
// the mount. The root CA is returned.
//
// The default mount path is ssh and the default role name is boundary.
// WithTestMountPath, WithTestRoleName, and WithAllowedExtension are the
// test options supported. extensions are combined into allowed_extensions.
// allowed_extensions defaults to "*" (allow all)
// The role is defined as:
//
//	{
//		"key_type": "ca",
//		"allowed_users": "*",
//		"allowed_extensions": "*",
//		"allow_user_certificates": true,
//		"ttl": "12h0m0s"
//	}
//
// MountSSH also adds a Vault policy named 'ssh' to v and adds it to the
// standard set of polices attached to tokens created with v.CreateToken.
// The policy is defined as:
//
//	path "mountPath/*" {
//	  capabilities = ["create", "read", "update", "delete", "list"]
//	}
func (v *TestVaultServer) MountSSH(t testing.TB, opt ...TestOption) *vault.Secret {
	require := require.New(t)
	opts := getTestOpts(t, opt...)
	vc := v.client(t).cl

	// Mount SSH
	maxTTL := 24 * time.Hour
	if t, ok := t.(*testing.T); ok {
		if deadline, ok := t.Deadline(); ok {
			maxTTL = time.Until(deadline) * 2
		}
	}

	defaultTTL := maxTTL / 2
	t.Logf("maxTTL: %s, defaultTTL: %s", maxTTL, defaultTTL)
	mountInput := &vault.MountInput{
		Type:        "ssh",
		Description: t.Name(),
		Config: vault.MountConfigInput{
			DefaultLeaseTTL: defaultTTL.String(),
			MaxLeaseTTL:     maxTTL.String(),
		},
	}
	mountPath := opts.mountPath
	if mountPath == "" {
		mountPath = "ssh/"
	}
	// this is the call that actually enables the secrets engine
	require.NoError(vc.Sys().Mount(mountPath, mountInput))

	policyPath := fmt.Sprintf("%s*", mountPath)
	pc := pathCapabilities{
		policyPath: createCapability | readCapability | updateCapability | deleteCapability | listCapability,
	}
	v.addPolicy(t, "ssh", pc)

	// Generate a root CA
	caPath := path.Join(mountPath, "config/ca")
	caOptions := map[string]any{
		"generate_signing_key": true,
	}
	s, err := vc.Logical().Write(caPath, caOptions)
	require.NoError(err)
	require.NotEmpty(s)

	// Create default role
	rolePath := path.Join(mountPath, "roles", opts.roleName)
	allowExtensions := "*"
	if len(opts.allowedExtensions) > 0 {
		allowExtensions = strings.Join(opts.allowedExtensions, ",")
	}
	roleOptions := map[string]any{
		"key_type":                "ca",
		"allowed_users":           "*",
		"allowed_extensions":      allowExtensions,
		"allow_user_certificates": true,
		"ttl":                     defaultTTL.String(),
	}
	_, err = vc.Logical().Write(rolePath, roleOptions)
	require.NoError(err)

	return s
}

// MountLdap mounts the Vault LDAP secrets engine on a Vault instance and
// initializes it.
//
// Supported options include:
//   - WithTestMountPath: Sets the mount path. Defaults to "ldap".
//   - WithBindDn: Sets the BindDN to be used by the Vault LDAP secrets engine.
//     Defaults to "cn=admin,dc=example,dc=org".
//   - WithBindPass: Sets the password to be used by the Vault LDAP secrets
//     engine. Defaults to "admin".
//   - WithLdapUrl: Sets the URL of the LDAP server to be used by the Vault LDAP
//     secrets engine. Defaults to "ldap://ldap".
//
// MountLdap also adds a Vault policy named 'ldap' to the standard set of
// policies attached to tokens created with v.CreateToken. The policy is defined
// as:
//
//	path "<ldapMountPath>/*" {
//	  capabilities = ["create", "read", "update", "delete", "list"]
//	}
func (v *TestVaultServer) MountLdap(t testing.TB, opt ...TestOption) {
	require := require.New(t)
	opts := getTestOpts(t, opt...)
	vc := v.client(t).cl

	maxTTL := 24 * time.Hour
	if t, ok := t.(*testing.T); ok {
		if deadline, ok := t.Deadline(); ok {
			maxTTL = time.Until(deadline) * 2
		}
	}

	defaultTTL := maxTTL / 2
	mountInput := &vault.MountInput{
		Type:        "ldap",
		Description: t.Name(),
		Config: vault.MountConfigInput{
			DefaultLeaseTTL: defaultTTL.String(),
			MaxLeaseTTL:     maxTTL.String(),
		},
	}
	mountPath := opts.mountPath
	if mountPath == "" {
		mountPath = "ldap/"
	}
	require.True(mountPath[len(mountPath)-1] == '/', "mount path must end with a slash")
	require.NoError(vc.Sys().Mount(mountPath, mountInput))

	policyPath := fmt.Sprintf("%s*", mountPath)
	pc := pathCapabilities{
		policyPath: createCapability | readCapability | updateCapability | deleteCapability | listCapability,
	}
	v.addPolicy(t, "ldap", pc)

	// Create and write LDAP server config
	bindDn := opts.bindDn
	if bindDn == "" {
		bindDn = "cn=admin,dc=example,dc=org"
	}
	bindPass := opts.bindPass
	if bindPass == "" {
		bindPass = "admin"
	}
	url := opts.ldapUrl
	if url == "" {
		url = "ldap://ldap"
	}
	ldapConfig := map[string]any{
		"binddn":   bindDn,
		"bindpass": bindPass,
		"url":      url,
	}
	_, err := vc.Logical().Write(path.Join(mountPath, "config"), ldapConfig)
	require.NoError(err)
}

// MountPKI mounts the Vault PKI secret engine and initializes it by
// generating a root certificate authority and creating a default role on
// the mount. The root CA is returned.
//
// The default mount path is pki and the default role name is boundary.
// WithTestMountPath and WithTestRoleName are the only test options
// supported.
//
// MountPKI also adds a Vault policy named 'pki' to v and adds it to the
// standard set of polices attached to tokens created with v.CreateToken.
// The policy is defined as:
//
//	path "mountPath/*" {
//	  capabilities = ["create", "read", "update", "delete", "list"]
//	}
func (v *TestVaultServer) MountPKI(t testing.TB, opt ...TestOption) *vault.Secret {
	t.Helper()
	require := require.New(t)
	opts := getTestOpts(t, opt...)
	vc := v.client(t).cl

	// Mount PKI
	maxTTL := 24 * time.Hour
	if t, ok := t.(*testing.T); ok {
		if deadline, ok := t.Deadline(); ok {
			maxTTL = time.Until(deadline) * 2
		}
	}

	defaultTTL := maxTTL / 2
	t.Logf("maxTTL: %s, defaultTTL: %s", maxTTL, defaultTTL)
	mountInput := &vault.MountInput{
		Type:        "pki",
		Description: t.Name(),
		Config: vault.MountConfigInput{
			DefaultLeaseTTL: defaultTTL.String(),
			MaxLeaseTTL:     maxTTL.String(),
		},
	}
	mountPath := opts.mountPath
	if mountPath == "" {
		mountPath = "pki/"
	}
	require.NoError(vc.Sys().Mount(mountPath, mountInput))
	policyPath := fmt.Sprintf("%s*", mountPath)
	pc := pathCapabilities{
		policyPath: createCapability | readCapability | updateCapability | deleteCapability | listCapability,
	}
	v.addPolicy(t, "pki", pc)

	// Generate a root CA
	caPath := path.Join(mountPath, "root/generate/internal")
	caOptions := map[string]any{
		"common_name": t.Name(),
		"ttl":         maxTTL.String(),
	}
	s, err := vc.Logical().Write(caPath, caOptions)
	require.NoError(err)
	require.NotEmpty(s)

	// Create default role
	rolePath := path.Join(mountPath, "roles", opts.roleName)
	roleOptions := map[string]any{
		"allow_any_name": true,
		"ttl":            defaultTTL.String(),
	}
	_, err = vc.Logical().Write(rolePath, roleOptions)
	require.NoError(err)

	return s
}

// AddKVPolicy adds a Vault policy named 'secret' to v and adds it to the
// standard set of polices attached to tokens created with v.CreateToken.
// The policy is defined as:
//
//	path "secret/*" {
//	  capabilities = ["create", "read", "update", "delete", "list"]
//	}
//
// All options are ignored.
func (v *TestVaultServer) AddKVPolicy(t testing.TB, _ ...TestOption) {
	t.Helper()

	pc := pathCapabilities{
		"secret/data/*": createCapability | readCapability | updateCapability | deleteCapability | listCapability,
	}
	v.addPolicy(t, "secret", pc)
}

// CreateKVSecret calls the /secret/data/:p endpoint with the provided
// data. Please note for KV-v2 the provided data needs to be in JSON format similar to:
// `{"data": {"key": "value", "key2": "value2"}}`
// See
// https://www.vaultproject.io/api-docs/secret/kv/kv-v2#create-update-secret
func (v *TestVaultServer) CreateKVSecret(t testing.TB, p string, data []byte) *vault.Secret {
	t.Helper()
	ctx := context.Background()
	require := require.New(t)

	vc := v.client(t)
	cred, err := vc.post(ctx, path.Join("secret", "data", p), data)
	require.NoError(err)
	return cred
}

// TestVaultServer is a vault server running in a docker container suitable
// for testing.
type TestVaultServer struct {
	RootToken string
	Addr      string

	CaCert        []byte
	ServerCert    []byte
	ClientCert    []byte
	ClientKey     []byte
	TlsServerName string
	TlsSkipVerify bool

	serverCertBundle *testCertBundle
	clientCertBundle *testCertBundle

	// Use any here instead of *dockertest.[Pool|Network|Resource] to avoid
	// problems with dockertest not building for some netbsd architectures.
	pool              any
	network           any
	vaultContainer    any
	ldapContainer     any
	postgresContainer any

	stopped  atomic.Bool
	Shutdown func(t *testing.T)
}

// NewTestVaultServer creates and returns a TestVaultServer. Some Vault
// secret engines require the Vault server be created with a docker
// network. Check the Mount method for the Vault secret engine to see if a
// docker network is required.
//
// WithTestVaultTLS and WithDockerNetwork are the only valid options.
// Setting the WithDockerNetwork option can significantly increase the
// amount of time required for a test to run.
func NewTestVaultServer(t testing.TB, opt ...TestOption) *TestVaultServer {
	t.Helper()
	return newVaultServer(t, opt...)
}

// MountDatabase starts a PostgreSQL database in a docker container then
// mounts the Vault database secrets engine and configures it to issue
// credentials for the database.
//
// MountDatabase also adds a Vault policy named 'database' to v and adds it
// to the standard set of polices attached to tokens created with
// v.CreateToken. The policy is defined as:
//
//	path "mountPath/*" {
//	  capabilities = ["create", "read", "update", "delete", "list"]
//	}
//
// MountDatabase returns a TestDatabase for testing credentials from the
// mount.
func (v *TestVaultServer) MountDatabase(t testing.TB, opt ...TestOption) *TestDatabase {
	t.Helper()
	return mountDatabase(t, v, opt...)
}

// TestDatabaseURL is a connection string with place holders for username
// and password to the database started by MountDatabase.
type TestDatabaseURL string

// Encode encodes the username and password credentials from s into u.
func (u TestDatabaseURL) Encode(t testing.TB, s *vault.Secret) string {
	t.Helper()
	require := require.New(t)
	require.NotNil(s, "vault.Secret")
	username, ok := s.Data["username"].(string)
	require.True(ok, "username")
	password, ok := s.Data["password"].(string)
	require.True(ok, "password")
	return fmt.Sprintf(string(u), username, password)
}

// TestDatabase is returned from MountDatabase and can be used to test
// database credentials returned by Vault for that mount.
type TestDatabase struct {
	URL TestDatabaseURL
}

// ValidateCredential tests the credentials in s against d. An error is
// returned if the credentials are not valid.
func (d *TestDatabase) ValidateCredential(t testing.TB, s *vault.Secret) error {
	t.Helper()
	require := require.New(t)
	require.NotNil(s)
	dburl := d.URL.Encode(t, s)
	db, err := common.SqlOpen("postgres", dburl)
	if err != nil {
		return err
	}
	defer db.Close()
	if err := db.Ping(); err != nil {
		return err
	}
	return nil
}

// TestLdapServer is returned from MountLdapServer and contains information
// about how an LDAP server was setup. It is primarily used to test
// Boundary <> Vault <> LDAP Server interactions.
type TestLdapServer struct {
	BindDomain string
	BindPass   string
	BindDn     string
	Url        string
}

// MountLdapServer creates an LDAP server as a Docker container, within a
// running Vault's Docker network. Additionally, it mounts and configures the
// LDAP secrets engine in the passed in Vault instance.
//
// Supported options:
//   - WithBindPass. Controls the LDAP server's admin password. Defaults to
//     "admin".
//   - WithLdapRootDomain. Controls the LDAP server's root domain. Defaults to
//     "example.org".
//
// Some roles are added to the LDAP server as well as configured within Vault:
//   - scientists: A group of unique members: cn=scientists,dc=example,dc=org
//   - einstein: A user belonging to the scientists group: cn=einstein,dc=example,dc=org
//   - newton: Another user beloging to the scientists group: cn=newton,dc=example,dc=org
//
// Note that the domain components (dc) will change accordingly if WithDomain is
// passed in.
//
// Dynamic roles are also setup using LDIF templates. These roles trigger Vault
// to create users on-demand and manage their lifecycle. All dynamic users are
// added to the "scientists" group.
func MountLdapServer(t testing.TB, v *TestVaultServer, opt ...TestOption) *TestLdapServer {
	t.Helper()
	return mountLdapServer(t, v, opt...)
}
