package agent

import (
	"fmt"
	"io/ioutil"
	"net"
	"os"
	"path/filepath"
	"reflect"
	"testing"
	"time"

	"github.com/hashicorp/consul/lib/freeport"
	"github.com/hashicorp/nomad/helper"
	"github.com/hashicorp/nomad/nomad/structs"
	"github.com/hashicorp/nomad/nomad/structs/config"
	"github.com/stretchr/testify/require"
)

var (
	// trueValue/falseValue are used to get a pointer to a boolean
	trueValue  = true
	falseValue = false
)

func TestConfig_Merge(t *testing.T) {
	c0 := &Config{}

	c1 := &Config{
		Telemetry:      &Telemetry{},
		Client:         &ClientConfig{},
		Server:         &ServerConfig{},
		ACL:            &ACLConfig{},
		Ports:          &Ports{},
		Addresses:      &Addresses{},
		AdvertiseAddrs: &AdvertiseAddrs{},
		Vault:          &config.VaultConfig{},
		Consul:         &config.ConsulConfig{},
		Sentinel:       &config.SentinelConfig{},
		Autopilot:      &config.AutopilotConfig{},
	}

	c2 := &Config{
		Region:                    "global",
		Datacenter:                "dc1",
		NodeName:                  "node1",
		DataDir:                   "/tmp/dir1",
		LogLevel:                  "INFO",
		EnableDebug:               false,
		LeaveOnInt:                false,
		LeaveOnTerm:               false,
		EnableSyslog:              false,
		SyslogFacility:            "local0.info",
		DisableUpdateCheck:        helper.BoolToPtr(false),
		DisableAnonymousSignature: false,
		BindAddr:                  "127.0.0.1",
		Telemetry: &Telemetry{
			StatsiteAddr:                       "127.0.0.1:8125",
			StatsdAddr:                         "127.0.0.1:8125",
			DataDogAddr:                        "127.0.0.1:8125",
			DataDogTags:                        []string{"cat1:tag1", "cat2:tag2"},
			PrometheusMetrics:                  true,
			DisableHostname:                    false,
			DisableTaggedMetrics:               true,
			BackwardsCompatibleMetrics:         true,
			CirconusAPIToken:                   "0",
			CirconusAPIApp:                     "nomadic",
			CirconusAPIURL:                     "http://api.circonus.com/v2",
			CirconusSubmissionInterval:         "60s",
			CirconusCheckSubmissionURL:         "https://someplace.com/metrics",
			CirconusCheckID:                    "0",
			CirconusCheckForceMetricActivation: "true",
			CirconusCheckInstanceID:            "node1:nomadic",
			CirconusCheckSearchTag:             "service:nomadic",
			CirconusCheckDisplayName:           "node1:nomadic",
			CirconusCheckTags:                  "cat1:tag1,cat2:tag2",
			CirconusBrokerID:                   "0",
			CirconusBrokerSelectTag:            "dc:dc1",
			PrefixFilter:                       []string{"filter1", "filter2"},
		},
		Client: &ClientConfig{
			Enabled:   false,
			StateDir:  "/tmp/state1",
			AllocDir:  "/tmp/alloc1",
			NodeClass: "class1",
			Options: map[string]string{
				"foo": "bar",
			},
			NetworkSpeed:   100,
			CpuCompute:     100,
			MemoryMB:       100,
			MaxKillTimeout: "20s",
			ClientMaxPort:  19996,
			Reserved: &Resources{
				CPU:                 10,
				MemoryMB:            10,
				DiskMB:              10,
				IOPS:                10,
				ReservedPorts:       "1,10-30,55",
				ParsedReservedPorts: []int{1, 2, 4},
			},
		},
		Server: &ServerConfig{
			Enabled:                false,
			AuthoritativeRegion:    "global",
			BootstrapExpect:        1,
			DataDir:                "/tmp/data1",
			ProtocolVersion:        1,
			RaftProtocol:           1,
			NumSchedulers:          helper.IntToPtr(1),
			NodeGCThreshold:        "1h",
			HeartbeatGrace:         30 * time.Second,
			MinHeartbeatTTL:        30 * time.Second,
			MaxHeartbeatsPerSecond: 30.0,
			RedundancyZone:         "foo",
			UpgradeVersion:         "foo",
		},
		ACL: &ACLConfig{
			Enabled:          true,
			TokenTTL:         60 * time.Second,
			PolicyTTL:        60 * time.Second,
			ReplicationToken: "foo",
		},
		Ports: &Ports{
			HTTP: 4646,
			RPC:  4647,
			Serf: 4648,
		},
		Addresses: &Addresses{
			HTTP: "127.0.0.1",
			RPC:  "127.0.0.1",
			Serf: "127.0.0.1",
		},
		AdvertiseAddrs: &AdvertiseAddrs{
			RPC:  "127.0.0.1",
			Serf: "127.0.0.1",
		},
		HTTPAPIResponseHeaders: map[string]string{
			"Access-Control-Allow-Origin": "*",
		},
		Vault: &config.VaultConfig{
			Token:                "1",
			AllowUnauthenticated: &falseValue,
			TaskTokenTTL:         "1",
			Addr:                 "1",
			TLSCaFile:            "1",
			TLSCaPath:            "1",
			TLSCertFile:          "1",
			TLSKeyFile:           "1",
			TLSSkipVerify:        &falseValue,
			TLSServerName:        "1",
		},
		Consul: &config.ConsulConfig{
			ServerServiceName:  "1",
			ClientServiceName:  "1",
			AutoAdvertise:      &falseValue,
			Addr:               "1",
			Timeout:            1 * time.Second,
			Token:              "1",
			Auth:               "1",
			EnableSSL:          &falseValue,
			VerifySSL:          &falseValue,
			CAFile:             "1",
			CertFile:           "1",
			KeyFile:            "1",
			ServerAutoJoin:     &falseValue,
			ClientAutoJoin:     &falseValue,
			ChecksUseAdvertise: &falseValue,
		},
		Autopilot: &config.AutopilotConfig{
			CleanupDeadServers:      &falseValue,
			ServerStabilizationTime: 1 * time.Second,
			LastContactThreshold:    1 * time.Second,
			MaxTrailingLogs:         1,
			EnableRedundancyZones:   &falseValue,
			DisableUpgradeMigration: &falseValue,
			EnableCustomUpgrades:    &falseValue,
		},
	}

	c3 := &Config{
		Region:                    "region2",
		Datacenter:                "dc2",
		NodeName:                  "node2",
		DataDir:                   "/tmp/dir2",
		LogLevel:                  "DEBUG",
		EnableDebug:               true,
		LeaveOnInt:                true,
		LeaveOnTerm:               true,
		EnableSyslog:              true,
		SyslogFacility:            "local0.debug",
		DisableUpdateCheck:        helper.BoolToPtr(true),
		DisableAnonymousSignature: true,
		BindAddr:                  "127.0.0.2",
		Telemetry: &Telemetry{
			StatsiteAddr:                       "127.0.0.2:8125",
			StatsdAddr:                         "127.0.0.2:8125",
			DataDogAddr:                        "127.0.0.1:8125",
			DataDogTags:                        []string{"cat1:tag1", "cat2:tag2"},
			PrometheusMetrics:                  true,
			DisableHostname:                    true,
			PublishNodeMetrics:                 true,
			PublishAllocationMetrics:           true,
			DisableTaggedMetrics:               true,
			BackwardsCompatibleMetrics:         true,
			CirconusAPIToken:                   "1",
			CirconusAPIApp:                     "nomad",
			CirconusAPIURL:                     "https://api.circonus.com/v2",
			CirconusSubmissionInterval:         "10s",
			CirconusCheckSubmissionURL:         "https://example.com/metrics",
			CirconusCheckID:                    "1",
			CirconusCheckForceMetricActivation: "false",
			CirconusCheckInstanceID:            "node2:nomad",
			CirconusCheckSearchTag:             "service:nomad",
			CirconusCheckDisplayName:           "node2:nomad",
			CirconusCheckTags:                  "cat1:tag1,cat2:tag2",
			CirconusBrokerID:                   "1",
			CirconusBrokerSelectTag:            "dc:dc2",
			PrefixFilter:                       []string{"prefix1", "prefix2"},
			DisableDispatchedJobSummaryMetrics: true,
			FilterDefault:                      helper.BoolToPtr(false),
		},
		Client: &ClientConfig{
			Enabled:   true,
			StateDir:  "/tmp/state2",
			AllocDir:  "/tmp/alloc2",
			NodeClass: "class2",
			Servers:   []string{"server2"},
			Meta: map[string]string{
				"baz": "zip",
			},
			Options: map[string]string{
				"foo": "bar",
				"baz": "zip",
			},
			ChrootEnv:      map[string]string{},
			ClientMaxPort:  20000,
			ClientMinPort:  22000,
			NetworkSpeed:   105,
			CpuCompute:     105,
			MemoryMB:       105,
			MaxKillTimeout: "50s",
			Reserved: &Resources{
				CPU:                 15,
				MemoryMB:            15,
				DiskMB:              15,
				IOPS:                15,
				ReservedPorts:       "2,10-30,55",
				ParsedReservedPorts: []int{1, 2, 3},
			},
			GCInterval:            6 * time.Second,
			GCParallelDestroys:    6,
			GCDiskUsageThreshold:  71,
			GCInodeUsageThreshold: 86,
		},
		Server: &ServerConfig{
			Enabled:                true,
			AuthoritativeRegion:    "global2",
			BootstrapExpect:        2,
			DataDir:                "/tmp/data2",
			ProtocolVersion:        2,
			RaftProtocol:           2,
			NumSchedulers:          helper.IntToPtr(2),
			EnabledSchedulers:      []string{structs.JobTypeBatch},
			NodeGCThreshold:        "12h",
			HeartbeatGrace:         2 * time.Minute,
			MinHeartbeatTTL:        2 * time.Minute,
			MaxHeartbeatsPerSecond: 200.0,
			RejoinAfterLeave:       true,
			StartJoin:              []string{"1.1.1.1"},
			RetryJoin:              []string{"1.1.1.1"},
			RetryInterval:          time.Second * 10,
			NonVotingServer:        true,
			RedundancyZone:         "bar",
			UpgradeVersion:         "bar",
		},
		ACL: &ACLConfig{
			Enabled:          true,
			TokenTTL:         20 * time.Second,
			PolicyTTL:        20 * time.Second,
			ReplicationToken: "foobar",
		},
		Ports: &Ports{
			HTTP: 20000,
			RPC:  21000,
			Serf: 22000,
		},
		Addresses: &Addresses{
			HTTP: "127.0.0.2",
			RPC:  "127.0.0.2",
			Serf: "127.0.0.2",
		},
		AdvertiseAddrs: &AdvertiseAddrs{
			RPC:  "127.0.0.2",
			Serf: "127.0.0.2",
		},
		HTTPAPIResponseHeaders: map[string]string{
			"Access-Control-Allow-Origin":  "*",
			"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
		},
		Vault: &config.VaultConfig{
			Token:                "2",
			AllowUnauthenticated: &trueValue,
			TaskTokenTTL:         "2",
			Addr:                 "2",
			TLSCaFile:            "2",
			TLSCaPath:            "2",
			TLSCertFile:          "2",
			TLSKeyFile:           "2",
			TLSSkipVerify:        &trueValue,
			TLSServerName:        "2",
		},
		Consul: &config.ConsulConfig{
			ServerServiceName:  "2",
			ClientServiceName:  "2",
			AutoAdvertise:      &trueValue,
			Addr:               "2",
			Timeout:            2 * time.Second,
			Token:              "2",
			Auth:               "2",
			EnableSSL:          &trueValue,
			VerifySSL:          &trueValue,
			CAFile:             "2",
			CertFile:           "2",
			KeyFile:            "2",
			ServerAutoJoin:     &trueValue,
			ClientAutoJoin:     &trueValue,
			ChecksUseAdvertise: &trueValue,
		},
		Sentinel: &config.SentinelConfig{
			Imports: []*config.SentinelImport{
				{
					Name: "foo",
					Path: "foo",
					Args: []string{"a", "b", "c"},
				},
			},
		},
		Autopilot: &config.AutopilotConfig{
			CleanupDeadServers:      &trueValue,
			ServerStabilizationTime: 2 * time.Second,
			LastContactThreshold:    2 * time.Second,
			MaxTrailingLogs:         2,
			EnableRedundancyZones:   &trueValue,
			DisableUpgradeMigration: &trueValue,
			EnableCustomUpgrades:    &trueValue,
		},
	}

	result := c0.Merge(c1)
	result = result.Merge(c2)
	result = result.Merge(c3)
	if !reflect.DeepEqual(result, c3) {
		t.Fatalf("bad:\n%#v\n%#v", result, c3)
	}
}

