package config

import (
	"bytes"
	"fmt"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"testing"
	"time"

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

func TestEnvironmentSubstitution(t *testing.T) {
	tests := []struct {
		name         string
		setEnv       func(*testing.T)
		contents     string
		expected     string
		wantErr      bool
		errSubstring string
	}{
		{
			name: "Legacy with ${} and without {}",
			setEnv: func(t *testing.T) {
				t.Setenv("TEST_ENV1", "VALUE1")
				t.Setenv("TEST_ENV2", "VALUE2")
			},
			contents: "A string with ${TEST_ENV1}, $TEST_ENV2 and $TEST_ENV1 as repeated",
			expected: "A string with VALUE1, VALUE2 and VALUE1 as repeated",
		},
		{
			name:     "Env not set",
			contents: "Env variable ${NOT_SET} will be empty",
			expected: "Env variable ${NOT_SET} will be empty",
		},
		{
			name:     "Env not set, fallback to default",
			contents: "Env variable ${THIS_IS_ABSENT:-Fallback}",
			expected: "Env variable Fallback",
		},
		{
			name: "No fallback",
			setEnv: func(t *testing.T) {
				t.Setenv("MY_ENV1", "VALUE1")
			},
			contents: "Env variable ${MY_ENV1:-Fallback}",
			expected: "Env variable VALUE1",
		},
		{
			name: "Mix and match",
			setEnv: func(t *testing.T) {
				t.Setenv("MY_VAR", "VALUE")
				t.Setenv("MY_VAR2", "VALUE2")
			},
			contents: "Env var ${MY_VAR} is set, with $MY_VAR syntax and default on this ${MY_VAR1:-Substituted}, no default on this ${MY_VAR2:-NoDefault}",
			expected: "Env var VALUE is set, with VALUE syntax and default on this Substituted, no default on this VALUE2",
		},
		{
			name: "empty but set",
			setEnv: func(t *testing.T) {
				t.Setenv("EMPTY", "")
			},
			contents: "Contains ${EMPTY} nothing",
			expected: "Contains  nothing",
		},
		{
			name:     "Default has special chars",
			contents: `Not recommended but supported ${MY_VAR:-Default with special chars Supported#$\"}`,
			expected: `Not recommended but supported Default with special chars Supported#$\"`, // values are escaped
		},
		{
			name:         "unset error",
			contents:     "Contains ${THIS_IS_NOT_SET?unset-error}",
			wantErr:      true,
			errSubstring: "unset-error",
		},
		{
			name: "env empty error",
			setEnv: func(t *testing.T) {
				t.Setenv("ENV_EMPTY", "")
			},
			contents:     "Contains ${ENV_EMPTY:?empty-error}",
			wantErr:      true,
			errSubstring: "empty-error",
		},
		{
			name: "Fallback as env variable",
			setEnv: func(t *testing.T) {
				t.Setenv("FALLBACK", "my-fallback")
			},
			contents: "Should output ${NOT_SET:-${FALLBACK}}",
			expected: "Should output my-fallback",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if tt.setEnv != nil {
				tt.setEnv(t)
			}
			actual, err := substituteEnvironment([]byte(tt.contents), false)
			if tt.wantErr {
				require.ErrorContains(t, err, tt.errSubstring)
				return
			}
			require.EqualValues(t, tt.expected, string(actual))
		})
	}
}

func TestEnvironmentSubstitutionOldBehavior(t *testing.T) {
	tests := []struct {
		name     string
		contents string
		expected string
	}{
		{
			name:     "not defined no brackets",
			contents: `my-da$tabase`,
			expected: `my-da$tabase`,
		},
		{
			name:     "not defined brackets",
			contents: `my-da${ta}base`,
			expected: `my-da${ta}base`,
		},
		{
			name:     "not defined no brackets double dollar",
			contents: `my-da$$tabase`,
			expected: `my-da$$tabase`,
		},
		{
			name:     "not defined no brackets backslash",
			contents: `my-da\$tabase`,
			expected: `my-da\$tabase`,
		},
		{
			name:     "not defined brackets backslash",
			contents: `my-da\${ta}base`,
			expected: `my-da\${ta}base`,
		},
		{
			name:     "no brackets and suffix",
			contents: `my-da$VARbase`,
			expected: `my-da$VARbase`,
		},
		{
			name:     "no brackets",
			contents: `my-da$VAR`,
			expected: `my-dafoobar`,
		},
		{
			name:     "brackets",
			contents: `my-da${VAR}base`,
			expected: `my-dafoobarbase`,
		},
		{
			name:     "no brackets double dollar",
			contents: `my-da$$VAR`,
			expected: `my-da$foobar`,
		},
		{
			name:     "brackets double dollar",
			contents: `my-da$${VAR}`,
			expected: `my-da$foobar`,
		},
		{
			name:     "no brackets backslash",
			contents: `my-da\$VAR`,
			expected: `my-da\foobar`,
		},
		{
			name:     "brackets backslash",
			contents: `my-da\${VAR}base`,
			expected: `my-da\foobarbase`,
		},
		{
			name:     "fallback",
			contents: `my-da${ta:-omg}base`,
			expected: `my-daomgbase`,
		},
		{
			name:     "fallback env",
			contents: `my-da${ta:-${FALLBACK}}base`,
			expected: `my-dadefaultbase`,
		},
		{
			name:     "regex substitution",
			contents: `${1}`,
			expected: `${1}`,
		},
		{
			name:     "empty but set",
			contents: "Contains ${EMPTY} nothing",
			expected: "Contains  nothing",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Setenv("VAR", "foobar")
			t.Setenv("FALLBACK", "default")
			t.Setenv("EMPTY", "")
			actual, err := substituteEnvironment([]byte(tt.contents), true)
			require.NoError(t, err)
			require.EqualValues(t, tt.expected, string(actual))
		})
	}
}

