package consul

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

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

type Config struct {
	docker.ServiceHostPort
	Token string
}

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

// PrepareTestContainer creates a Consul docker container.  If version is empty,
// the Consul version used will be given by the environment variable
// CONSUL_DOCKER_VERSION, or if that's empty, whatever we've hardcoded as the
// the latest Consul version.
func PrepareTestContainer(t *testing.T, version string, isEnterprise bool, doBootstrapSetup bool) (func(), *Config) {
	t.Helper()

	if retAddress := os.Getenv("CONSUL_HTTP_ADDR"); retAddress != "" {
		shp, err := docker.NewServiceHostPortParse(retAddress)
		if err != nil {
			t.Fatal(err)
		}
		return func() {}, &Config{ServiceHostPort: *shp, Token: os.Getenv("CONSUL_HTTP_TOKEN")}
	}

	config := `acl { enabled = true default_policy = "deny" }`
	if version == "" {
		consulVersion := os.Getenv("CONSUL_DOCKER_VERSION")
		if consulVersion != "" {
			version = consulVersion
		} else {
			version = "1.11.3" // Latest Consul version, update as new releases come out
		}
	}
	if strings.HasPrefix(version, "1.3") {
		config = `datacenter = "test" acl_default_policy = "deny" acl_datacenter = "test" acl_master_token = "test"`
	}

	name := "consul"
	repo := "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 {
			t.Fatalf("Failed to find enterprise license")
		}
	}

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

	runner, err := docker.NewServiceRunner(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"),
	})
	if err != nil {
		t.Fatalf("Could not start docker Consul: %s", err)
	}

	svc, err := runner.StartService(context.Background(), 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 parition 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 {
		t.Fatalf("Could not start docker Consul: %s", err)
	}

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