func TestConfig_ParseConfigFile(t *testing.T) {
	// Fails if the file doesn't exist
	if _, err := ParseConfigFile("/unicorns/leprechauns"); err == nil {
		t.Fatalf("expected error, got nothing")
	}

	fh, err := ioutil.TempFile("", "nomad")
	if err != nil {
		t.Fatalf("err: %s", err)
	}
	defer os.RemoveAll(fh.Name())

	// Invalid content returns error
	if _, err := fh.WriteString("nope;!!!"); err != nil {
		t.Fatalf("err: %s", err)
	}
	if _, err := ParseConfigFile(fh.Name()); err == nil {
		t.Fatalf("expected load error, got nothing")
	}

	// Valid content parses successfully
	if err := fh.Truncate(0); err != nil {
		t.Fatalf("err: %s", err)
	}
	if _, err := fh.Seek(0, 0); err != nil {
		t.Fatalf("err: %s", err)
	}
	if _, err := fh.WriteString(`{"region":"west"}`); err != nil {
		t.Fatalf("err: %s", err)
	}

	config, err := ParseConfigFile(fh.Name())
	if err != nil {
		t.Fatalf("err: %s", err)
	}
	if config.Region != "west" {
		t.Fatalf("bad region: %q", config.Region)
	}
}

func TestConfig_LoadConfigDir(t *testing.T) {
	// Fails if the dir doesn't exist.
	if _, err := LoadConfigDir("/unicorns/leprechauns"); err == nil {
		t.Fatalf("expected error, got nothing")
	}

	dir, err := ioutil.TempDir("", "nomad")
	if err != nil {
		t.Fatalf("err: %s", err)
	}
	defer os.RemoveAll(dir)

	// Returns empty config on empty dir
	config, err := LoadConfig(dir)
	if err != nil {
		t.Fatalf("err: %s", err)
	}
	if config == nil {
		t.Fatalf("should not be nil")
	}

	file1 := filepath.Join(dir, "conf1.hcl")
	err = ioutil.WriteFile(file1, []byte(`{"region":"west"}`), 0600)
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	file2 := filepath.Join(dir, "conf2.hcl")
	err = ioutil.WriteFile(file2, []byte(`{"datacenter":"sfo"}`), 0600)
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	file3 := filepath.Join(dir, "conf3.hcl")
	err = ioutil.WriteFile(file3, []byte(`nope;!!!`), 0600)
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	// Fails if we have a bad config file
	if _, err := LoadConfigDir(dir); err == nil {
		t.Fatalf("expected load error, got nothing")
	}

	if err := os.Remove(file3); err != nil {
		t.Fatalf("err: %s", err)
	}

	// Works if configs are valid
	config, err = LoadConfigDir(dir)
	if err != nil {
		t.Fatalf("err: %s", err)
	}
	if config.Region != "west" || config.Datacenter != "sfo" {
		t.Fatalf("bad: %#v", config)
	}
}

