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

// Package vault provides methods for commonly used vault actions that are used in end-to-end tests.
package vault

import (
	"context"
	"errors"
	"fmt"
	"os"
	"path"
	"path/filepath"
	"strings"
	"testing"

	"github.com/hashicorp/boundary/testing/internal/e2e"
	"github.com/hashicorp/go-secure-stdlib/base62"
	"github.com/stretchr/testify/require"
)

// CreateTokenResponse parses the json response from running `vault token create`
type CreateTokenResponse struct {
	Auth struct {
		Client_Token string
	}
}

// SetupForBoundaryController verifies if appropriate credentials are set and
// adds the boundary controller policy to vault. Returns the policy name.
func SetupForBoundaryController(t testing.TB, boundaryControllerFilePath string) (boundaryPolicyName string) {
	// Set up boundary policy
	boundaryPolicyFilePath, err := filepath.Abs(boundaryControllerFilePath)
	require.NoError(t, err)
	boundaryPolicyName = WritePolicy(t, t.Context(), boundaryPolicyFilePath)

	return boundaryPolicyName
}

// SetupLdapWithAd sets a Vault server up for LDAP against an Active Directory server. It
// enables the LDAP secrets engine, configures it and creates a static user
// according to what is in Active Directory. Note that this function does not put any
// clean-up in place to run after a test is complete. When applicable, callers
// should destroy the Vault LDAP policy this function creates.
func SetupLdapWithAd(t testing.TB, vaultLdapMountPath, ldapAddr, ldapAdmin, ldapAdminPw, ldapUser, ldapDn string) (string, error) {
	// Enable LDAP secrets engine.
	output := e2e.RunCommand(t.Context(), "vault",
		e2e.WithArgs("secrets", "enable", fmt.Sprintf("-path=%s", vaultLdapMountPath), "ldap"),
	)
	if output.Err != nil {
		return "", errors.New(strings.TrimSpace(string(output.Stderr)))
	}

	// Define and write LDAP access policy to Vault.
	vaultLdapPolicyFilePath := path.Join(t.TempDir(), "ldap-policy.hcl")
	f, err := os.Create(vaultLdapPolicyFilePath)
	if err != nil {
		return "", err
	}
	_, err = fmt.Fprintf(f, `
		path "%[1]s/static-cred/%[2]s" {
			capabilities = ["read"]
		}
		path "%[1]s/static-role/%[2]s" {
			capabilities = ["create", "read", "update", "patch", "delete", "list"]
		}
	`, vaultLdapMountPath, ldapUser)
	if err != nil {
		return "", err
	}
	err = f.Sync()
	if err != nil {
		return "", err
	}
	_ = f.Close()

	policyName := WritePolicy(t, t.Context(), vaultLdapPolicyFilePath)

	// Configure LDAP secrets engine to point to AD service.
	output = e2e.RunCommand(t.Context(), "vault",
		e2e.WithArgs(
			"write",
			fmt.Sprintf("%s/config", vaultLdapMountPath),
			fmt.Sprintf("url=%s", ldapAddr),
			fmt.Sprintf("binddn=cn=%s,%s", ldapAdmin, ldapDn),
			fmt.Sprintf("bindpass=%s", ldapAdminPw),
			"schema=ad",
			"insecure_tls=true",
		),
	)
	if output.Err != nil {
		return "", errors.New(strings.TrimSpace(string(output.Stderr)))
	}

	// Create static LDAP user in Vault (already defined in AD server).
	output = e2e.RunCommand(t.Context(), "vault",
		e2e.WithArgs(
			"write",
			fmt.Sprintf("%s/static-role/%s", vaultLdapMountPath, ldapUser),
			fmt.Sprintf("dn=cn=%s,%s", ldapUser, ldapDn),
			fmt.Sprintf("username=%s", ldapUser),
			"rotation_period=24h",
		),
	)
	if output.Err != nil {
		return "", errors.New(strings.TrimSpace(string(output.Stderr)))
	}

	return policyName, nil
}

