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

package config

import (
	"testing"
	"time"

	"github.com/hashicorp/consul-template/config"
	"github.com/hashicorp/nomad/ci"
	"github.com/hashicorp/nomad/helper/pointer"
	"github.com/stretchr/testify/require"
)

func TestConfigRead(t *testing.T) {
	ci.Parallel(t)

	config := Config{}

	actual := config.Read("cake")
	if actual != "" {
		t.Errorf("Expected empty string; found %s", actual)
	}

	expected := "chocolate"
	config.Options = map[string]string{"cake": expected}
	actual = config.Read("cake")
	if actual != expected {
		t.Errorf("Expected %s, found %s", expected, actual)
	}
}

func TestConfigReadDefault(t *testing.T) {
	ci.Parallel(t)

	config := Config{}

	expected := "vanilla"
	actual := config.ReadDefault("cake", expected)
	if actual != expected {
		t.Errorf("Expected %s, found %s", expected, actual)
	}

	expected = "chocolate"
	config.Options = map[string]string{"cake": expected}
	actual = config.ReadDefault("cake", "vanilla")
	if actual != expected {
		t.Errorf("Expected %s, found %s", expected, actual)
	}
}

func mockWaitConfig() *WaitConfig {
	return &WaitConfig{
		Min: pointer.Of(5 * time.Second),
		Max: pointer.Of(10 * time.Second),
	}
}

func TestWaitConfig_Copy(t *testing.T) {
	ci.Parallel(t)

	cases := []struct {
		Name     string
		Wait     *WaitConfig
		Expected *WaitConfig
	}{
		{
			"fully-populated",
			mockWaitConfig(),
			&WaitConfig{
				Min: pointer.Of(5 * time.Second),
				Max: pointer.Of(10 * time.Second),
			},
		},
		{
			"min-only",
			&WaitConfig{
				Min: pointer.Of(5 * time.Second),
			},
			&WaitConfig{
				Min: pointer.Of(5 * time.Second),
			},
		},
		{
			"max-only",
			&WaitConfig{
				Max: pointer.Of(5 * time.Second),
			},
			&WaitConfig{
				Max: pointer.Of(5 * time.Second),
			},
		},
	}

	for _, _case := range cases {
		t.Run(_case.Name, func(t *testing.T) {
			result := _case.Expected.Equal(_case.Wait.Copy())
			if !result {
				t.Logf("\nExpected %v\n   Found %v", _case.Expected, result)
			}
			require.True(t, result)
		})
	}
}

func TestWaitConfig_IsEmpty(t *testing.T) {
	ci.Parallel(t)

	cases := []struct {
		Name     string
		Wait     *WaitConfig
		Expected bool
	}{
		{
			"is-nil",
			nil,
			true,
		},
		{
			"is-empty",
			&WaitConfig{},
			true,
		},
		{
			"is-not-empty",
			&WaitConfig{
				Min: pointer.Of(10 * time.Second),
			},
			false,
		},
	}

	for _, _case := range cases {
		t.Run(_case.Name, func(t *testing.T) {
			require.Equal(t, _case.Expected, _case.Wait.IsEmpty())
		})
	}
}

func TestWaitConfig_IsEqual(t *testing.T) {
	ci.Parallel(t)

	cases := []struct {
		Name     string
		Wait     *WaitConfig
		Other    *WaitConfig
		Expected bool
	}{
		{
			"are-equal",
			mockWaitConfig(),
			&WaitConfig{
				Min: pointer.Of(5 * time.Second),
				Max: pointer.Of(10 * time.Second),
			},
			true,
		},
		{
			"min-different",
			mockWaitConfig(),
			&WaitConfig{
				Min: pointer.Of(4 * time.Second),
				Max: pointer.Of(10 * time.Second),
			},
			false,
		},
		{
			"max-different",
			mockWaitConfig(),
			&WaitConfig{
				Min: pointer.Of(5 * time.Second),
				Max: pointer.Of(9 * time.Second),
			},
			false,
		},
	}

	for _, _case := range cases {
		t.Run(_case.Name, func(t *testing.T) {
			require.Equal(t, _case.Expected, _case.Wait.Equal(_case.Other))
		})
	}
}

