// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package agent

import (
	"bytes"
	"encoding/base64"
	"io/ioutil"
	"os"
	"path/filepath"
	"reflect"
	"testing"
	"time"
)

func TestConfigBindAddrParts(t *testing.T) {
	testCases := []struct {
		Value string
		IP    string
		Port  int
		Error bool
	}{
		{"0.0.0.0", "0.0.0.0", DefaultBindPort, false},
		{"0.0.0.0:1234", "0.0.0.0", 1234, false},
	}

	for _, tc := range testCases {
		c := &Config{BindAddr: tc.Value}
		ip, port, err := c.AddrParts(c.BindAddr)
		if tc.Error != (err != nil) {
			t.Errorf("Bad error: %s", err)
			continue
		}

		if tc.IP != ip {
			t.Errorf("%s: Got IP %#v", tc.Value, ip)
			continue
		}

		if tc.Port != port {
			t.Errorf("%s: Got port %d", tc.Value, port)
			continue
		}
	}
}

func TestConfigEncryptBytes(t *testing.T) {
	// Test with some input
	src := []byte("abc")
	c := &Config{
		EncryptKey: base64.StdEncoding.EncodeToString(src),
	}

	result, err := c.EncryptBytes()
	if err != nil {
		t.Fatalf("err: %v", err)
	}

	if !bytes.Equal(src, result) {
		t.Fatalf("bad: %#v", result)
	}

	// Test with no input
	c = &Config{}
	result, err = c.EncryptBytes()
	if err != nil {
		t.Fatalf("err: %v", err)
	}

	if len(result) > 0 {
		t.Fatalf("bad: %#v", result)
	}
}

func TestConfigEventScripts(t *testing.T) {
	c := &Config{
		EventHandlers: []string{
			"foo.sh",
			"bar=blah.sh",
		},
	}

	result := c.EventScripts()
	if len(result) != 2 {
		t.Fatalf("bad: %#v", result)
	}

	expected := []EventScript{
		{EventFilter{"*", ""}, "foo.sh"},
		{EventFilter{"bar", ""}, "blah.sh"},
	}

	if !reflect.DeepEqual(result, expected) {
		t.Fatalf("bad: %#v", result)
	}
}