// SetupLdapWithOpenLdap sets a Vault server up for LDAP against an OpenLDAP server. It
// enables the LDAP secrets engine, configures it and creates a static user
// according to what is in OpenLDAP. Additionally, it sets up Vault's ability to
// manage LDAP users dynamically. Note that this function does not put any
// clean-up in place to run after a test is complete. When applicable, callers
// should destroy the Vault LDAP policy this function creates.
func SetupLdapWithOpenLdap(t testing.TB, vaultLdapMountPath, ldapAddr, ldapAdminDn, ldapAdminPw, ldapDn, ldapUser, ldapGroup string) (string, error) {
	// Enable LDAP secrets engine.
	output := e2e.RunCommand(t.Context(), "vault",
		e2e.WithArgs("secrets", "enable", fmt.Sprintf("-path=%s", vaultLdapMountPath), "ldap"),
	)
	if output.Err != nil {
		return "", errors.New(strings.TrimSpace(string(output.Stderr)))
	}

	// Define and write LDAP access policy to Vault.
	vaultLdapPolicyFilePath := path.Join(t.TempDir(), "ldap-policy.hcl")
	f, err := os.Create(vaultLdapPolicyFilePath)
	if err != nil {
		return "", err
	}

	_, err = fmt.Fprintf(f, `
		path "%[1]s/static-cred/%[2]s" {
			capabilities = ["read"]
		}
		path "%[1]s/static-role/%[2]s" {
			capabilities = ["create", "read", "update", "patch", "delete", "list"]
		}

		path "%[1]s/creds/%[3]s" {
			capabilities = ["read"]
		}
		path "%[1]s/role/%[3]s" {
			capabilities = ["create", "read", "update", "patch", "delete", "list"]
		}
	`, vaultLdapMountPath, ldapUser, ldapGroup)
	if err != nil {
		return "", err
	}
	err = f.Sync()
	if err != nil {
		return "", err
	}
	_ = f.Close()

	policyName := WritePolicy(t, t.Context(), vaultLdapPolicyFilePath)

	// Configure LDAP secrets engine to point to existing OpenLDAP server.
	output = e2e.RunCommand(t.Context(), "vault",
		e2e.WithArgs(
			"write",
			fmt.Sprintf("%s/config", vaultLdapMountPath),
			fmt.Sprintf("url=%s", ldapAddr),
			fmt.Sprintf("binddn=%s", ldapAdminDn),
			fmt.Sprintf("bindpass=%s", ldapAdminPw),
		),
	)
	if output.Err != nil {
		return "", errors.New(strings.TrimSpace(string(output.Stderr)))
	}

	// Create static LDAP user in Vault (already defined in OpenLDAP server).
	output = e2e.RunCommand(t.Context(), "vault",
		e2e.WithArgs(
			"write",
			fmt.Sprintf("%s/static-role/%s", vaultLdapMountPath, ldapUser),
			fmt.Sprintf("dn=cn=%s,%s", ldapUser, ldapDn),
			fmt.Sprintf("username=%s", ldapUser),
			"rotation_period=24h",
		),
	)
	if output.Err != nil {
		return "", errors.New(strings.TrimSpace(string(output.Stderr)))
	}

	// Create Vault dynamic role for LDAP group.
	createLdif := fmt.Sprintf(`
		dn: cn={{.Username}},%[1]s
		changetype: add
		objectClass: inetOrgPerson
		cn: {{.Username}}
		sn: {{.Username}}
		uid: {{.Username}}
		userPassword:{{.Password}}

		dn: cn=%s,%[1]s
		changetype: modify
		add: uniqueMember
		uniqueMember: cn={{.Username}},%[1]s
		-
	`, ldapDn, ldapGroup)
	createLdif = strings.ReplaceAll(createLdif, "\t", "")

	deleteLdif := fmt.Sprintf(`
		dn: cn={{.Username}},%s
		changetype: delete
	`, ldapDn)
	deleteLdif = strings.ReplaceAll(deleteLdif, "\t", "")

	output = e2e.RunCommand(t.Context(), "vault",
		e2e.WithArgs(
			"write",
			fmt.Sprintf("%s/role/%s", vaultLdapMountPath, ldapGroup),
			"username_template=b_{{.DisplayName}}_{{.RoleName}}_{{random 10}}_{{unix_time}}",
			fmt.Sprintf("creation_ldif=%s", createLdif),
			fmt.Sprintf("rollback_ldif=%s", deleteLdif),
			fmt.Sprintf("deletion_ldif=%s", deleteLdif),
			"default_ttl=1h",
			"max_ttxl=24h",
		),
	)
	if output.Err != nil {
		return "", errors.New(strings.TrimSpace(string(output.Stderr)))
	}

	return policyName, nil
}

// CreateKvPrivateKeyCredential creates a private key credential in vault and
// creates a vault policy to be able to read that credential. Returns the secret
// and policy names. Note that this function does not put any clean-up in place
// to run after a test is complete. When applicable, callers should destroy the
// policy and secret this function creates.
func CreateKvPrivateKeyCredential(t testing.TB, secretPath string, user string, keyPath string) (secretName string, policyName string) {
	secretName, err := base62.Random(16)
	require.NoError(t, err)

	policyFilePath := path.Join(t.TempDir(), fmt.Sprintf("kv-pk-%s-policy.hcl", secretName))
	f, err := os.Create(policyFilePath)
	require.NoError(t, err)

	_, err = fmt.Fprintf(f, "path \"%s/data/%s\" { capabilities = [\"read\"] }\n",
		secretPath,
		secretName,
	)
	require.NoError(t, err)

	policyName = WritePolicy(t, t.Context(), policyFilePath)

	// Create secret
	output := e2e.RunCommand(context.Background(), "vault",
		e2e.WithArgs(
			"kv", "put",
			"-mount", secretPath,
			secretName,
			"username="+user,
			"private_key=@"+keyPath,
		),
	)
	require.NoError(t, output.Err, string(output.Stderr))

	return secretName, policyName
}