func TestWaitConfig_IsValid(t *testing.T) {
	ci.Parallel(t)

	cases := []struct {
		Name     string
		Retry    *WaitConfig
		Expected string
	}{
		{
			"is-valid",
			&WaitConfig{
				Min: pointer.Of(5 * time.Second),
				Max: pointer.Of(10 * time.Second),
			},
			"",
		},
		{
			"is-nil",
			nil,
			"is nil",
		},
		{
			"is-empty",
			&WaitConfig{},
			"or empty",
		},
		{
			"min-greater-than-max",
			&WaitConfig{
				Min: pointer.Of(10 * time.Second),
				Max: pointer.Of(5 * time.Second),
			},
			"greater than",
		},
		{
			"max-not-set",
			&WaitConfig{
				Min: pointer.Of(10 * time.Second),
			},
			"",
		},
	}

	for _, _case := range cases {
		t.Run(_case.Name, func(t *testing.T) {
			if _case.Expected == "" {
				require.Nil(t, _case.Retry.Validate())
			} else {
				err := _case.Retry.Validate()
				require.Contains(t, err.Error(), _case.Expected)
			}
		})
	}
}

func TestWaitConfig_Merge(t *testing.T) {
	ci.Parallel(t)

	cases := []struct {
		Name     string
		Target   *WaitConfig
		Other    *WaitConfig
		Expected *WaitConfig
	}{
		{
			"all-fields",
			mockWaitConfig(),
			&WaitConfig{
				Min: pointer.Of(4 * time.Second),
				Max: pointer.Of(9 * time.Second),
			},
			&WaitConfig{
				Min: pointer.Of(4 * time.Second),
				Max: pointer.Of(9 * time.Second),
			},
		},
		{
			"min-only",
			mockWaitConfig(),
			&WaitConfig{
				Min: pointer.Of(4 * time.Second),
				Max: pointer.Of(10 * time.Second),
			},
			&WaitConfig{
				Min: pointer.Of(4 * time.Second),
				Max: pointer.Of(10 * time.Second),
			},
		},
		{
			"max-only",
			mockWaitConfig(),
			&WaitConfig{
				Min: pointer.Of(5 * time.Second),
				Max: pointer.Of(9 * time.Second),
			},
			&WaitConfig{
				Min: pointer.Of(5 * time.Second),
				Max: pointer.Of(9 * time.Second),
			},
		},
	}

	for _, _case := range cases {
		t.Run(_case.Name, func(t *testing.T) {
			merged := _case.Target.Merge(_case.Other)
			result := _case.Expected.Equal(merged)
			if !result {
				t.Logf("\nExpected %v\n   Found %v", _case.Expected, merged)
			}
			require.True(t, result)
		})
	}
}

func TestWaitConfig_ToConsulTemplate(t *testing.T) {
	ci.Parallel(t)

	expected := config.WaitConfig{
		Enabled: pointer.Of(true),
		Min:     pointer.Of(5 * time.Second),
		Max:     pointer.Of(10 * time.Second),
	}

	clientWaitConfig := &WaitConfig{
		Min: pointer.Of(5 * time.Second),
		Max: pointer.Of(10 * time.Second),
	}

	actual, err := clientWaitConfig.ToConsulTemplate()
	require.NoError(t, err)
	require.Equal(t, *expected.Min, *actual.Min)
	require.Equal(t, *expected.Max, *actual.Max)
}

func mockRetryConfig() *RetryConfig {
	return &RetryConfig{
		Attempts:      pointer.Of(5),
		Backoff:       pointer.Of(5 * time.Second),
		BackoffHCL:    "5s",
		MaxBackoff:    pointer.Of(10 * time.Second),
		MaxBackoffHCL: "10s",
	}
}
func TestRetryConfig_Copy(t *testing.T) {
	ci.Parallel(t)

	cases := []struct {
		Name     string
		Retry    *RetryConfig
		Expected *RetryConfig
	}{
		{
			"fully-populated",
			mockRetryConfig(),
			&RetryConfig{
				Attempts:      pointer.Of(5),
				Backoff:       pointer.Of(5 * time.Second),
				BackoffHCL:    "5s",
				MaxBackoff:    pointer.Of(10 * time.Second),
				MaxBackoffHCL: "10s",
			},
		},
		{
			"attempts-only",
			&RetryConfig{
				Attempts: pointer.Of(5),
			},
			&RetryConfig{
				Attempts: pointer.Of(5),
			},
		},
		{
			"backoff-only",
			&RetryConfig{
				Backoff: pointer.Of(5 * time.Second),
			},
			&RetryConfig{
				Backoff: pointer.Of(5 * time.Second),
			},
		},
		{
			"backoff-hcl-only",
			&RetryConfig{
				BackoffHCL: "5s",
			},
			&RetryConfig{
				BackoffHCL: "5s",
			},
		},
		{
			"max-backoff-only",
			&RetryConfig{
				MaxBackoff: pointer.Of(10 * time.Second),
			},
			&RetryConfig{
				MaxBackoff: pointer.Of(10 * time.Second),
			},
		},
		{
			"max-backoff-hcl-only",
			&RetryConfig{
				MaxBackoffHCL: "10s",
			},
			&RetryConfig{
				MaxBackoffHCL: "10s",
			},
		},
	}

	for _, _case := range cases {
		t.Run(_case.Name, func(t *testing.T) {
			result := _case.Expected.Equal(_case.Retry.Copy())
			if !result {
				t.Logf("\nExpected %v\n   Found %v", _case.Expected, result)
			}
			require.True(t, result)
		})
	}
}

