// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

//go:build foundationdb

package foundationdb

import (
	"bytes"
	"fmt"
	"io/ioutil"
	"os"
	"testing"
	"time"

	log "github.com/hashicorp/go-hclog"
	uuid "github.com/hashicorp/go-uuid"

	"github.com/apple/foundationdb/bindings/go/src/fdb"
	"github.com/apple/foundationdb/bindings/go/src/fdb/directory"

	"github.com/hashicorp/vault/sdk/helper/logging"
	"github.com/hashicorp/vault/sdk/physical"

	dockertest "gopkg.in/ory-am/dockertest.v3"
)

func connectToFoundationDB(clusterFile string) (*fdb.Database, error) {
	if err := fdb.APIVersion(520); err != nil {
		return nil, fmt.Errorf("failed to set FDB API version: %w", err)
	}

	db, err := fdb.Open(clusterFile, []byte("DB"))
	if err != nil {
		return nil, fmt.Errorf("failed to open database: %w", err)
	}

	return &db, nil
}

func cleanupTopDir(clusterFile, topDir string) error {
	db, err := connectToFoundationDB(clusterFile)
	if err != nil {
		return fmt.Errorf("could not connect to FDB for cleanup: %w", err)
	}

	if _, err := directory.Root().Remove(db, []string{topDir}); err != nil {
		return fmt.Errorf("could not remove directory: %w", err)
	}

	return nil
}

func TestFoundationDBPathDecoration(t *testing.T) {
	cases := map[string][]byte{
		"foo":              []byte("/\x01foo"),
		"foo/":             []byte("/\x01foo/"),
		"foo/bar":          []byte("/\x02foo/\x01bar"),
		"foo/bar/":         []byte("/\x02foo/\x01bar/"),
		"foo/bar/baz":      []byte("/\x02foo/\x02bar/\x01baz"),
		"foo/bar/baz/":     []byte("/\x02foo/\x02bar/\x01baz/"),
		"foo/bar/baz/quux": []byte("/\x02foo/\x02bar/\x02baz/\x01quux"),
	}

	for path, expected := range cases {
		decorated, err := decoratePath(path)
		if err != nil {
			t.Fatalf("path %s error: %s", path, err)
		}

		if !bytes.Equal(expected, decorated) {
			t.Fatalf("path %s expected %v got %v", path, expected, decorated)
		}

		undecorated := undecoratePath(decorated)
		if undecorated != path {
			t.Fatalf("expected %s got %s", path, undecorated)
		}
	}
}

func TestFoundationDBBackend(t *testing.T) {
	if testing.Short() {
		t.Skipf("skipping in short mode")
	}

	testUUID, err := uuid.GenerateUUID()
	if err != nil {
		t.Fatalf("foundationdb: could not generate UUID to top-level directory: %s", err)
	}

	topDir := fmt.Sprintf("vault-test-%s", testUUID)

	var clusterFile string
	clusterFile = os.Getenv("FOUNDATIONDB_CLUSTER_FILE")
	if clusterFile == "" {
		var cleanup func()
		cleanup, clusterFile = prepareFoundationDBTestDirectory(t, topDir)
		defer cleanup()
	}

	// Remove the test data once done
	defer func() {
		if err := cleanupTopDir(clusterFile, topDir); err != nil {
			t.Fatalf("foundationdb: could not cleanup test data at end of test: %s", err)
		}
	}()

	// Remove any leftover test data before starting
	if err := cleanupTopDir(clusterFile, topDir); err != nil {
		t.Fatalf("foundationdb: could not cleanup test data before starting test: %s", err)
	}

	// Run vault tests
	logger := logging.NewVaultLogger(log.Debug)
	config := map[string]string{
		"path":         topDir,
		"api_version":  "520",
		"cluster_file": clusterFile,
	}

	b, err := NewFDBBackend(config, logger)
	if err != nil {
		t.Fatalf("foundationdb: failed to create new backend: %s", err)
	}

	b2, err := NewFDBBackend(config, logger)
	if err != nil {
		t.Fatalf("foundationdb: failed to create new backend: %s", err)
	}

	physical.ExerciseBackend(t, b)
	physical.ExerciseBackend_ListPrefix(t, b)
	physical.ExerciseTransactionalBackend(t, b)
	physical.ExerciseHABackend(t, b.(physical.HABackend), b2.(physical.HABackend))
}

func prepareFoundationDBTestDirectory(t *testing.T, topDir string) (func(), string) {
	pool, err := dockertest.NewPool("")
	if err != nil {
		t.Fatalf("foundationdb: failed to connect to docker: %s", err)
	}

	resource, err := pool.Run("foundationdb", "5.1.7", nil)
	if err != nil {
		t.Fatalf("foundationdb: could not start container: %s", err)
	}

	tmpFile, err := ioutil.TempFile("", topDir)
	if err != nil {
		t.Fatalf("foundationdb: could not create temporary file for cluster file: %s", err)
	}

	clusterFile := tmpFile.Name()

	cleanup := func() {
		var err error
		for i := 0; i < 10; i++ {
			err = pool.Purge(resource)
			if err == nil {
				break
			}
			time.Sleep(1 * time.Second)
		}
		os.Remove(clusterFile)
		if err != nil {
			t.Fatalf("Failed to cleanup local container: %s", err)
		}
	}

	setup := func() error {
		connectString := fmt.Sprintf("foundationdb:foundationdb@127.0.0.1:%s", resource.GetPort("4500/tcp"))

		if err := tmpFile.Truncate(0); err != nil {
			return fmt.Errorf("could not truncate cluster file: %w", err)
		}

		_, err := tmpFile.WriteAt([]byte(connectString), 0)
		if err != nil {
			return fmt.Errorf("could not write cluster file: %w", err)
		}

		if _, err := connectToFoundationDB(clusterFile); err != nil {
			return fmt.Errorf("could not connect to FoundationDB after starting container: %s", err)
		}

		return nil
	}

	if pool.Retry(setup); err != nil {
		cleanup()

		t.Fatalf("foundationdb: could not setup container: %s", err)
	}

	tmpFile.Close()

	return cleanup, clusterFile
}