func TestEnvironmentSubstitutionNewBehavior(t *testing.T) {
	tests := []struct {
		name     string
		contents string
		expected string
	}{
		{
			name:     "not defined no brackets",
			contents: `my-da$tabase`,
			expected: `my-da$tabase`,
		},
		{
			name:     "not defined brackets",
			contents: `my-da${ta}base`,
			expected: `my-da${ta}base`,
		},
		{
			name:     "not defined no brackets double dollar",
			contents: `my-da$$tabase`,
			expected: `my-da$tabase`,
		},
		{
			name:     "not defined no brackets backslash",
			contents: `my-da\$tabase`,
			expected: `my-da\$tabase`,
		},
		{
			name:     "not defined brackets backslash",
			contents: `my-da\${ta}base`,
			expected: `my-da\${ta}base`,
		},
		{
			name:     "no brackets and suffix",
			contents: `my-da$VARbase`,
			expected: `my-da$VARbase`,
		},
		{
			name:     "no brackets",
			contents: `my-da$VAR`,
			expected: `my-dafoobar`,
		},
		{
			name:     "brackets",
			contents: `my-da${VAR}base`,
			expected: `my-dafoobarbase`,
		},
		{
			name:     "no brackets double dollar",
			contents: `my-da$$VAR`,
			expected: `my-da$VAR`,
		},
		{
			name:     "brackets double dollar",
			contents: `my-da$${VAR}`,
			expected: `my-da${VAR}`,
		},
		{
			name:     "no brackets backslash",
			contents: `my-da\$VAR`,
			expected: `my-da\foobar`,
		},
		{
			name:     "brackets backslash",
			contents: `my-da\${VAR}base`,
			expected: `my-da\foobarbase`,
		},
		{
			name:     "fallback",
			contents: `my-da${ta:-omg}base`,
			expected: `my-daomgbase`,
		},
		{
			name:     "fallback env",
			contents: `my-da${ta:-${FALLBACK}}base`,
			expected: `my-dadefaultbase`,
		},
		{
			name:     "regex substitution",
			contents: `${1}`,
			expected: `${1}`,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Setenv("VAR", "foobar")
			t.Setenv("FALLBACK", "default")
			actual, err := substituteEnvironment([]byte(tt.contents), false)
			require.NoError(t, err)
			require.EqualValues(t, tt.expected, string(actual))
		})
	}
}

func TestParseConfig(t *testing.T) {
	tests := []struct {
		name     string
		setEnv   func(*testing.T)
		contents string
		expected string
		errmsg   string
	}{
		{
			name: "empty var name",
			contents: `
# Environment variables can be used anywhere in this config file, simply surround
# them with ${}. For strings the variable must be within quotes (ie, "${STR_VAR}"),
# for numbers and booleans they should be plain (ie, ${INT_VAR}, ${BOOL_VAR})Should output ${NOT_SET:-${FALLBACK}}
`,
			expected: "\n\n\n\n",
		},
		{
			name: "comment in command (issue #13643)",
			contents: `
			[[inputs.exec]]
			  commands = ["echo \"abc#def\""]
			`,
			expected: `
			[[inputs.exec]]
			  commands = ["echo \"abc#def\""]
			`,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if tt.setEnv != nil {
				tt.setEnv(t)
			}
			tbl, err := parseConfig([]byte(tt.contents))
			if tt.errmsg != "" {
				require.ErrorContains(t, err, tt.errmsg)
				return
			}

			require.NoError(t, err)
			if len(tt.expected) > 0 {
				require.EqualValues(t, tt.expected, string(tbl.Data))
			}
		})
	}
}

func TestRemoveComments(t *testing.T) {
	// Read expectation
	expected, err := os.ReadFile(filepath.Join("testdata", "envvar_comments_expected.toml"))
	require.NoError(t, err)

	// Read the file and remove the comments
	buf, err := os.ReadFile(filepath.Join("testdata", "envvar_comments.toml"))
	require.NoError(t, err)
	removed, err := removeComments(buf)
	require.NoError(t, err)
	lines := bytes.Split(removed, []byte{'\n'})
	for i, line := range lines {
		lines[i] = bytes.TrimRight(line, " \t")
	}
	actual := bytes.Join(lines, []byte{'\n'})

	// Do the comparison
	require.Equal(t, string(expected), string(actual))
}

func TestURLRetries3Fails(t *testing.T) {
	httpLoadConfigRetryInterval = 0 * time.Second
	responseCounter := 0
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusNotFound)
		responseCounter++
	}))
	defer ts.Close()

	expected := fmt.Sprintf("error loading config file %s: retry 3 of 3 failed to retrieve remote config: 404 Not Found", ts.URL)

	c := NewConfig()
	err := c.LoadConfig(ts.URL)
	require.Error(t, err)
	require.Equal(t, expected, err.Error())
	require.Equal(t, 4, responseCounter)
}

func TestURLRetries3FailsThenPasses(t *testing.T) {
	httpLoadConfigRetryInterval = 0 * time.Second
	responseCounter := 0
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if responseCounter <= 2 {
			w.WriteHeader(http.StatusNotFound)
		} else {
			w.WriteHeader(http.StatusOK)
		}
		responseCounter++
	}))
	defer ts.Close()

	c := NewConfig()
	require.NoError(t, c.LoadConfig(ts.URL))
	require.Equal(t, 4, responseCounter)
}
