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

package consul

import (
	"context"
	"fmt"
	"os"
	"strings"
	"testing"

	consulapi "github.com/hashicorp/consul/api"
	goversion "github.com/hashicorp/go-version"
	"github.com/hashicorp/vault/sdk/helper/docker"
)

// LatestConsulVersion is the most recent version of Consul which is used unless
// another version is specified in the test config or environment. This will
// probably go stale as we don't always update it on every release but we rarely
// rely on specific new Consul functionality so that's probably not a problem.
const LatestConsulVersion = "1.15.3"

type Config struct {
	docker.ServiceHostPort
	Token             string
	ContainerHTTPAddr string
}

func (c *Config) APIConfig() *consulapi.Config {
	apiConfig := consulapi.DefaultConfig()
	apiConfig.Address = c.Address()
	apiConfig.Token = c.Token
	return apiConfig
}

// PrepareTestContainer is a test helper that creates a Consul docker container
// or fails the test if unsuccessful. See RunContainer for more details on the
// configuration.
func PrepareTestContainer(t *testing.T, version string, isEnterprise bool, doBootstrapSetup bool) (func(), *Config) {
	t.Helper()

	cleanup, config, err := RunContainer(context.Background(), "", version, isEnterprise, doBootstrapSetup)
	if err != nil {
		t.Fatalf("failed starting consul: %s", err)
	}
	return cleanup, config
}