func TestConfig_LoadConfig(t *testing.T) {
	// Fails if the target doesn't exist
	if _, err := LoadConfig("/unicorns/leprechauns"); err == nil {
		t.Fatalf("expected error, got nothing")
	}

	fh, err := ioutil.TempFile("", "nomad")
	if err != nil {
		t.Fatalf("err: %s", err)
	}
	defer os.Remove(fh.Name())

	if _, err := fh.WriteString(`{"region":"west"}`); err != nil {
		t.Fatalf("err: %s", err)
	}

	// Works on a config file
	config, err := LoadConfig(fh.Name())
	if err != nil {
		t.Fatalf("err: %s", err)
	}
	if config.Region != "west" {
		t.Fatalf("bad: %#v", config)
	}

	expectedConfigFiles := []string{fh.Name()}
	if !reflect.DeepEqual(config.Files, expectedConfigFiles) {
		t.Errorf("Loaded configs don't match\nExpected\n%+vGot\n%+v\n",
			expectedConfigFiles, config.Files)
	}

	dir, err := ioutil.TempDir("", "nomad")
	if err != nil {
		t.Fatalf("err: %s", err)
	}
	defer os.RemoveAll(dir)

	file1 := filepath.Join(dir, "config1.hcl")
	err = ioutil.WriteFile(file1, []byte(`{"datacenter":"sfo"}`), 0600)
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	// Works on config dir
	config, err = LoadConfig(dir)
	if err != nil {
		t.Fatalf("err: %s", err)
	}
	if config.Datacenter != "sfo" {
		t.Fatalf("bad: %#v", config)
	}

	expectedConfigFiles = []string{file1}
	if !reflect.DeepEqual(config.Files, expectedConfigFiles) {
		t.Errorf("Loaded configs don't match\nExpected\n%+vGot\n%+v\n",
			expectedConfigFiles, config.Files)
	}
}