func TestRetryConfig_IsEmpty(t *testing.T) {
	ci.Parallel(t)

	cases := []struct {
		Name     string
		Retry    *RetryConfig
		Expected bool
	}{
		{
			"is-nil",
			nil,
			true,
		},
		{
			"is-empty",
			&RetryConfig{},
			true,
		},
		{
			"is-not-empty",
			&RetryConfig{
				Attempts: pointer.Of(12),
			},
			false,
		},
	}

	for _, _case := range cases {
		t.Run(_case.Name, func(t *testing.T) {
			require.Equal(t, _case.Expected, _case.Retry.IsEmpty())
		})
	}
}

func TestRetryConfig_IsEqual(t *testing.T) {
	ci.Parallel(t)

	cases := []struct {
		Name     string
		Retry    *RetryConfig
		Other    *RetryConfig
		Expected bool
	}{
		{
			"are-equal",
			mockRetryConfig(),
			&RetryConfig{
				Attempts:      pointer.Of(5),
				Backoff:       pointer.Of(5 * time.Second),
				BackoffHCL:    "5s",
				MaxBackoff:    pointer.Of(10 * time.Second),
				MaxBackoffHCL: "10s",
			},
			true,
		},
		{
			"attempts-different",
			mockRetryConfig(),
			&RetryConfig{
				Attempts:      pointer.Of(4),
				Backoff:       pointer.Of(5 * time.Second),
				BackoffHCL:    "5s",
				MaxBackoff:    pointer.Of(10 * time.Second),
				MaxBackoffHCL: "10s",
			},
			false,
		},
		{
			"backoff-different",
			mockRetryConfig(),
			&RetryConfig{
				Attempts:      pointer.Of(5),
				Backoff:       pointer.Of(4 * time.Second),
				BackoffHCL:    "5s",
				MaxBackoff:    pointer.Of(10 * time.Second),
				MaxBackoffHCL: "10s",
			},
			false,
		},
		{
			"backoff-hcl-different",
			mockRetryConfig(),
			&RetryConfig{
				Attempts:      pointer.Of(5),
				Backoff:       pointer.Of(5 * time.Second),
				BackoffHCL:    "4s",
				MaxBackoff:    pointer.Of(10 * time.Second),
				MaxBackoffHCL: "10s",
			},
			false,
		},
		{
			"max-backoff-different",
			mockRetryConfig(),
			&RetryConfig{
				Attempts:      pointer.Of(5),
				Backoff:       pointer.Of(5 * time.Second),
				BackoffHCL:    "5s",
				MaxBackoff:    pointer.Of(9 * time.Second),
				MaxBackoffHCL: "10s",
			},
			false,
		},
		{
			"max-backoff-hcl-different",
			mockRetryConfig(),
			&RetryConfig{
				Attempts:      pointer.Of(5),
				Backoff:       pointer.Of(5 * time.Second),
				BackoffHCL:    "5s",
				MaxBackoff:    pointer.Of(10 * time.Second),
				MaxBackoffHCL: "9s",
			},
			false,
		},
	}

	for _, _case := range cases {
		t.Run(_case.Name, func(t *testing.T) {
			require.Equal(t, _case.Expected, _case.Retry.Equal(_case.Other))
		})
	}
}

