package config

import (
	"context"
	"os"
	"path/filepath"
	"sync"
	"testing"
	"time"

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

type mockSecretFetcher struct {
	secretYaml       string
	err              error
	calledWithKey    string
	calledWithRegion string
}

func (m *mockSecretFetcher) GetSecret(_ context.Context, secretKey, region string) (string, error) {
	m.calledWithKey = secretKey
	m.calledWithRegion = region
	if m.err != nil {
		return "", m.err
	}
	return m.secretYaml, nil
}

func setupTestConfigFile(t *testing.T, content string) (string, func()) {
	t.Helper() // Marks this as a test helper function
	dir, err := os.MkdirTemp("", "config-test-dir")
	require.NoError(t, err)

	// We use a specific file name as viper relies on it
	filePath := filepath.Join(dir, "config.yaml")
	err = os.WriteFile(filePath, []byte(content), 0644)
	require.NoError(t, err)

	cleanup := func() {
		_ = os.RemoveAll(dir)
	}
	return filePath, cleanup
}

func TestProvider_Load_Success(t *testing.T) {
	t.Parallel()
	fileContent := `
env: "file-env" # will be overridden by secret
server:
  logLevel: "info" # will be overridden by secret
  httpPort: 12345 # new value from file
  secret:
    secretKey: "my-secret-key"
    region: "us-east-1"
o11y:
  globalServerDomain: "domain.com"
  bizType:            "biz"
  auth0Domain:        "auth0.com"
  auth0ClientID:      "id"
  auth0Audience:      "aud"
`
	configPath, cleanup := setupTestConfigFile(t, fileContent)
	defer cleanup()
	secretContent := `
env: "secret-env" # should win
server:
  logLevel: "debug" # should win
  grpcPort: 9999    # should merge
`
	mockFetcher := &mockSecretFetcher{
		secretYaml: secretContent,
	}
	provider, err := newProvider(configPath, mockFetcher)
	require.NoError(t, err)
	conf := provider.Get()
	require.NotNil(t, conf)
	assert.Equal(t, "my-secret-key", mockFetcher.calledWithKey)
	assert.Equal(t, "us-east-1", mockFetcher.calledWithRegion)
	assert.Equal(t, "kubernetes:///account-provider-grpc:10000", conf.GrpcConfig.AccountProviderAddr)
	assert.Equal(t, 12345, conf.ServerConfig.HttpPort)
	assert.Equal(t, "domain.com", conf.O11yConfig.GlobalServerDomain)
	assert.Equal(t, 9999, conf.ServerConfig.GrpcPort)
	assert.Equal(t, "debug", conf.ServerConfig.LogLevel)
	assert.Equal(t, "secret-env", conf.Env)
}

func TestProvider_Load_ValidationFailure(t *testing.T) {
	t.Parallel()
	fileContent := `
o11y:
  bizType: "test-biz"
  # Missing: globalServerDomain, auth0Domain, etc.
`
	configPath, cleanup := setupTestConfigFile(t, fileContent)
	defer cleanup()

	mockFetcher := &mockSecretFetcher{}
	_, err := newProvider(configPath, mockFetcher)

	require.Error(t, err)
	assert.Contains(t, err.Error(), "config validation failed", "Error message should indicate validation failure")
	assert.Contains(t, err.Error(), "O11yConfig.GlobalServerDomain", "Error message should specify the failed field")
}

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

	validFileWithSecret := `
server:
  httpPort: 80
  secret:
    secretKey: "key"
    region: "region"
o11y:
  globalServerDomain: "domain.com"
  bizType:            "biz"
  auth0Domain:        "auth0.com"
  auth0ClientID:      "id"
  auth0Audience:      "aud"
`

	testCases := []struct {
		name          string
		fileContent   string
		configPath    string
		mockFetcher   *mockSecretFetcher
		expectedError string
	}{
		{
			name:          "Secret Fetch Failure",
			fileContent:   validFileWithSecret,
			mockFetcher:   &mockSecretFetcher{err: errors.New("aws network timeout")},
			expectedError: "failed to load secret config",
		},
		{
			name:          "Secret YAML Parse Failure",
			fileContent:   validFileWithSecret,
			mockFetcher:   &mockSecretFetcher{secretYaml: `not: yaml: : {`}, // Invalid YAML
			expectedError: "failed to unmarshal secret yaml",
		},
		{
			name:          "Secret Mapstructure Failure",
			fileContent:   validFileWithSecret,
			mockFetcher:   &mockSecretFetcher{secretYaml: `server: { logLevel: { nested: "true" } }`},
			expectedError: "failed to mapstructure secret config",
		},
		{
			name:          "Config File Read Failure (Not 'Not Found')",
			configPath:    `not exits`,
			mockFetcher:   &mockSecretFetcher{},
			expectedError: "failed to read config file",
		},
		{
			name:          "Config File Unmarshal Failure",
			fileContent:   `server: { logLevel: { nested: "true" } }`, // Bad structure in file
			mockFetcher:   &mockSecretFetcher{},
			expectedError: "failed to unmarshal from viper",
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			configPath := tc.configPath
			if configPath == "" {
				path, cleanup := setupTestConfigFile(t, tc.fileContent)
				defer cleanup()
				configPath = path
			}
			_, err := newProvider(configPath, tc.mockFetcher)

			require.Error(t, err, "Expected an error but got nil")
			assert.Contains(t, err.Error(), tc.expectedError, "Error message mismatch")
		})
	}
}

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

	configPath, cleanup := setupTestConfigFile(t, "") // Empty content
	defer cleanup()
	mockFetcher := &mockSecretFetcher{}
	provider, err := newProvider(configPath, mockFetcher)

	require.Error(t, err)
	assert.Nil(t, provider) // Provider should not be returned on error
	assert.Contains(t, err.Error(), "config validation failed")
	assert.Contains(t, err.Error(), "O11yConfig.GlobalServerDomain")
}

