package loader

import (
	"context"
	"errors"
	"os"
	"path/filepath"
	"testing"

	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/aisupport/packages/sdk-go/infra/config"
	"github.com/tidbcloud/aisupport/packages/sdk-go/infra/secret"
	"go.uber.org/mock/gomock"
)

type testConfig struct {
	Name         string        `yaml:"name" validate:"required"`
	Port         int           `yaml:"port" default:"8080"`
	Nested       testNested    `yaml:"nested"`
	SecretName   string        `yaml:"secretName"`
	SecretConfig config.Secret `yaml:"secret"`
}

type testNested struct {
	Value string `yaml:"value"`
	Flag  bool   `yaml:"flag"`
}

func TestConfigurationLoaderMergesSourcesInOrder(t *testing.T) {
	t.Parallel()

	ctrl := gomock.NewController(t)
	base := NewMockConfigurationSource[testConfig](ctrl)
	overlay := NewMockConfigurationSource[testConfig](ctrl)
	nilSource := NewMockConfigurationSource[testConfig](ctrl)

	base.EXPECT().
		Load(gomock.Any(), gomock.AssignableToTypeOf(&testConfig{})).
		DoAndReturn(func(_ context.Context, pre *testConfig) (*testConfig, error) {
			require.Empty(t, pre.Name)
			return &testConfig{
				Name: "base",
				Port: 9090,
				Nested: testNested{
					Value: "base-value",
					Flag:  true,
				},
			}, nil
		})
	overlay.EXPECT().
		Load(gomock.Any(), gomock.AssignableToTypeOf(&testConfig{})).
		DoAndReturn(func(_ context.Context, pre *testConfig) (*testConfig, error) {
			require.Equal(t, "base", pre.Name)
			require.Equal(t, 9090, pre.Port)
			return &testConfig{
				Name: "overlay",
				Nested: testNested{
					Value: "overlay-value",
				},
			}, nil
		})
	nilSource.EXPECT().
		Load(gomock.Any(), gomock.AssignableToTypeOf(&testConfig{})).
		Return(nil, nil)

	conf, err := NewConfigurationLoader(
		[]ConfigurationSource[testConfig]{base, overlay, nilSource},
		false,
	).Load(context.Background())

	require.NoError(t, err)
	require.Equal(t, "overlay", conf.Name)
	require.Equal(t, 9090, conf.Port)
	require.Equal(t, "overlay-value", conf.Nested.Value)
	require.True(t, conf.Nested.Flag)
}

func TestConfigurationLoaderReturnsSourceError(t *testing.T) {
	t.Parallel()

	ctrl := gomock.NewController(t)
	source := NewMockConfigurationSource[testConfig](ctrl)
	expectedErr := errors.New("load failed")
	source.EXPECT().
		Load(gomock.Any(), gomock.AssignableToTypeOf(&testConfig{})).
		Return(nil, expectedErr)

	conf, err := NewConfigurationLoader(
		[]ConfigurationSource[testConfig]{source},
		true,
	).Load(context.Background())

	require.ErrorIs(t, err, expectedErr)
	require.Nil(t, conf)
}

func TestConfigurationLoaderValidatesConfig(t *testing.T) {
	t.Parallel()

	conf, err := NewConfigurationLoader(
		[]ConfigurationSource[testConfig]{
			NewConfigurationSourceMemory(&testConfig{Port: 8080}),
		},
		false,
	).Load(context.Background())

	require.Error(t, err)
	require.ErrorContains(t, err, "failed to validate config")
	require.Nil(t, conf)
}

func TestConfigurationLoaderCanSkipValidation(t *testing.T) {
	t.Parallel()

	conf, err := NewConfigurationLoader(
		[]ConfigurationSource[testConfig]{
			NewConfigurationSourceMemory(&testConfig{Port: 8080}),
		},
		true,
	).Load(context.Background())

	require.NoError(t, err)
	require.NotNil(t, conf)
	require.Empty(t, conf.Name)
}

func TestConfigurationSourceDefaultPopulatesDefaults(t *testing.T) {
	t.Parallel()

	conf, err := NewConfigurationSourceDefault[testConfig]().Load(context.Background(), &testConfig{})

	require.NoError(t, err)
	require.NotNil(t, conf)
	require.Equal(t, 8080, conf.Port)
}

func TestConfigurationSourceMemoryReturnsConfig(t *testing.T) {
	t.Parallel()

	expected := &testConfig{Name: "memory", Port: 9090}

	conf, err := NewConfigurationSourceMemory(expected).Load(context.Background(), &testConfig{})

	require.NoError(t, err)
	require.Same(t, expected, conf)
}

func TestConfigurationSourceMemoryReturnsNilConfig(t *testing.T) {
	t.Parallel()

	conf, err := NewConfigurationSourceMemory[testConfig](nil).Load(context.Background(), &testConfig{})

	require.NoError(t, err)
	require.Nil(t, conf)
}

func TestConfigurationSourceFileLoadsYAML(t *testing.T) {
	t.Parallel()

	path := writeTempConfig(t, `
name: file
port: 9090
nested:
  value: from-file
  flag: true
secretName: root/config
secret:
  awsASM:
    region: us-east-1
`)

	conf, err := NewConfigurationSourceFile[testConfig](path).Load(context.Background(), &testConfig{})

	require.NoError(t, err)
	require.Equal(t, "file", conf.Name)
	require.Equal(t, 9090, conf.Port)
	require.Equal(t, "from-file", conf.Nested.Value)
	require.True(t, conf.Nested.Flag)
	require.Equal(t, "root/config", conf.SecretName)
	require.NotNil(t, conf.SecretConfig)
	require.NotNil(t, conf.SecretConfig.AWSASM)
	require.Equal(t, "us-east-1", conf.SecretConfig.AWSASM.Region)
}