func TestConfig_LoadConfigsFileOrder(t *testing.T) {
	config1, err := LoadConfigDir("test-resources/etcnomad")
	if err != nil {
		t.Fatalf("Failed to load config: %s", err)
	}

	config2, err := LoadConfig("test-resources/myconf")
	if err != nil {
		t.Fatalf("Failed to load config: %s", err)
	}

	expected := []string{
		// filepath.FromSlash changes these to backslash \ on Windows
		filepath.FromSlash("test-resources/etcnomad/common.hcl"),
		filepath.FromSlash("test-resources/etcnomad/server.json"),
		filepath.FromSlash("test-resources/myconf"),
	}

	config := config1.Merge(config2)

	if !reflect.DeepEqual(config.Files, expected) {
		t.Errorf("Loaded configs don't match\nwant: %+v\n got: %+v\n",
			expected, config.Files)
	}
}

func TestConfig_Listener(t *testing.T) {
	config := DefaultConfig()

	// Fails on invalid input
	if ln, err := config.Listener("tcp", "nope", 8080); err == nil {
		ln.Close()
		t.Fatalf("expected addr error")
	}
	if ln, err := config.Listener("nope", "127.0.0.1", 8080); err == nil {
		ln.Close()
		t.Fatalf("expected protocol err")
	}
	if ln, err := config.Listener("tcp", "127.0.0.1", -1); err == nil {
		ln.Close()
		t.Fatalf("expected port error")
	}

	// Works with valid inputs
	ports := freeport.GetT(t, 2)
	ln, err := config.Listener("tcp", "127.0.0.1", ports[0])
	if err != nil {
		t.Fatalf("err: %s", err)
	}
	ln.Close()

	if net := ln.Addr().Network(); net != "tcp" {
		t.Fatalf("expected tcp, got: %q", net)
	}
	want := fmt.Sprintf("127.0.0.1:%d", ports[0])
	if addr := ln.Addr().String(); addr != want {
		t.Fatalf("expected %q, got: %q", want, addr)
	}

	// Falls back to default bind address if non provided
	config.BindAddr = "0.0.0.0"
	ln, err = config.Listener("tcp4", "", ports[1])
	if err != nil {
		t.Fatalf("err: %s", err)
	}
	ln.Close()

	want = fmt.Sprintf("0.0.0.0:%d", ports[1])
	if addr := ln.Addr().String(); addr != want {
		t.Fatalf("expected %q, got: %q", want, addr)
	}
}