// CreateKvPasswordCredential creates a username/password credential in vault
// and creates a vault policy to be able to read that credential. Returns the
// secret and policy names as well as the password for the secret. Note that
// this function does not put any clean-up in place to run after a test is
// complete. When applicable, callers should destroy the policy and secret this
// function creates.
func CreateKvPasswordCredential(t testing.TB, secretPath string, user string, providedPassword ...string) (secretName string, policyName string, password string) {
	secretName, err := base62.Random(16)
	require.NoError(t, err)

	policyFilePath := path.Join(t.TempDir(), fmt.Sprintf("kv-up-%s-policy.hcl", secretName))
	f, err := os.Create(policyFilePath)
	require.NoError(t, err)

	_, err = fmt.Fprintf(f, "path \"%s/data/%s\" { capabilities = [\"read\"] }\n",
		secretPath,
		secretName,
	)
	require.NoError(t, err)

	policyName = WritePolicy(t, t.Context(), policyFilePath)

	// Use provided password or generate random one
	if len(providedPassword) > 0 && providedPassword[0] != "" {
		password = providedPassword[0]
	} else {
		password, err = base62.Random(16)
		require.NoError(t, err)
	}

	// Escape '@' when prefixed to password. Vault CLI interprets '@' as a file
	var passwordArg string
	if strings.HasPrefix(password, "@") {
		passwordArg = fmt.Sprintf("\\%s", password)
	} else {
		passwordArg = password
	}

	// Create secret
	output := e2e.RunCommand(context.Background(), "vault",
		e2e.WithArgs(
			"kv", "put",
			"-mount", secretPath,
			secretName,
			fmt.Sprintf("username=%s", user),
			fmt.Sprintf("password=%s", passwordArg),
		),
	)
	require.NoError(t, output.Err, string(output.Stderr))

	return secretName, policyName, password
}

// CreateKvPasswordDomainCredential creates a username/password/domain
// credential in vault and creates a vault policy to be able to read that
// credential. Returns the name of the policy. Note that this function does not
// put any clean-up in place to run after a test is complete. When applicable,
// callers should destroy the policy and secret this function creates.
func CreateKvPasswordDomainCredential(t testing.TB, secretPath string, user string, domain string, providedPassword ...string) (secretName string, policyName string, password string) {
	secretName, err := base62.Random(16)
	require.NoError(t, err)

	policyFilePath := path.Join(t.TempDir(), fmt.Sprintf("kv-upd-%s-policy.hcl", secretName))
	f, err := os.Create(policyFilePath)
	require.NoError(t, err)

	_, err = fmt.Fprintf(f, "path \"%s/data/%s\" { capabilities = [\"read\"] }\n",
		secretPath,
		secretName,
	)
	require.NoError(t, err)

	policyName = WritePolicy(t, t.Context(), policyFilePath)

	// Use provided password or generate random one
	if len(providedPassword) > 0 && providedPassword[0] != "" {
		password = providedPassword[0]
	} else {
		password, err = base62.Random(16)
		require.NoError(t, err)
	}

	// Escape '@' when prefixed to password. Vault CLI interprets '@' as a file
	var passwordArg string
	if strings.HasPrefix(password, "@") {
		passwordArg = fmt.Sprintf("\\%s", password)
	} else {
		passwordArg = password
	}

	// Create secret
	output := e2e.RunCommand(context.Background(), "vault",
		e2e.WithArgs(
			"kv", "put",
			"-mount", secretPath,
			secretName,
			fmt.Sprintf("username=%s", user),
			fmt.Sprintf("password=%s", passwordArg),
			fmt.Sprintf("domain=%s", domain),
		),
	)
	require.NoError(t, output.Err, string(output.Stderr))

	return secretName, policyName, password
}

// WritePolicy adds a policy to vault. Provide a name for the policy that you want to create as well
// as the path to the file that contains the policy definition. Returns a policy name
func WritePolicy(t testing.TB, ctx context.Context, policyFilePath string) string {
	policyName, err := base62.Random(16)
	require.NoError(t, err)

	output := e2e.RunCommand(ctx, "vault",
		e2e.WithArgs("policy", "write", policyName, policyFilePath),
	)
	require.NoError(t, output.Err, string(output.Stderr))

	return policyName
}