func TestDecodeConfig(t *testing.T) {
	// Without a protocol
	input := `{"node_name": "foo"}`
	config, err := DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %v", err)
	}

	if config.NodeName != "foo" {
		t.Fatalf("bad: %#v", config)
	}

	if config.Protocol != 0 {
		t.Fatalf("bad: %#v", config)
	}

	if config.SkipLeaveOnInt != DefaultConfig().SkipLeaveOnInt {
		t.Fatalf("bad: %#v", config)
	}

	if config.LeaveOnTerm != DefaultConfig().LeaveOnTerm {
		t.Fatalf("bad: %#v", config)
	}

	// With a protocol
	input = `{"node_name": "foo", "protocol": 7}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %v", err)
	}

	if config.NodeName != "foo" {
		t.Fatalf("bad: %#v", config)
	}

	if config.Protocol != 7 {
		t.Fatalf("bad: %#v", config)
	}

	// A bind addr
	input = `{"bind": "127.0.0.2"}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %v", err)
	}

	if config.BindAddr != "127.0.0.2" {
		t.Fatalf("bad: %#v", config)
	}

	// replayOnJoin
	input = `{"replay_on_join": true}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %v", err)
	}

	if config.ReplayOnJoin != true {
		t.Fatalf("bad: %#v", config)
	}

	// leave_on_terminate
	input = `{"leave_on_terminate": true}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %v", err)
	}

	if config.LeaveOnTerm != true {
		t.Fatalf("bad: %#v", config)
	}

	// skip_leave_on_interrupt
	input = `{"skip_leave_on_interrupt": true}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %v", err)
	}

	if config.SkipLeaveOnInt != true {
		t.Fatalf("bad: %#v", config)
	}

	// tags
	input = `{"tags": {"foo": "bar", "role": "test"}}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %v", err)
	}

	if config.Tags["foo"] != "bar" {
		t.Fatalf("bad: %#v", config)
	}
	if config.Tags["role"] != "test" {
		t.Fatalf("bad: %#v", config)
	}

	// tags file
	input = `{"tags_file": "/some/path"}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %v", err)
	}

	if config.TagsFile != "/some/path" {
		t.Fatalf("bad: %#v", config)
	}

	// Discover
	input = `{"discover": "foobar"}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %v", err)
	}

	if config.Discover != "foobar" {
		t.Fatalf("bad: %#v", config)
	}

	// Interface
	input = `{"interface": "eth0"}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %v", err)
	}

	if config.Interface != "eth0" {
		t.Fatalf("bad: %#v", config)
	}

	// Reconnect intervals
	input = `{"reconnect_interval": "15s", "reconnect_timeout": "48h"}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %v", err)
	}

	if config.ReconnectInterval != 15*time.Second {
		t.Fatalf("bad: %#v", config)
	}

	if config.ReconnectTimeout != 48*time.Hour {
		t.Fatalf("bad: %#v", config)
	}

	// RPC Auth
	input = `{"rpc_auth": "foobar"}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %v", err)
	}

	if config.RPCAuthKey != "foobar" {
		t.Fatalf("bad: %#v", config)
	}

	// DisableNameResolution
	input = `{"disable_name_resolution": true}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %v", err)
	}

	if !config.DisableNameResolution {
		t.Fatalf("bad: %#v", config)
	}

	// Tombstone intervals
	input = `{"tombstone_timeout": "48h"}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %v", err)
	}

	if config.TombstoneTimeout != 48*time.Hour {
		t.Fatalf("bad: %#v", config)
	}

	// Syslog
	input = `{"enable_syslog": true, "syslog_facility": "LOCAL4"}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %v", err)
	}

	if !config.EnableSyslog {
		t.Fatalf("bad: %#v", config)
	}
	if config.SyslogFacility != "LOCAL4" {
		t.Fatalf("bad: %#v", config)
	}

	// Retry configs
	input = `{"retry_max_attempts": 5, "retry_interval": "60s"}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %v", err)
	}

	if config.RetryMaxAttempts != 5 {
		t.Fatalf("bad: %#v", config)
	}

	if config.RetryInterval != 60*time.Second {
		t.Fatalf("bad: %#v", config)
	}

	// Broadcast timeout
	input = `{"broadcast_timeout": "10s"}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %v", err)
	}

	if config.BroadcastTimeout != 10*time.Second {
		t.Fatalf("bad: %#v", config)
	}

	// Retry configs
	input = `{"retry_join": ["127.0.0.1", "127.0.0.2"]}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %v", err)
	}

	if len(config.RetryJoin) != 2 {
		t.Fatalf("bad: %#v", config)
	}

	if config.RetryJoin[0] != "127.0.0.1" {
		t.Fatalf("bad: %#v", config)
	}

	if config.RetryJoin[1] != "127.0.0.2" {
		t.Fatalf("bad: %#v", config)
	}

	// Rejoin configs
	input = `{"rejoin_after_leave": true}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %v", err)
	}

	if !config.RejoinAfterLeave {
		t.Fatalf("bad: %#v", config)
	}

	// Stats configs
	input = `{"statsite_addr": "127.0.0.1:8123", "statsd_addr": "127.0.0.1:8125"}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %v", err)
	}

	if config.StatsiteAddr != "127.0.0.1:8123" {
		t.Fatalf("bad: %#v", config)
	}

	if config.StatsdAddr != "127.0.0.1:8125" {
		t.Fatalf("bad: %#v", config)
	}

	// Query sizes
	input = `{"query_response_size_limit": 123, "query_size_limit": 456}`
	config, err = DecodeConfig(bytes.NewReader([]byte(input)))
	if err != nil {
		t.Fatalf("err: %v", err)
	}

	if config.QueryResponseSizeLimit != 123 || config.QuerySizeLimit != 456 {
		t.Fatalf("bad: %#v", config)
	}
}

func TestDecodeConfig_unknownDirective(t *testing.T) {
	input := `{"unknown_directive": "titi"}`
	_, err := DecodeConfig(bytes.NewReader([]byte(input)))
	if err == nil {
		t.Fatal("should have err")
	}
}

func TestMergeConfig(t *testing.T) {
	a := &Config{
		NodeName:      "foo",
		Role:          "bar",
		Protocol:      7,
		EventHandlers: []string{"foo"},
		StartJoin:     []string{"foo"},
		ReplayOnJoin:  true,
		RetryJoin:     []string{"zab"},
	}

	b := &Config{
		NodeName:               "bname",
		DisableCoordinates:     true,
		Protocol:               -1,
		EncryptKey:             "foo",
		EventHandlers:          []string{"bar"},
		StartJoin:              []string{"bar"},
		LeaveOnTerm:            true,
		SkipLeaveOnInt:         true,
		Discover:               "tubez",
		Interface:              "eth0",
		ReconnectInterval:      15 * time.Second,
		ReconnectTimeout:       48 * time.Hour,
		RPCAuthKey:             "foobar",
		DisableNameResolution:  true,
		TombstoneTimeout:       36 * time.Hour,
		EnableSyslog:           true,
		RetryJoin:              []string{"zip"},
		RetryMaxAttempts:       10,
		RetryInterval:          120 * time.Second,
		RejoinAfterLeave:       true,
		StatsiteAddr:           "127.0.0.1:8125",
		QueryResponseSizeLimit: 123,
		QuerySizeLimit:         456,
		BroadcastTimeout:       20 * time.Second,
		EnableCompression:      true,
	}

	c := MergeConfig(a, b)

	if c.NodeName != "bname" {
		t.Fatalf("bad: %#v", c)
	}

	if c.Role != "bar" {
		t.Fatalf("bad: %#v", c)
	}

	if c.DisableCoordinates != true {
		t.Fatalf("bad: %#v", c)
	}

	if c.Protocol != 7 {
		t.Fatalf("bad: %#v", c)
	}

	if c.EncryptKey != "foo" {
		t.Fatalf("bad: %#v", c.EncryptKey)
	}

	if c.ReplayOnJoin != true {
		t.Fatalf("bad: %#v", c.ReplayOnJoin)
	}

	if !c.LeaveOnTerm {
		t.Fatalf("bad: %#v", c.LeaveOnTerm)
	}

	if !c.SkipLeaveOnInt {
		t.Fatalf("bad: %#v", c.SkipLeaveOnInt)
	}

	if c.Discover != "tubez" {
		t.Fatalf("Bad: %v", c.Discover)
	}

	if c.Interface != "eth0" {
		t.Fatalf("Bad: %v", c.Interface)
	}

	if c.ReconnectInterval != 15*time.Second {
		t.Fatalf("bad: %#v", c)
	}

	if c.ReconnectTimeout != 48*time.Hour {
		t.Fatalf("bad: %#v", c)
	}

	if c.TombstoneTimeout != 36*time.Hour {
		t.Fatalf("bad: %#v", c)
	}

	if c.RPCAuthKey != "foobar" {
		t.Fatalf("bad: %#v", c)
	}

	if !c.DisableNameResolution {
		t.Fatalf("bad: %#v", c)
	}

	if !c.EnableSyslog {
		t.Fatalf("bad: %#v", c)
	}

	if c.RetryMaxAttempts != 10 {
		t.Fatalf("bad: %#v", c)
	}

	if c.RetryInterval != 120*time.Second {
		t.Fatalf("bad: %#v", c)
	}

	if !c.RejoinAfterLeave {
		t.Fatalf("bad: %#v", c)
	}

	if c.StatsiteAddr != "127.0.0.1:8125" {
		t.Fatalf("bad: %#v", c)
	}

	expected := []string{"foo", "bar"}
	if !reflect.DeepEqual(c.EventHandlers, expected) {
		t.Fatalf("bad: %#v", c)
	}

	if !reflect.DeepEqual(c.StartJoin, expected) {
		t.Fatalf("bad: %#v", c)
	}

	expected = []string{"zab", "zip"}
	if !reflect.DeepEqual(c.RetryJoin, expected) {
		t.Fatalf("bad: %#v", c)
	}

	if c.QueryResponseSizeLimit != 123 || c.QuerySizeLimit != 456 {
		t.Fatalf("bad: %#v", c)
	}

	if c.BroadcastTimeout != 20*time.Second {
		t.Fatalf("bad: %#v", c)
	}

	if !c.EnableCompression {
		t.Fatalf("bad: %#v", c)
	}
}

func TestReadConfigPaths_badPath(t *testing.T) {
	_, err := ReadConfigPaths([]string{"/i/shouldnt/exist/ever/rainbows"})
	if err == nil {
		t.Fatal("should have err")
	}
}

func TestReadConfigPaths_file(t *testing.T) {
	tf, err := ioutil.TempFile("", "serf")
	if err != nil {
		t.Fatalf("err: %v", err)
	}
	tf.Write([]byte(`{"node_name":"bar"}`))
	tf.Close()
	defer os.Remove(tf.Name())

	config, err := ReadConfigPaths([]string{tf.Name()})
	if err != nil {
		t.Fatalf("err: %v", err)
	}

	if config.NodeName != "bar" {
		t.Fatalf("bad: %#v", config)
	}
}

func TestReadConfigPaths_dir(t *testing.T) {
	td, err := ioutil.TempDir("", "serf")
	if err != nil {
		t.Fatalf("err: %v", err)
	}
	defer os.RemoveAll(td)

	err = ioutil.WriteFile(filepath.Join(td, "a.json"),
		[]byte(`{"node_name": "bar"}`), 0644)
	if err != nil {
		t.Fatalf("err: %v", err)
	}

	err = ioutil.WriteFile(filepath.Join(td, "b.json"),
		[]byte(`{"node_name": "baz"}`), 0644)
	if err != nil {
		t.Fatalf("err: %v", err)
	}

	// A non-json file, shouldn't be read
	err = ioutil.WriteFile(filepath.Join(td, "c"),
		[]byte(`{"node_name": "bad"}`), 0644)
	if err != nil {
		t.Fatalf("err: %v", err)
	}

	config, err := ReadConfigPaths([]string{td})
	if err != nil {
		t.Fatalf("err: %v", err)
	}

	if config.NodeName != "baz" {
		t.Fatalf("bad: %#v", config)
	}
}
