package env

import (
	"io/ioutil"
	"os"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestGetWithFallback(t *testing.T) {
	var1Exist := os.Getenv("TEST_LEGO_VAR_EXIST_1")
	var2Exist := os.Getenv("TEST_LEGO_VAR_EXIST_2")
	var1Missing := os.Getenv("TEST_LEGO_VAR_MISSING_1")
	var2Missing := os.Getenv("TEST_LEGO_VAR_MISSING_2")

	defer func() {
		_ = os.Setenv("TEST_LEGO_VAR_EXIST_1", var1Exist)
		_ = os.Setenv("TEST_LEGO_VAR_EXIST_2", var2Exist)
		_ = os.Setenv("TEST_LEGO_VAR_MISSING_1", var1Missing)
		_ = os.Setenv("TEST_LEGO_VAR_MISSING_2", var2Missing)
	}()

	err := os.Setenv("TEST_LEGO_VAR_EXIST_1", "VAR1")
	require.NoError(t, err)
	err = os.Setenv("TEST_LEGO_VAR_EXIST_2", "VAR2")
	require.NoError(t, err)
	err = os.Unsetenv("TEST_LEGO_VAR_MISSING_1")
	require.NoError(t, err)
	err = os.Unsetenv("TEST_LEGO_VAR_MISSING_2")
	require.NoError(t, err)

	type expected struct {
		value map[string]string
		error string
	}

	testCases := []struct {
		desc     string
		groups   [][]string
		expected expected
	}{
		{
			desc:   "no groups",
			groups: nil,
			expected: expected{
				value: map[string]string{},
			},
		},
		{
			desc:   "empty groups",
			groups: [][]string{{}, {}},
			expected: expected{
				error: "undefined environment variable names",
			},
		},
		{
			desc:   "missing env var",
			groups: [][]string{{"TEST_LEGO_VAR_MISSING_1"}},
			expected: expected{
				error: "some credentials information are missing: TEST_LEGO_VAR_MISSING_1",
			},
		},
		{
			desc:   "all env var in a groups are missing",
			groups: [][]string{{"TEST_LEGO_VAR_MISSING_1", "TEST_LEGO_VAR_MISSING_2"}},
			expected: expected{
				error: "some credentials information are missing: TEST_LEGO_VAR_MISSING_1",
			},
		},
		{
			desc:   "only the first env var have a value",
			groups: [][]string{{"TEST_LEGO_VAR_EXIST_1", "TEST_LEGO_VAR_MISSING_1"}},
			expected: expected{
				value: map[string]string{"TEST_LEGO_VAR_EXIST_1": "VAR1"},
			},
		},
		{
			desc:   "only the second env var have a value",
			groups: [][]string{{"TEST_LEGO_VAR_MISSING_1", "TEST_LEGO_VAR_EXIST_1"}},
			expected: expected{
				value: map[string]string{"TEST_LEGO_VAR_MISSING_1": "VAR1"},
			},
		},
		{
			desc:   "all env vars in a groups have a value",
			groups: [][]string{{"TEST_LEGO_VAR_EXIST_1", "TEST_LEGO_VAR_EXIST_2"}},
			expected: expected{
				value: map[string]string{"TEST_LEGO_VAR_EXIST_1": "VAR1"},
			},
		},
	}

	for _, test := range testCases {
		t.Run(test.desc, func(t *testing.T) {
			value, err := GetWithFallback(test.groups...)
			if len(test.expected.error) > 0 {
				assert.EqualError(t, err, test.expected.error)
			} else {
				require.NoError(t, err)
				assert.Equal(t, test.expected.value, value)
			}
		})
	}
}

func TestGetOrDefaultInt(t *testing.T) {
	testCases := []struct {
		desc         string
		envValue     string
		defaultValue int
		expected     int
	}{
		{
			desc:         "valid value",
			envValue:     "100",
			defaultValue: 2,
			expected:     100,
		},
		{
			desc:         "invalid content, use default value",
			envValue:     "abc123",
			defaultValue: 2,
			expected:     2,
		},
		{
			desc:         "valid negative value",
			envValue:     "-111",
			defaultValue: 2,
			expected:     -111,
		},
		{
			desc:         "float: invalid type, use default value",
			envValue:     "1.11",
			defaultValue: 2,
			expected:     2,
		},
	}

	const key = "LEGO_ENV_TC"

	for _, test := range testCases {
		t.Run(test.desc, func(t *testing.T) {
			defer os.Unsetenv(key)
			err := os.Setenv(key, test.envValue)
			require.NoError(t, err)

			result := GetOrDefaultInt(key, test.defaultValue)
			assert.Equal(t, test.expected, result)
		})
	}
}

func TestGetOrDefaultSecond(t *testing.T) {
	testCases := []struct {
		desc         string
		envValue     string
		defaultValue time.Duration
		expected     time.Duration
	}{
		{
			desc:         "valid value",
			envValue:     "100",
			defaultValue: 2 * time.Second,
			expected:     100 * time.Second,
		},
		{
			desc:         "invalid content, use default value",
			envValue:     "abc123",
			defaultValue: 2 * time.Second,
			expected:     2 * time.Second,
		},
		{
			desc:         "invalid content, negative value",
			envValue:     "-111",
			defaultValue: 2 * time.Second,
			expected:     2 * time.Second,
		},
		{
			desc:         "float: invalid type, use default value",
			envValue:     "1.11",
			defaultValue: 2 * time.Second,
			expected:     2 * time.Second,
		},
	}

	key := "LEGO_ENV_TC"

	for _, test := range testCases {
		t.Run(test.desc, func(t *testing.T) {
			defer os.Unsetenv(key)
			err := os.Setenv(key, test.envValue)
			require.NoError(t, err)

			result := GetOrDefaultSecond(key, test.defaultValue)
			assert.Equal(t, test.expected, result)
		})
	}
}

func TestGetOrDefaultString(t *testing.T) {
	testCases := []struct {
		desc         string
		envValue     string
		defaultValue string
		expected     string
	}{
		{
			desc:         "missing env var",
			defaultValue: "foo",
			expected:     "foo",
		},
		{
			desc:         "with env var",
			envValue:     "bar",
			defaultValue: "foo",
			expected:     "bar",
		},
	}

	key := "LEGO_ENV_TC"

	for _, test := range testCases {
		t.Run(test.desc, func(t *testing.T) {
			defer os.Unsetenv(key)
			err := os.Setenv(key, test.envValue)
			require.NoError(t, err)

			actual := GetOrDefaultString(key, test.defaultValue)
			assert.Equal(t, test.expected, actual)
		})
	}
}

func TestGetOrDefaultBool(t *testing.T) {
	testCases := []struct {
		desc         string
		envValue     string
		defaultValue bool
		expected     bool
	}{
		{
			desc:         "missing env var",
			defaultValue: true,
			expected:     true,
		},
		{
			desc:         "with env var",
			envValue:     "true",
			defaultValue: false,
			expected:     true,
		},
		{
			desc:         "invalid value",
			envValue:     "foo",
			defaultValue: false,
			expected:     false,
		},
	}

	key := "LEGO_ENV_TC"

	for _, test := range testCases {
		t.Run(test.desc, func(t *testing.T) {
			defer os.Unsetenv(key)
			err := os.Setenv(key, test.envValue)
			require.NoError(t, err)

			actual := GetOrDefaultBool(key, test.defaultValue)
			assert.Equal(t, test.expected, actual)
		})
	}
}

func TestGetOrFile_ReadsEnvVars(t *testing.T) {
	err := os.Setenv("TEST_LEGO_ENV_VAR", "lego_env")
	require.NoError(t, err)
	defer os.Unsetenv("TEST_LEGO_ENV_VAR")

	value := GetOrFile("TEST_LEGO_ENV_VAR")

	assert.Equal(t, "lego_env", value)
}

func TestGetOrFile_ReadsFiles(t *testing.T) {
	varEnvFileName := "TEST_LEGO_ENV_VAR_FILE"
	varEnvName := "TEST_LEGO_ENV_VAR"

	testCases := []struct {
		desc        string
		fileContent []byte
	}{
		{
			desc:        "simple",
			fileContent: []byte("lego_file"),
		},
		{
			desc:        "with an empty last line",
			fileContent: []byte("lego_file\n"),
		},
	}

	for _, test := range testCases {
		t.Run(test.desc, func(t *testing.T) {
			err := os.Unsetenv(varEnvFileName)
			require.NoError(t, err)
			err = os.Unsetenv(varEnvName)
			require.NoError(t, err)

			file, err := ioutil.TempFile("", "lego")
			require.NoError(t, err)
			defer os.Remove(file.Name())

			err = os.WriteFile(file.Name(), []byte("lego_file\n"), 0o644)
			require.NoError(t, err)

			err = os.Setenv(varEnvFileName, file.Name())
			require.NoError(t, err)
			defer os.Unsetenv(varEnvFileName)

			value := GetOrFile(varEnvName)

			assert.Equal(t, "lego_file", value)
		})
	}
}

func TestGetOrFile_PrefersEnvVars(t *testing.T) {
	varEnvFileName := "TEST_LEGO_ENV_VAR_FILE"
	varEnvName := "TEST_LEGO_ENV_VAR"

	err := os.Unsetenv(varEnvFileName)
	require.NoError(t, err)
	err = os.Unsetenv(varEnvName)
	require.NoError(t, err)

	file, err := ioutil.TempFile("", "lego")
	require.NoError(t, err)
	defer os.Remove(file.Name())

	err = os.WriteFile(file.Name(), []byte("lego_file"), 0o644)
	require.NoError(t, err)

	err = os.Setenv(varEnvFileName, file.Name())
	require.NoError(t, err)
	defer os.Unsetenv(varEnvFileName)

	err = os.Setenv(varEnvName, "lego_env")
	require.NoError(t, err)
	defer os.Unsetenv(varEnvName)

	value := GetOrFile(varEnvName)

	assert.Equal(t, "lego_env", value)
}