package fflag_test

import (
	"os"
	"strings"
	"testing"

	"github.com/sirupsen/logrus"
	logtest "github.com/sirupsen/logrus/hooks/test"
	"github.com/stretchr/testify/require"

	"github.com/crowdsecurity/go-cs-lib/cstest"

	"github.com/crowdsecurity/crowdsec/pkg/fflag"
)

func TestRegisterFeature(t *testing.T) {
	tests := []struct {
		name        string
		feature     fflag.Feature
		expectedErr string
	}{
		{
			name: "a plain feature",
			feature: fflag.Feature{
				Name: "plain",
			},
		},
		{
			name: "capitalized feature name",
			feature: fflag.Feature{
				Name: "Plain",
			},
			expectedErr: "feature flag 'Plain': name is not lowercase",
		},
		{
			name: "empty feature name",
			feature: fflag.Feature{
				Name: "",
			},
			expectedErr: "feature flag '': name is empty",
		},
		{
			name: "invalid feature name",
			feature: fflag.Feature{
				Name: "meh!",
			},
			expectedErr: "feature flag 'meh!': invalid name (allowed a-z, 0-9, _, .)",
		},
	}

	for _, tc := range tests {
		t.Run("", func(t *testing.T) {
			fr := fflag.FeatureRegister{EnvPrefix: "FFLAG_TEST_"}
			err := fr.RegisterFeature(&tc.feature)
			cstest.RequireErrorContains(t, err, tc.expectedErr)
		})
	}
}

func setUp(t *testing.T) fflag.FeatureRegister {
	t.Helper()

	fr := fflag.FeatureRegister{EnvPrefix: "FFLAG_TEST_"}

	err := fr.RegisterFeature(&fflag.Feature{Name: "experimental1"})
	require.NoError(t, err)

	err = fr.RegisterFeature(&fflag.Feature{
		Name:        "some_feature",
		Description: "A feature that does something, with a description",
	})
	require.NoError(t, err)

	err = fr.RegisterFeature(&fflag.Feature{
		Name:           "new_standard",
		State:          fflag.DeprecatedState,
		Description:    "This implements the new standard T34.256w",
		DeprecationMsg: "In 2.0 we'll do T34.256w by default",
	})
	require.NoError(t, err)

	err = fr.RegisterFeature(&fflag.Feature{
		Name:           "was_adopted",
		State:          fflag.RetiredState,
		Description:    "This implements a new tricket",
		DeprecationMsg: "The trinket was implemented in 1.5",
	})
	require.NoError(t, err)

	return fr
}

func TestGetFeature(t *testing.T) {
	tests := []struct {
		name        string
		feature     string
		expectedErr string
	}{
		{
			name:    "just a feature",
			feature: "experimental1",
		}, {
			name:        "feature that does not exist",
			feature:     "will_never_exist",
			expectedErr: "unknown feature",
		},
	}

	fr := setUp(t)

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			_, err := fr.GetFeature(tc.feature)
			cstest.RequireErrorMessage(t, err, tc.expectedErr)
			if tc.expectedErr != "" {
				return
			}
		})
	}
}

func TestIsEnabled(t *testing.T) {
	tests := []struct {
		name     string
		feature  string
		enable   bool
		expected bool
	}{
		{
			name:     "feature that was not enabled",
			feature:  "experimental1",
			expected: false,
		}, {
			name:     "feature that was enabled",
			feature:  "experimental1",
			enable:   true,
			expected: true,
		},
	}

	fr := setUp(t)

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			feat, err := fr.GetFeature(tc.feature)
			require.NoError(t, err)

			err = feat.Set(tc.enable)
			require.NoError(t, err)

			require.Equal(t, tc.expected, feat.IsEnabled())
		})
	}
}

func TestFeatureSet(t *testing.T) {
	tests := []struct {
		name           string // test description
		feature        string // feature name
		value          bool   // value for SetFeature
		expected       bool   // expected value from IsEnabled
		expectedSetErr string // error expected from SetFeature
		expectedGetErr string // error expected from GetFeature
	}{
		{
			name:     "enable a feature to try something new",
			feature:  "experimental1",
			value:    true,
			expected: true,
		}, {
			// not useful in practice, unlikely to happen
			name:     "disable the feature that was enabled",
			feature:  "experimental1",
			value:    false,
			expected: false,
		}, {
			name:           "enable a feature that will be retired in v2",
			feature:        "new_standard",
			value:          true,
			expected:       true,
			expectedSetErr: "the flag is deprecated",
		}, {
			name:           "enable a feature that was retired in v1.5",
			feature:        "was_adopted",
			value:          true,
			expected:       false,
			expectedSetErr: "the flag is retired",
		}, {
			name:           "enable a feature that does not exist",
			feature:        "will_never_exist",
			value:          true,
			expectedSetErr: "unknown feature",
			expectedGetErr: "unknown feature",
		},
	}

	// the tests are not indepedent because we don't instantiate a feature
	// map for each one, but it simplified the code
	fr := setUp(t)

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			feat, err := fr.GetFeature(tc.feature)
			cstest.RequireErrorMessage(t, err, tc.expectedGetErr)
			if tc.expectedGetErr != "" {
				return
			}

			err = feat.Set(tc.value)
			cstest.RequireErrorMessage(t, err, tc.expectedSetErr)
			require.Equal(t, tc.expected, feat.IsEnabled())
		})
	}
}