// RunContainer runs Consul in a Docker container unless CONSUL_HTTP_ADDR is
// already found in the environment. Consul version is determined by the version
// argument. If version is empty string, the CONSUL_DOCKER_VERSION environment
// variable is used and if that is empty too, LatestConsulVersion is used
// (defined above). If namePrefix is provided we assume you have chosen a unique
// enough prefix to avoid collision with other tests that may be running in
// parallel and so _do not_ add an additional unique ID suffix. We will also
// ensure previous instances are deleted and leave the container running for
// debugging. This is useful for using Consul as part of at testcluster (i.e.
// when Vault is in Docker too). If namePrefix is empty then a unique suffix is
// added since many older tests rely on a uniq instance of the container. This
// is used by `PrepareTestContainer` which is used typically in tests that rely
// on Consul but run tested code within the test process.
func RunContainer(ctx context.Context, namePrefix, version string, isEnterprise bool, doBootstrapSetup bool) (func(), *Config, error) {
	if retAddress := os.Getenv("CONSUL_HTTP_ADDR"); retAddress != "" {
		shp, err := docker.NewServiceHostPortParse(retAddress)
		if err != nil {
			return nil, nil, err
		}
		return func() {}, &Config{ServiceHostPort: *shp, Token: os.Getenv("CONSUL_HTTP_TOKEN")}, nil
	}

	config := `acl { enabled = true default_policy = "deny" }`
	if version == "" {
		consulVersion := os.Getenv("CONSUL_DOCKER_VERSION")
		if consulVersion != "" {
			version = consulVersion
		} else {
			version = LatestConsulVersion
		}
	}
	if strings.HasPrefix(version, "1.3") {
		config = `datacenter = "test" acl_default_policy = "deny" acl_datacenter = "test" acl_master_token = "test"`
	}

	name := "consul"
	repo := "docker.mirror.hashicorp.services/library/consul"
	var envVars []string
	// If running the enterprise container, set the appropriate values below.
	if isEnterprise {
		version += "-ent"
		name = "consul-enterprise"
		repo = "docker.mirror.hashicorp.services/hashicorp/consul-enterprise"
		license, hasLicense := os.LookupEnv("CONSUL_LICENSE")
		envVars = append(envVars, "CONSUL_LICENSE="+license)

		if !hasLicense {
			return nil, nil, fmt.Errorf("Failed to find enterprise license")
		}
	}
	if namePrefix != "" {
		name = namePrefix + name
	}

	if dockerRepo, hasEnvRepo := os.LookupEnv("CONSUL_DOCKER_REPO"); hasEnvRepo {
		repo = dockerRepo
	}

	dockerOpts := docker.RunOptions{
		ContainerName: name,
		ImageRepo:     repo,
		ImageTag:      version,
		Env:           envVars,
		Cmd:           []string{"agent", "-dev", "-client", "0.0.0.0", "-hcl", config},
		Ports:         []string{"8500/tcp"},
		AuthUsername:  os.Getenv("CONSUL_DOCKER_USERNAME"),
		AuthPassword:  os.Getenv("CONSUL_DOCKER_PASSWORD"),
	}

	// Add a unique suffix if there is no per-test prefix provided
	addSuffix := true
	if namePrefix != "" {
		// Don't add a suffix if the caller already provided a prefix
		addSuffix = false
		// Also enable predelete and non-removal to make debugging easier for test
		// cases with named containers).
		dockerOpts.PreDelete = true
		dockerOpts.DoNotAutoRemove = true
	}

	runner, err := docker.NewServiceRunner(dockerOpts)
	if err != nil {
		return nil, nil, fmt.Errorf("Could not start docker Consul: %s", err)
	}

	svc, _, err := runner.StartNewService(ctx, addSuffix, false, func(ctx context.Context, host string, port int) (docker.ServiceConfig, error) {
		shp := docker.NewServiceHostPort(host, port)
		apiConfig := consulapi.DefaultNonPooledConfig()
		apiConfig.Address = shp.Address()
		consul, err := consulapi.NewClient(apiConfig)
		if err != nil {
			return nil, err
		}

		// Make sure Consul is up
		if _, err = consul.Status().Leader(); err != nil {
			return nil, err
		}

		// For version of Consul < 1.4
		if strings.HasPrefix(version, "1.3") {
			consulToken := "test"
			_, err = consul.KV().Put(&consulapi.KVPair{
				Key:   "setuptest",
				Value: []byte("setuptest"),
			}, &consulapi.WriteOptions{
				Token: consulToken,
			})
			if err != nil {
				return nil, err
			}
			return &Config{
				ServiceHostPort: *shp,
				Token:           consulToken,
			}, nil
		}

		// New default behavior
		var consulToken string
		if doBootstrapSetup {
			aclbootstrap, _, err := consul.ACL().Bootstrap()
			if err != nil {
				return nil, err
			}
			consulToken = aclbootstrap.SecretID
			policy := &consulapi.ACLPolicy{
				Name:        "test",
				Description: "test",
				Rules: `node_prefix "" {
					policy = "write"
				}

				service_prefix "" {
					policy = "read"
				}`,
			}
			q := &consulapi.WriteOptions{
				Token: consulToken,
			}
			_, _, err = consul.ACL().PolicyCreate(policy, q)
			if err != nil {
				return nil, err
			}

			// Create a Consul role that contains the test policy, for Consul 1.5 and newer
			currVersion, _ := goversion.NewVersion(version)
			roleVersion, _ := goversion.NewVersion("1.5")
			if currVersion.GreaterThanOrEqual(roleVersion) {
				ACLList := []*consulapi.ACLTokenRoleLink{{Name: "test"}}

				role := &consulapi.ACLRole{
					Name:        "role-test",
					Description: "consul roles test",
					Policies:    ACLList,
				}

				_, _, err = consul.ACL().RoleCreate(role, q)
				if err != nil {
					return nil, err
				}
			}

			// Configure a namespace and partition if testing enterprise Consul
			if isEnterprise {
				// Namespaces require Consul 1.7 or newer
				namespaceVersion, _ := goversion.NewVersion("1.7")
				if currVersion.GreaterThanOrEqual(namespaceVersion) {
					namespace := &consulapi.Namespace{
						Name:        "ns1",
						Description: "ns1 test",
					}

					_, _, err = consul.Namespaces().Create(namespace, q)
					if err != nil {
						return nil, err
					}

					nsPolicy := &consulapi.ACLPolicy{
						Name:        "ns-test",
						Description: "namespace test",
						Namespace:   "ns1",
						Rules: `service_prefix "" {
							policy = "read"
						}`,
					}
					_, _, err = consul.ACL().PolicyCreate(nsPolicy, q)
					if err != nil {
						return nil, err
					}
				}

				// Partitions require Consul 1.11 or newer
				partitionVersion, _ := goversion.NewVersion("1.11")
				if currVersion.GreaterThanOrEqual(partitionVersion) {
					partition := &consulapi.Partition{
						Name:        "part1",
						Description: "part1 test",
					}

					_, _, err = consul.Partitions().Create(ctx, partition, q)
					if err != nil {
						return nil, err
					}

					partPolicy := &consulapi.ACLPolicy{
						Name:        "part-test",
						Description: "partition test",
						Partition:   "part1",
						Rules: `service_prefix "" {
							policy = "read"
						}`,
					}
					_, _, err = consul.ACL().PolicyCreate(partPolicy, q)
					if err != nil {
						return nil, err
					}
				}
			}
		}

		return &Config{
			ServiceHostPort: *shp,
			Token:           consulToken,
		}, nil
	})
	if err != nil {
		return nil, nil, err
	}

	// Find the container network info.
	if len(svc.Container.NetworkSettings.Networks) < 1 {
		svc.Cleanup()
		return nil, nil, fmt.Errorf("failed to find any network settings for container")
	}
	cfg := svc.Config.(*Config)
	for _, eps := range svc.Container.NetworkSettings.Networks {
		// Just pick the first network, we assume only one for now.
		// Pull out the real container IP and set that up
		cfg.ContainerHTTPAddr = fmt.Sprintf("http://%s:8500", eps.IPAddress)
		break
	}
	return svc.Cleanup, cfg, nil
}