// TestConfig_normalizeAddrs_DevMode asserts that normalizeAddrs allows
// advertising localhost in dev mode.
func TestConfig_normalizeAddrs_DevMode(t *testing.T) {
	// allow to advertise 127.0.0.1 if dev-mode is enabled
	c := &Config{
		BindAddr: "127.0.0.1",
		Ports: &Ports{
			HTTP: 4646,
			RPC:  4647,
			Serf: 4648,
		},
		Addresses:      &Addresses{},
		AdvertiseAddrs: &AdvertiseAddrs{},
		DevMode:        true,
	}

	if err := c.normalizeAddrs(); err != nil {
		t.Fatalf("unable to normalize addresses: %s", err)
	}

	if c.BindAddr != "127.0.0.1" {
		t.Fatalf("expected BindAddr 127.0.0.1, got %s", c.BindAddr)
	}

	if c.normalizedAddrs.HTTP != "127.0.0.1:4646" {
		t.Fatalf("expected HTTP address 127.0.0.1:4646, got %s", c.normalizedAddrs.HTTP)
	}

	if c.normalizedAddrs.RPC != "127.0.0.1:4647" {
		t.Fatalf("expected RPC address 127.0.0.1:4647, got %s", c.normalizedAddrs.RPC)
	}

	if c.normalizedAddrs.Serf != "127.0.0.1:4648" {
		t.Fatalf("expected Serf address 127.0.0.1:4648, got %s", c.normalizedAddrs.Serf)
	}

	if c.AdvertiseAddrs.HTTP != "127.0.0.1:4646" {
		t.Fatalf("expected HTTP advertise address 127.0.0.1:4646, got %s", c.AdvertiseAddrs.HTTP)
	}

	if c.AdvertiseAddrs.RPC != "127.0.0.1:4647" {
		t.Fatalf("expected RPC advertise address 127.0.0.1:4647, got %s", c.AdvertiseAddrs.RPC)
	}

	// Client mode, no Serf address defined
	if c.AdvertiseAddrs.Serf != "" {
		t.Fatalf("expected unset Serf advertise address, got %s", c.AdvertiseAddrs.Serf)
	}
}

// TestConfig_normalizeAddrs_NoAdvertise asserts that normalizeAddrs will
// fail if no valid advertise address available in non-dev mode.
func TestConfig_normalizeAddrs_NoAdvertise(t *testing.T) {
	c := &Config{
		BindAddr: "127.0.0.1",
		Ports: &Ports{
			HTTP: 4646,
			RPC:  4647,
			Serf: 4648,
		},
		Addresses:      &Addresses{},
		AdvertiseAddrs: &AdvertiseAddrs{},
		DevMode:        false,
	}

	if err := c.normalizeAddrs(); err == nil {
		t.Fatalf("expected an error when no valid advertise address is available")
	}

	if c.AdvertiseAddrs.HTTP == "127.0.0.1:4646" {
		t.Fatalf("expected non-localhost HTTP advertise address, got %s", c.AdvertiseAddrs.HTTP)
	}

	if c.AdvertiseAddrs.RPC == "127.0.0.1:4647" {
		t.Fatalf("expected non-localhost RPC advertise address, got %s", c.AdvertiseAddrs.RPC)
	}

	if c.AdvertiseAddrs.Serf == "127.0.0.1:4648" {
		t.Fatalf("expected non-localhost Serf advertise address, got %s", c.AdvertiseAddrs.Serf)
	}
}

