package consulacls

import (
	"context"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"os/exec"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
)

// DefaultTFStateFile is the location of the TF state file, as created for the
// e2e test framework. This file is used to extract the TF serial number, which
// is used to determine whether the consul bootstrap process is necessary or has
// already taken place.
const DefaultTFStateFile = "terraform/terraform.tfstate"

// A Manager is used to manipulate whether Consul ACLs are enabled or disabled.
// Only works with TF provisioned clusters.
type Manager interface {
	// Enable Consul ACLs in the Consul cluster. The Consul ACL master token
	// associated with the Consul cluster is returned.
	//
	// A complete bootstrap process will take place if necessary.
	//
	// Once enabled, Consul ACLs can be disabled with Disable.
	Enable(t *testing.T) string

	// Disable Consul ACLs in the Consul Cluster.
	//
	// Once disabled, Consul ACLs can be re-enabled with Enable.
	Disable(t *testing.T)
}

type tfManager struct {
	serial int
}

func New(tfStateFile string) (*tfManager, error) {
	serial, err := extractSerial(tfStateFile)
	if err != nil {
		return nil, err
	}
	return &tfManager{
		serial: serial,
	}, nil
}

func (m *tfManager) Enable(t *testing.T) string {
	// Run the consul ACL bootstrap script, which will store the master token
	// in the deterministic path based on the TF state serial number. If the
	// bootstrap process had already taken place, ACLs will be activated but
	// without going through the bootstrap process again, re-using the already
	// existing Consul ACL master token.
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
	defer cancel()

	response, err := exec.CommandContext(ctx,
		"consulacls/consul-acls-manage.sh", "enable").CombinedOutput()
	require.NoError(t, err, "consul-acls-manage.sh failed: %v", string(response))
	fmt.Println(string(response))

	// Read the Consul ACL master token that was generated (or if the token
	// already existed because the bootstrap process had already taken place,
	// that one).
	token, err := m.readToken()
	require.NoError(t, err)
	return token
}

type tfState struct {
	Serial int `json:"serial"`
}

// extractSerial will parse the TF state file looking for the serial number.
func extractSerial(filename string) (int, error) {
	if filename == "" {
		filename = DefaultTFStateFile
	}
	b, err := ioutil.ReadFile(filename)
	if err != nil {
		return 0, fmt.Errorf("failed to extract TF serial: %w", err)
	}
	var state tfState
	if err := json.Unmarshal(b, &state); err != nil {
		return 0, fmt.Errorf("failed to extract TF serial: %w", err)
	}
	return state.Serial, nil
}

// tokenPath returns the expected path for the Consul ACL master token generated
// by the consul-acls-manage.sh bootstrap script for the current TF serial version.
func (m *tfManager) tokenPath() string {
	return fmt.Sprintf("/tmp/e2e-consul-bootstrap-%d.token", m.serial)
}

func (m *tfManager) readToken() (string, error) {
	b, err := ioutil.ReadFile(m.tokenPath())
	if err != nil {
		return "", err
	}
	return strings.TrimSpace(string(b)), nil
}

func (m *tfManager) Disable(t *testing.T) {
	// Run the consul ACL bootstrap script, which will modify the Consul Server
	// ACL policies to disable ACLs, and then restart those agents.
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
	defer cancel()

	response, err := exec.CommandContext(ctx,
		"consulacls/consul-acls-manage.sh", "disable").CombinedOutput()
	require.NoError(t, err)
	fmt.Println(string(response))
}