func TestProvider_DynamicReload(t *testing.T) {
	fileContent := `
server:
  logLevel: "info"
o11y:
  globalServerDomain: "domain.com"
  bizType:            "biz"
  auth0Domain:        "auth0.com"
  auth0ClientID:      "id"
  auth0Audience:      "aud"
`
	configPath, cleanup := setupTestConfigFile(t, fileContent)
	defer cleanup()

	mockFetcher := &mockSecretFetcher{}

	provider, err := newProvider(configPath, mockFetcher)
	require.NoError(t, err)
	assert.Equal(t, "info", provider.Get().ServerConfig.LogLevel)

	newFileContent := `
server:
  logLevel: "debug" # Changed value
o11y:
  globalServerDomain: "domain.com"
  bizType:            "biz"
  auth0Domain:        "auth0.com"
  auth0ClientID:      "id"
  auth0Audience:      "aud"
`
	time.Sleep(100 * time.Millisecond)
	err = os.WriteFile(configPath, []byte(newFileContent), 0644)
	require.NoError(t, err)

	assert.Eventually(t, func() bool {
		conf := provider.Get()
		return conf.ServerConfig.LogLevel == "debug"
	}, 2*time.Second, 100*time.Millisecond, "config was not reloaded")

	conf := provider.Get()
	assert.Equal(t, "debug", conf.ServerConfig.LogLevel)
}

func TestGlobal_Init_And_Get(t *testing.T) {
	globalProvider = nil
	initOnce = sync.Once{}

	fileContent := `
env: "test"
o11y:
  globalServerDomain: "domain.com"
  bizType:            "biz"
  auth0Domain:        "auth0.com"
  auth0ClientID:      "id"
  auth0Audience:      "aud"
`
	configPath, cleanup := setupTestConfigFile(t, fileContent)
	defer cleanup()

	err := InitConfig(configPath)
	require.NoError(t, err)

	conf := Global()
	require.NotNil(t, conf)
	assert.Equal(t, "test", conf.Env)
	assert.Equal(t, "info", conf.ServerConfig.LogLevel)

	err = InitConfig("./different-path.yaml")
	require.NoError(t, err, "Second InitConfig call should not fail")
	conf2 := Global()
	assert.Equal(t, "test", conf2.Env)
	assert.Same(t, conf, conf2)
}

// TestGlobal_PanicIfNotInitialized tests that Global() panics if InitConfig was not called
func TestGlobal_PanicIfNotInitialized(t *testing.T) {
	globalProvider = nil
	initOnce = sync.Once{}
	assert.Panics(t, func() {
		Global()
	}, "Global() should panic if InitConfig was not called")
}