// TestConfig_normalizeAddrs_AdvertiseLocalhost asserts localhost can be
// advertised if it's explicitly set in the config.
func TestConfig_normalizeAddrs_AdvertiseLocalhost(t *testing.T) {
	c := &Config{
		BindAddr: "127.0.0.1",
		Ports: &Ports{
			HTTP: 4646,
			RPC:  4647,
			Serf: 4648,
		},
		Addresses: &Addresses{},
		AdvertiseAddrs: &AdvertiseAddrs{
			HTTP: "127.0.0.1",
			RPC:  "127.0.0.1",
			Serf: "127.0.0.1",
		},
		DevMode: false,
		Server:  &ServerConfig{Enabled: true},
	}

	if err := c.normalizeAddrs(); err != nil {
		t.Fatalf("unexpected error when manually setting bind mode: %v", err)
	}

	if c.AdvertiseAddrs.HTTP != "127.0.0.1:4646" {
		t.Errorf("expected localhost HTTP advertise address, got %s", c.AdvertiseAddrs.HTTP)
	}

	if c.AdvertiseAddrs.RPC != "127.0.0.1:4647" {
		t.Errorf("expected localhost RPC advertise address, got %s", c.AdvertiseAddrs.RPC)
	}

	if c.AdvertiseAddrs.Serf != "127.0.0.1:4648" {
		t.Errorf("expected localhost Serf advertise address, got %s", c.AdvertiseAddrs.Serf)
	}
}

// TestConfig_normalizeAddrs_IPv6Loopback asserts that an IPv6 loopback address
// is normalized properly. See #2739
func TestConfig_normalizeAddrs_IPv6Loopback(t *testing.T) {
	c := &Config{
		BindAddr: "::1",
		Ports: &Ports{
			HTTP: 4646,
			RPC:  4647,
		},
		Addresses: &Addresses{},
		AdvertiseAddrs: &AdvertiseAddrs{
			HTTP: "::1",
			RPC:  "::1",
		},
		DevMode: false,
	}

	if err := c.normalizeAddrs(); err != nil {
		t.Fatalf("unexpected error when manually setting bind mode: %v", err)
	}

	if c.Addresses.HTTP != "::1" {
		t.Errorf("expected ::1 HTTP address, got %s", c.Addresses.HTTP)
	}

	if c.Addresses.RPC != "::1" {
		t.Errorf("expected ::1 RPC address, got %s", c.Addresses.RPC)
	}

	if c.AdvertiseAddrs.HTTP != "[::1]:4646" {
		t.Errorf("expected [::1] HTTP advertise address, got %s", c.AdvertiseAddrs.HTTP)
	}

	if c.AdvertiseAddrs.RPC != "[::1]:4647" {
		t.Errorf("expected [::1] RPC advertise address, got %s", c.AdvertiseAddrs.RPC)
	}
}

