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

package couchdb

import (
	"context"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"runtime"
	"strings"
	"testing"
	"time"

	log "github.com/hashicorp/go-hclog"
	"github.com/hashicorp/vault/sdk/helper/docker"
	"github.com/hashicorp/vault/sdk/helper/logging"
	"github.com/hashicorp/vault/sdk/physical"
)

func TestCouchDBBackend(t *testing.T) {
	cleanup, config := prepareCouchdbDBTestContainer(t)
	defer cleanup()

	logger := logging.NewVaultLogger(log.Debug)

	b, err := NewCouchDBBackend(map[string]string{
		"endpoint": config.URL().String(),
		"username": config.username,
		"password": config.password,
	}, logger)
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	physical.ExerciseBackend(t, b)
	physical.ExerciseBackend_ListPrefix(t, b)
}

func TestTransactionalCouchDBBackend(t *testing.T) {
	cleanup, config := prepareCouchdbDBTestContainer(t)
	defer cleanup()

	logger := logging.NewVaultLogger(log.Debug)

	b, err := NewTransactionalCouchDBBackend(map[string]string{
		"endpoint": config.URL().String(),
		"username": config.username,
		"password": config.password,
	}, logger)
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	physical.ExerciseBackend(t, b)
	physical.ExerciseBackend_ListPrefix(t, b)
}

type couchDB struct {
	baseURL  url.URL
	dbname   string
	username string
	password string
}

func (c couchDB) Address() string {
	return c.baseURL.Host
}

func (c couchDB) URL() *url.URL {
	u := c.baseURL
	u.Path = c.dbname
	return &u
}

var _ docker.ServiceConfig = &couchDB{}

func prepareCouchdbDBTestContainer(t *testing.T) (func(), *couchDB) {
	// ARM64 is only supported on CouchDB 2 and above. If we update
	// our image and support to 2 and above, we can unskip these:
	// https://hub.docker.com/r/arm64v8/couchdb/
	if strings.Contains(runtime.GOARCH, "arm") {
		t.Skip("Skipping, as CouchDB 1.6 is not supported on ARM architectures")
	}

	// If environment variable is set, assume caller wants to target a real
	// DynamoDB.
	if os.Getenv("COUCHDB_ENDPOINT") != "" {
		return func() {}, &couchDB{
			baseURL:  url.URL{Host: os.Getenv("COUCHDB_ENDPOINT")},
			username: os.Getenv("COUCHDB_USERNAME"),
			password: os.Getenv("COUCHDB_PASSWORD"),
		}
	}

	runner, err := docker.NewServiceRunner(docker.RunOptions{
		ContainerName:   "couchdb",
		ImageRepo:       "docker.mirror.hashicorp.services/library/couchdb",
		ImageTag:        "1.6",
		Ports:           []string{"5984/tcp"},
		DoNotAutoRemove: true,
	})
	if err != nil {
		t.Fatalf("Could not start local CouchDB: %s", err)
	}

	svc, err := runner.StartService(context.Background(), setupCouchDB)
	if err != nil {
		t.Fatalf("Could not start local CouchDB: %s", err)
	}

	return svc.Cleanup, svc.Config.(*couchDB)
}

func setupCouchDB(ctx context.Context, host string, port int) (docker.ServiceConfig, error) {
	c := &couchDB{
		baseURL:  url.URL{Scheme: "http", Host: fmt.Sprintf("%s:%d", host, port)},
		dbname:   fmt.Sprintf("vault-test-%d", time.Now().Unix()),
		username: "admin",
		password: "admin",
	}

	{
		resp, err := http.Get(c.baseURL.String())
		if err != nil {
			return nil, err
		}
		if resp.StatusCode != http.StatusOK {
			return nil, fmt.Errorf("expected couchdb to return status code 200, got (%s) instead", resp.Status)
		}
	}

	{
		req, err := http.NewRequest("PUT", c.URL().String(), nil)
		if err != nil {
			return nil, fmt.Errorf("could not create create database request: %q", err)
		}

		resp, err := http.DefaultClient.Do(req)
		if err != nil {
			return nil, fmt.Errorf("could not create database: %q", err)
		}
		defer resp.Body.Close()
		if resp.StatusCode != http.StatusCreated {
			bs, _ := io.ReadAll(resp.Body)
			return nil, fmt.Errorf("failed to create database: %s %s\n", resp.Status, string(bs))
		}
	}

	{
		u := c.baseURL
		u.Path = fmt.Sprintf("_config/admins/%s", c.username)
		req, err := http.NewRequest("PUT", u.String(), strings.NewReader(fmt.Sprintf(`"%s"`, c.password)))
		if err != nil {
			return nil, fmt.Errorf("Could not create admin user request: %q", err)
		}

		resp, err := http.DefaultClient.Do(req)
		if err != nil {
			return nil, fmt.Errorf("Could not create admin user: %q", err)
		}
		defer resp.Body.Close()
		if resp.StatusCode != http.StatusOK {
			bs, _ := io.ReadAll(resp.Body)
			return nil, fmt.Errorf("Failed to create admin user: %s %s\n", resp.Status, string(bs))
		}
	}

	return c, nil
}