func TestRetryConfig_IsValid(t *testing.T) {
	ci.Parallel(t)

	cases := []struct {
		Name     string
		Retry    *RetryConfig
		Expected string
	}{
		{
			"is-valid",
			&RetryConfig{
				Backoff:    pointer.Of(5 * time.Second),
				MaxBackoff: pointer.Of(10 * time.Second),
			},
			"",
		},
		{
			"is-nil",
			nil,
			"is nil",
		},
		{
			"is-empty",
			&RetryConfig{},
			"or empty",
		},
		{
			"backoff-greater-than-max-backoff",
			&RetryConfig{
				Backoff:    pointer.Of(10 * time.Second),
				MaxBackoff: pointer.Of(5 * time.Second),
			},
			"greater than max_backoff",
		},
		{
			"backoff-not-set",
			&RetryConfig{
				MaxBackoff: pointer.Of(10 * time.Second),
			},
			"",
		},
		{
			"max-backoff-not-set",
			&RetryConfig{
				Backoff: pointer.Of(2 * time.Minute),
			},
			"greater than default",
		},
		{
			"max-backoff-unbounded",
			&RetryConfig{
				Backoff:    pointer.Of(10 * time.Second),
				MaxBackoff: pointer.Of(0 * time.Second),
			},
			"",
		},
	}

	for _, _case := range cases {
		t.Run(_case.Name, func(t *testing.T) {
			if _case.Expected == "" {
				require.Nil(t, _case.Retry.Validate())
			} else {
				err := _case.Retry.Validate()
				require.Contains(t, err.Error(), _case.Expected)
			}
		})
	}
}

func TestRetryConfig_Merge(t *testing.T) {
	ci.Parallel(t)

	cases := []struct {
		Name     string
		Target   *RetryConfig
		Other    *RetryConfig
		Expected *RetryConfig
	}{
		{
			"all-fields",
			mockRetryConfig(),
			&RetryConfig{
				Attempts:      pointer.Of(4),
				Backoff:       pointer.Of(4 * time.Second),
				BackoffHCL:    "4s",
				MaxBackoff:    pointer.Of(9 * time.Second),
				MaxBackoffHCL: "9s",
			},
			&RetryConfig{
				Attempts:      pointer.Of(4),
				Backoff:       pointer.Of(4 * time.Second),
				BackoffHCL:    "4s",
				MaxBackoff:    pointer.Of(9 * time.Second),
				MaxBackoffHCL: "9s",
			},
		},
		{
			"attempts-only",
			mockRetryConfig(),
			&RetryConfig{
				Attempts:      pointer.Of(4),
				Backoff:       pointer.Of(5 * time.Second),
				BackoffHCL:    "5s",
				MaxBackoff:    pointer.Of(10 * time.Second),
				MaxBackoffHCL: "10s",
			},
			&RetryConfig{
				Attempts:      pointer.Of(4),
				Backoff:       pointer.Of(5 * time.Second),
				BackoffHCL:    "5s",
				MaxBackoff:    pointer.Of(10 * time.Second),
				MaxBackoffHCL: "10s",
			},
		},
		{
			"multi-field",
			mockRetryConfig(),
			&RetryConfig{
				Attempts:      pointer.Of(5),
				Backoff:       pointer.Of(4 * time.Second),
				BackoffHCL:    "4s",
				MaxBackoff:    pointer.Of(9 * time.Second),
				MaxBackoffHCL: "9s",
			},
			&RetryConfig{
				Attempts:      pointer.Of(5),
				Backoff:       pointer.Of(4 * time.Second),
				BackoffHCL:    "4s",
				MaxBackoff:    pointer.Of(9 * time.Second),
				MaxBackoffHCL: "9s",
			},
		},
	}

	for _, _case := range cases {
		t.Run(_case.Name, func(t *testing.T) {
			merged := _case.Target.Merge(_case.Other)
			result := _case.Expected.Equal(merged)
			if !result {
				t.Logf("\nExpected %v\n   Found %v", _case.Expected, merged)
			}
			require.True(t, result)
		})
	}
}

func TestRetryConfig_ToConsulTemplate(t *testing.T) {
	ci.Parallel(t)

	expected := config.RetryConfig{
		Enabled:    pointer.Of(true),
		Attempts:   pointer.Of(5),
		Backoff:    pointer.Of(5 * time.Second),
		MaxBackoff: pointer.Of(10 * time.Second),
	}

	actual := mockRetryConfig()

	require.Equal(t, *expected.Attempts, *actual.Attempts)
	require.Equal(t, *expected.Backoff, *actual.Backoff)
	require.Equal(t, *expected.MaxBackoff, *actual.MaxBackoff)
}