func TestConfig_normalizeAddrs(t *testing.T) {
	c := &Config{
		BindAddr: "169.254.1.5",
		Ports: &Ports{
			HTTP: 4646,
			RPC:  4647,
			Serf: 4648,
		},
		Addresses: &Addresses{
			HTTP: "169.254.1.10",
		},
		AdvertiseAddrs: &AdvertiseAddrs{
			RPC: "169.254.1.40",
		},
		Server: &ServerConfig{
			Enabled: true,
		},
	}

	if err := c.normalizeAddrs(); err != nil {
		t.Fatalf("unable to normalize addresses: %s", err)
	}

	if c.BindAddr != "169.254.1.5" {
		t.Fatalf("expected BindAddr 169.254.1.5, got %s", c.BindAddr)
	}

	if c.AdvertiseAddrs.HTTP != "169.254.1.10:4646" {
		t.Fatalf("expected HTTP advertise address 169.254.1.10:4646, got %s", c.AdvertiseAddrs.HTTP)
	}

	if c.AdvertiseAddrs.RPC != "169.254.1.40:4647" {
		t.Fatalf("expected RPC advertise address 169.254.1.40:4647, got %s", c.AdvertiseAddrs.RPC)
	}

	if c.AdvertiseAddrs.Serf != "169.254.1.5:4648" {
		t.Fatalf("expected Serf advertise address 169.254.1.5:4648, got %s", c.AdvertiseAddrs.Serf)
	}

	c = &Config{
		BindAddr: "{{ GetPrivateIP }}",
		Ports: &Ports{
			HTTP: 4646,
			RPC:  4647,
			Serf: 4648,
		},
		Addresses: &Addresses{},
		AdvertiseAddrs: &AdvertiseAddrs{
			RPC: "{{ GetPrivateIP }}",
		},
		Server: &ServerConfig{
			Enabled: true,
		},
	}

	if err := c.normalizeAddrs(); err != nil {
		t.Fatalf("unable to normalize addresses: %s", err)
	}

	exp := net.JoinHostPort(c.BindAddr, "4646")
	if c.AdvertiseAddrs.HTTP != exp {
		t.Fatalf("expected HTTP advertise address %s, got %s", exp, c.AdvertiseAddrs.HTTP)
	}

	exp = net.JoinHostPort(c.BindAddr, "4647")
	if c.AdvertiseAddrs.RPC != exp {
		t.Fatalf("expected RPC advertise address %s, got %s", exp, c.AdvertiseAddrs.RPC)
	}

	exp = net.JoinHostPort(c.BindAddr, "4648")
	if c.AdvertiseAddrs.Serf != exp {
		t.Fatalf("expected Serf advertise address %s, got %s", exp, c.AdvertiseAddrs.Serf)
	}

	// allow to advertise 127.0.0.1 in non-dev mode, if explicitly configured to do so
	c = &Config{
		BindAddr: "127.0.0.1",
		Ports: &Ports{
			HTTP: 4646,
			RPC:  4647,
			Serf: 4648,
		},
		Addresses: &Addresses{},
		AdvertiseAddrs: &AdvertiseAddrs{
			HTTP: "127.0.0.1:4646",
			RPC:  "127.0.0.1:4647",
			Serf: "127.0.0.1:4648",
		},
		DevMode: false,
		Server: &ServerConfig{
			Enabled: true,
		},
	}

	if err := c.normalizeAddrs(); err != nil {
		t.Fatalf("unable to normalize addresses: %s", err)
	}

	if c.AdvertiseAddrs.HTTP != "127.0.0.1:4646" {
		t.Fatalf("expected HTTP advertise address 127.0.0.1:4646, got %s", c.AdvertiseAddrs.HTTP)
	}

	if c.AdvertiseAddrs.RPC != "127.0.0.1:4647" {
		t.Fatalf("expected RPC advertise address 127.0.0.1:4647, got %s", c.AdvertiseAddrs.RPC)
	}

	if c.AdvertiseAddrs.RPC != "127.0.0.1:4647" {
		t.Fatalf("expected RPC advertise address 127.0.0.1:4647, got %s", c.AdvertiseAddrs.RPC)
	}
}

func TestResources_ParseReserved(t *testing.T) {
	cases := []struct {
		Input  string
		Parsed []int
		Err    bool
	}{
		{
			"1,2,3",
			[]int{1, 2, 3},
			false,
		},
		{
			"3,1,2,1,2,3,1-3",
			[]int{1, 2, 3},
			false,
		},
		{
			"3-1",
			nil,
			true,
		},
		{
			"1-3,2-4",
			[]int{1, 2, 3, 4},
			false,
		},
		{
			"1-3,4,5-5,6,7,8-10",
			[]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
			false,
		},
	}

	for i, tc := range cases {
		r := &Resources{ReservedPorts: tc.Input}
		err := r.ParseReserved()
		if (err != nil) != tc.Err {
			t.Fatalf("test case %d: %v", i, err)
			continue
		}

		if !reflect.DeepEqual(r.ParsedReservedPorts, tc.Parsed) {
			t.Fatalf("test case %d: \n\n%#v\n\n%#v", i, r.ParsedReservedPorts, tc.Parsed)
		}

	}
}

func TestIsMissingPort(t *testing.T) {
	_, _, err := net.SplitHostPort("localhost")
	if missing := isMissingPort(err); !missing {
		t.Errorf("expected missing port error, but got %v", err)
	}
	_, _, err = net.SplitHostPort("localhost:9000")
	if missing := isMissingPort(err); missing {
		t.Errorf("expected no error, but got %v", err)
	}
}