func TestSetFromEnv(t *testing.T) {
	tests := []struct {
		name   string
		envvar string
		value  string
		// expected bool
		expectedLog []string
		expectedErr string
	}{
		{
			name:   "variable that does not start with FFLAG_TEST_",
			envvar: "PATH",
			value:  "/bin:/usr/bin/:/usr/local/bin",
			// silently ignored
		}, {
			name:        "enable a feature flag",
			envvar:      "FFLAG_TEST_EXPERIMENTAL1",
			value:       "true",
			expectedLog: []string{"Feature flag: experimental1=true (from envvar)"},
		}, {
			name:        "invalid value (not true or false)",
			envvar:      "FFLAG_TEST_EXPERIMENTAL1",
			value:       "maybe",
			expectedLog: []string{"Ignored envvar FFLAG_TEST_EXPERIMENTAL1=maybe: invalid value (must be 'true' or 'false')"},
		}, {
			name:        "feature flag that is unknown",
			envvar:      "FFLAG_TEST_WILL_NEVER_EXIST",
			value:       "true",
			expectedLog: []string{"Ignored envvar 'FFLAG_TEST_WILL_NEVER_EXIST': unknown feature"},
		}, {
			name:   "enable a feature flag with a description",
			envvar: "FFLAG_TEST_SOME_FEATURE",
			value:  "true",
			expectedLog: []string{
				"Feature flag: some_feature=true (from envvar). A feature that does something, with a description",
			},
		}, {
			name:   "enable a deprecated feature",
			envvar: "FFLAG_TEST_NEW_STANDARD",
			value:  "true",
			expectedLog: []string{
				"Envvar 'FFLAG_TEST_NEW_STANDARD': the flag is deprecated. In 2.0 we'll do T34.256w by default",
				"Feature flag: new_standard=true (from envvar). This implements the new standard T34.256w",
			},
		}, {
			name:   "enable a feature that was retired in v1.5",
			envvar: "FFLAG_TEST_WAS_ADOPTED",
			value:  "true",
			expectedLog: []string{
				"Ignored envvar 'FFLAG_TEST_WAS_ADOPTED': the flag is retired. " +
					"The trinket was implemented in 1.5",
			},
		}, {
			// this could happen in theory, but only if environment variables
			// are parsed after configuration files, which is not a good idea
			// because they are more useful asap
			name:   "disable a feature flag already set",
			envvar: "FFLAG_TEST_EXPERIMENTAL1",
			value:  "false",
		},
	}

	fr := setUp(t)

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			logger, hook := logtest.NewNullLogger()
			logger.SetLevel(logrus.DebugLevel)
			t.Setenv(tc.envvar, tc.value)
			err := fr.SetFromEnv(logger)
			cstest.RequireErrorMessage(t, err, tc.expectedErr)
			for _, expectedMessage := range tc.expectedLog {
				cstest.RequireLogContains(t, hook, expectedMessage)
			}
		})
	}
}

func TestSetFromYaml(t *testing.T) {
	tests := []struct {
		name        string
		yml         string
		expectedLog []string
		expectedErr string
	}{
		{
			name: "empty file",
			yml:  "",
			// no error
		}, {
			name:        "invalid yaml",
			yml:         "bad! content, bad!",
			expectedErr: "failed to parse feature flags: [1:1] string was used where sequence is expected\n>  1 | bad! content, bad!\n       ^\n",
		}, {
			name:        "invalid feature flag name",
			yml:         "- not_a_feature",
			expectedLog: []string{"Ignored feature flag 'not_a_feature': unknown feature"},
		}, {
			name:        "invalid value (must be a list)",
			yml:         "experimental1: true",
			expectedErr: "failed to parse feature flags: [1:14] mapping was used where sequence is expected\n>  1 | experimental1: true\n                    ^\n",
		}, {
			name:        "enable a feature flag",
			yml:         "- experimental1",
			expectedLog: []string{"Feature flag: experimental1=true (from config file)"},
		}, {
			name: "enable a deprecated feature",
			yml:  "- new_standard",
			expectedLog: []string{
				"Feature 'new_standard': the flag is deprecated. In 2.0 we'll do T34.256w by default",
				"Feature flag: new_standard=true (from config file). This implements the new standard T34.256w",
			},
		}, {
			name: "enable a retired feature",
			yml:  "- was_adopted",
			expectedLog: []string{
				"Ignored feature flag 'was_adopted': the flag is retired. The trinket was implemented in 1.5",
			},
		},
	}

	fr := setUp(t)

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			logger, hook := logtest.NewNullLogger()
			logger.SetLevel(logrus.DebugLevel)
			err := fr.SetFromYaml(strings.NewReader(tc.yml), logger)
			cstest.RequireErrorMessage(t, err, tc.expectedErr)
			for _, expectedMessage := range tc.expectedLog {
				cstest.RequireLogContains(t, hook, expectedMessage)
			}
		})
	}
}

func TestSetFromYamlFile(t *testing.T) {
	tmpfile, err := os.CreateTemp(t.TempDir(), "test")
	require.NoError(t, err)

	// write the config file
	_, err = tmpfile.WriteString("- experimental1")
	require.NoError(t, err)
	require.NoError(t, tmpfile.Close())

	fr := setUp(t)
	logger, hook := logtest.NewNullLogger()
	logger.SetLevel(logrus.DebugLevel)

	err = fr.SetFromYamlFile(tmpfile.Name(), logger)
	require.NoError(t, err)

	cstest.RequireLogContains(t, hook, "Feature flag: experimental1=true (from config file)")
}

func TestGetEnabledFeatures(t *testing.T) {
	fr := setUp(t)

	feat1, err := fr.GetFeature("new_standard")
	require.NoError(t, err)
	err = feat1.Set(true)
	require.Error(t, err, "the flag is deprecated")

	feat2, err := fr.GetFeature("experimental1")
	require.NoError(t, err)
	err = feat2.Set(true)
	require.NoError(t, err)

	expected := []string{
		"experimental1",
		"new_standard",
	}

	require.Equal(t, expected, fr.GetEnabledFeatures())
}