func TestConfigurationSourceFileSkipsEmptyPath(t *testing.T) {
	t.Parallel()

	conf, err := NewConfigurationSourceFile[testConfig]("").Load(context.Background(), &testConfig{})

	require.NoError(t, err)
	require.Nil(t, conf)
}

func TestConfigurationSourceFileReturnsReadError(t *testing.T) {
	t.Parallel()

	conf, err := NewConfigurationSourceFile[testConfig](
		filepath.Join(t.TempDir(), "missing.yaml"),
	).Load(context.Background(), &testConfig{})

	require.Error(t, err)
	require.ErrorContains(t, err, "failed to read config file")
	require.Nil(t, conf)
}

func TestConfigurationSourceFileReturnsUnmarshalError(t *testing.T) {
	t.Parallel()

	path := writeTempConfig(t, "name: [")

	conf, err := NewConfigurationSourceFile[testConfig](path).Load(context.Background(), &testConfig{})

	require.Error(t, err)
	require.ErrorContains(t, err, "failed to unmarshal config")
	require.Nil(t, conf)
}

func TestConfigurationSourceSecretSkipsEmptySecretName(t *testing.T) {
	t.Parallel()

	source := NewConfigurationSourceSecret(func(pre *testConfig) (string, config.Secret) {
		require.Equal(t, "from-pre", pre.Name)
		return "", config.Secret{}
	})

	conf, err := source.Load(context.Background(), &testConfig{Name: "from-pre"})

	require.NoError(t, err)
	require.Nil(t, conf)
}

func TestConfigurationSourceSecretLoadsYAML(t *testing.T) {
	t.Parallel()

	ctrl := gomock.NewController(t)
	mockSecret := secret.NewMockSecretSource(ctrl)
	mockSecret.EXPECT().
		GetSecretValue(gomock.Any(), "root/config").
		Return("name: from-secret\nnested:\n  value: secret-value\n", nil)

	source := NewConfigurationSourceSecret(func(pre *testConfig) (string, config.Secret) {
		require.Equal(t, "base", pre.Name)
		return pre.SecretName, pre.SecretConfig
	})
	source.newSecretSource = func(conf config.Secret) (secret.SecretSource, error) {
		require.NotNil(t, conf.AWSASM)
		require.Equal(t, "us-west-2", conf.AWSASM.Region)
		return mockSecret, nil
	}

	conf, err := source.Load(context.Background(), &testConfig{
		Name:       "base",
		SecretName: "root/config",
		SecretConfig: config.Secret{
			AWSASM: &config.SecretAWSASM{Region: "us-west-2"},
		},
	})

	require.NoError(t, err)
	require.Equal(t, "from-secret", conf.Name)
	require.Equal(t, "secret-value", conf.Nested.Value)
}

func TestConfigurationSourceSecretReturnsGetterError(t *testing.T) {
	t.Parallel()

	conf, err := (&ConfigurationSourceSecret[testConfig]{}).Load(context.Background(), &testConfig{})

	require.Error(t, err)
	require.ErrorContains(t, err, "secret config getter is nil")
	require.Nil(t, conf)
}

func TestConfigurationSourceSecretReturnsBuildError(t *testing.T) {
	t.Parallel()

	expectedErr := errors.New("unsupported")
	source := NewConfigurationSourceSecret(func(_ *testConfig) (string, config.Secret) {
		return "root/config", config.Secret{}
	})
	source.newSecretSource = func(config.Secret) (secret.SecretSource, error) {
		return nil, expectedErr
	}

	conf, err := source.Load(context.Background(), &testConfig{})

	require.ErrorIs(t, err, expectedErr)
	require.ErrorContains(t, err, "build secret source failed")
	require.Nil(t, conf)
}

func TestConfigurationSourceSecretReturnsReadError(t *testing.T) {
	t.Parallel()

	ctrl := gomock.NewController(t)
	expectedErr := errors.New("secret unavailable")
	mockSecret := secret.NewMockSecretSource(ctrl)
	mockSecret.EXPECT().
		GetSecretValue(gomock.Any(), "root/config").
		Return("", expectedErr)

	source := NewConfigurationSourceSecret(func(_ *testConfig) (string, config.Secret) {
		return "root/config", config.Secret{AWSASM: &config.SecretAWSASM{}}
	})
	source.newSecretSource = func(config.Secret) (secret.SecretSource, error) {
		return mockSecret, nil
	}

	conf, err := source.Load(context.Background(), &testConfig{})

	require.ErrorIs(t, err, expectedErr)
	require.ErrorContains(t, err, "failed to get secret value of root config")
	require.Nil(t, conf)
}

func TestConfigurationSourceSecretReturnsUnmarshalError(t *testing.T) {
	t.Parallel()

	ctrl := gomock.NewController(t)
	mockSecret := secret.NewMockSecretSource(ctrl)
	mockSecret.EXPECT().
		GetSecretValue(gomock.Any(), "root/config").
		Return("name: [", nil)

	source := NewConfigurationSourceSecret(func(_ *testConfig) (string, config.Secret) {
		return "root/config", config.Secret{AWSASM: &config.SecretAWSASM{}}
	})
	source.newSecretSource = func(config.Secret) (secret.SecretSource, error) {
		return mockSecret, nil
	}

	conf, err := source.Load(context.Background(), &testConfig{})

	require.Error(t, err)
	require.ErrorContains(t, err, "failed to unmarshal secret")
	require.Nil(t, conf)
}

func writeTempConfig(t *testing.T, content string) string {
	t.Helper()

	path := filepath.Join(t.TempDir(), "config.yaml")
	require.NoError(t, os.WriteFile(path, []byte(content), 0o600))
	return path
}