func TestMergeServerJoin(t *testing.T) {
	require := require.New(t)

	{
		retryJoin := []string{"127.0.0.1", "127.0.0.2"}
		startJoin := []string{"127.0.0.1", "127.0.0.2"}
		retryMaxAttempts := 1
		retryInterval := time.Duration(0)

		a := &ServerJoin{
			RetryJoin:        retryJoin,
			StartJoin:        startJoin,
			RetryMaxAttempts: retryMaxAttempts,
			RetryInterval:    time.Duration(retryInterval),
		}
		b := &ServerJoin{}

		result := a.Merge(b)
		require.Equal(result.RetryJoin, retryJoin)
		require.Equal(result.StartJoin, startJoin)
		require.Equal(result.RetryMaxAttempts, retryMaxAttempts)
		require.Equal(result.RetryInterval, retryInterval)
	}
	{
		retryJoin := []string{"127.0.0.1", "127.0.0.2"}
		startJoin := []string{"127.0.0.1", "127.0.0.2"}
		retryMaxAttempts := 1
		retryInterval := time.Duration(0)

		a := &ServerJoin{}
		b := &ServerJoin{
			RetryJoin:        retryJoin,
			StartJoin:        startJoin,
			RetryMaxAttempts: retryMaxAttempts,
			RetryInterval:    time.Duration(retryInterval),
		}

		result := a.Merge(b)
		require.Equal(result.RetryJoin, retryJoin)
		require.Equal(result.StartJoin, startJoin)
		require.Equal(result.RetryMaxAttempts, retryMaxAttempts)
		require.Equal(result.RetryInterval, retryInterval)
	}
	{
		retryJoin := []string{"127.0.0.1", "127.0.0.2"}
		startJoin := []string{"127.0.0.1", "127.0.0.2"}
		retryMaxAttempts := 1
		retryInterval := time.Duration(0)

		var a *ServerJoin
		b := &ServerJoin{
			RetryJoin:        retryJoin,
			StartJoin:        startJoin,
			RetryMaxAttempts: retryMaxAttempts,
			RetryInterval:    time.Duration(retryInterval),
		}

		result := a.Merge(b)
		require.Equal(result.RetryJoin, retryJoin)
		require.Equal(result.StartJoin, startJoin)
		require.Equal(result.RetryMaxAttempts, retryMaxAttempts)
		require.Equal(result.RetryInterval, retryInterval)
	}
	{
		retryJoin := []string{"127.0.0.1", "127.0.0.2"}
		startJoin := []string{"127.0.0.1", "127.0.0.2"}
		retryMaxAttempts := 1
		retryInterval := time.Duration(0)

		a := &ServerJoin{
			RetryJoin:        retryJoin,
			StartJoin:        startJoin,
			RetryMaxAttempts: retryMaxAttempts,
			RetryInterval:    time.Duration(retryInterval),
		}
		var b *ServerJoin

		result := a.Merge(b)
		require.Equal(result.RetryJoin, retryJoin)
		require.Equal(result.StartJoin, startJoin)
		require.Equal(result.RetryMaxAttempts, retryMaxAttempts)
		require.Equal(result.RetryInterval, retryInterval)
	}
	{
		retryJoin := []string{"127.0.0.1", "127.0.0.2"}
		startJoin := []string{"127.0.0.1", "127.0.0.2"}
		retryMaxAttempts := 1
		retryInterval := time.Duration(0)

		a := &ServerJoin{
			RetryJoin: retryJoin,
			StartJoin: startJoin,
		}
		b := &ServerJoin{
			RetryMaxAttempts: retryMaxAttempts,
			RetryInterval:    time.Duration(retryInterval),
		}

		result := a.Merge(b)
		require.Equal(result.RetryJoin, retryJoin)
		require.Equal(result.StartJoin, startJoin)
		require.Equal(result.RetryMaxAttempts, retryMaxAttempts)
		require.Equal(result.RetryInterval, retryInterval)
	}
}

func TestTelemetry_PrefixFilters(t *testing.T) {
	t.Parallel()
	cases := []struct {
		in       []string
		expAllow []string
		expBlock []string
		expErr   bool
	}{
		{
			in:       []string{"+foo"},
			expAllow: []string{"foo"},
		},
		{
			in:       []string{"-foo"},
			expBlock: []string{"foo"},
		},
		{
			in:       []string{"+a.b.c", "-x.y.z"},
			expAllow: []string{"a.b.c"},
			expBlock: []string{"x.y.z"},
		},
		{
			in:     []string{"+foo", "bad", "-bar"},
			expErr: true,
		},
	}

	for i, c := range cases {
		t.Run(fmt.Sprintf("PrefixCase%d", i), func(t *testing.T) {
			require := require.New(t)
			tel := &Telemetry{
				PrefixFilter: c.in,
			}

			allow, block, err := tel.PrefixFilters()
			require.Exactly(c.expAllow, allow)
			require.Exactly(c.expBlock, block)
			require.Equal(c.expErr, err != nil)
		})
	}
}

func TestTelemetry_Parse(t *testing.T) {
	require := require.New(t)
	dir, err := ioutil.TempDir("", "nomad")
	require.NoError(err)
	defer os.RemoveAll(dir)

	file1 := filepath.Join(dir, "config1.hcl")
	err = ioutil.WriteFile(file1, []byte(`telemetry{
		prefix_filter = ["+nomad.raft"]
		filter_default = false
		disable_dispatched_job_summary_metrics = true
	}`), 0600)
	require.NoError(err)

	// Works on config dir
	config, err := LoadConfig(dir)
	require.NoError(err)

	require.False(*config.Telemetry.FilterDefault)
	require.Exactly([]string{"+nomad.raft"}, config.Telemetry.PrefixFilter)
	require.True(config.Telemetry.DisableDispatchedJobSummaryMetrics)
}
