package config

import (
	"crypto/ed25519"
	"crypto/sha256"
	"crypto/x509"
	"encoding/pem"
	"os"
	"path/filepath"
	"sync"
	"testing"
	"time"

	"galaxy/redisconn"

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

var configEnvMu sync.Mutex

const (
	gatewayRedisMasterAddrEnvVar = "GATEWAY_REDIS_MASTER_ADDR"
	gatewayRedisPasswordEnvVar   = "GATEWAY_REDIS_PASSWORD"
)

const (
	defaultTestRedisMasterAddrValue = "127.0.0.1:6379"
	defaultTestRedisPasswordValue   = "secret"
	defaultTestBackendHTTPURL       = "http://127.0.0.1:8080"
	defaultTestBackendGRPCPushURL   = "127.0.0.1:8081"
	defaultTestBackendClientID      = "gw-test"
)

func TestLoadFromEnvAppliesBackendDefaults(t *testing.T) {
	configEnvMu.Lock()
	defer configEnvMu.Unlock()

	resetEnv(t)
	setBaseRequiredEnv(t)

	cfg, err := LoadFromEnv()
	require.NoError(t, err)

	assert.Equal(t, defaultShutdownTimeout, cfg.ShutdownTimeout)
	assert.Equal(t, defaultLogLevel, cfg.Logging.Level)

	assert.Equal(t, defaultTestBackendHTTPURL, cfg.Backend.HTTPBaseURL)
	assert.Equal(t, defaultTestBackendGRPCPushURL, cfg.Backend.GRPCPushURL)
	assert.Equal(t, defaultTestBackendClientID, cfg.Backend.GatewayClientID)
	assert.Equal(t, defaultBackendHTTPTimeout, cfg.Backend.HTTPTimeout)
	assert.Equal(t, defaultBackendPushReconnectBaseBackoff, cfg.Backend.PushReconnectBaseBackoff)
	assert.Equal(t, defaultBackendPushReconnectMaxBackoff, cfg.Backend.PushReconnectMaxBackoff)

	expectedRedis := redisconn.DefaultConfig()
	expectedRedis.MasterAddr = defaultTestRedisMasterAddrValue
	expectedRedis.Password = defaultTestRedisPasswordValue
	assert.Equal(t, expectedRedis, cfg.Redis)

	assert.Equal(t, defaultReplayRedisKeyPrefix, cfg.ReplayRedis.KeyPrefix)
	assert.Equal(t, defaultReplayRedisReserveTimeout, cfg.ReplayRedis.ReserveTimeout)
}

func TestLoadFromEnvBackendOverrides(t *testing.T) {
	configEnvMu.Lock()
	defer configEnvMu.Unlock()

	resetEnv(t)
	setBaseRequiredEnv(t)

	t.Setenv(backendHTTPURLEnvVar, "  http://backend.internal:9080/  ")
	t.Setenv(backendGRPCPushURLEnvVar, "backend.internal:9081")
	t.Setenv(backendGatewayClientIDEnvVar, "gw-prod-1")
	t.Setenv(backendHTTPTimeoutEnvVar, "7s")
	t.Setenv(backendPushReconnectBaseBackoffEnvVar, "750ms")
	t.Setenv(backendPushReconnectMaxBackoffEnvVar, "60s")

	cfg, err := LoadFromEnv()
	require.NoError(t, err)
	assert.Equal(t, "http://backend.internal:9080", cfg.Backend.HTTPBaseURL)
	assert.Equal(t, "backend.internal:9081", cfg.Backend.GRPCPushURL)
	assert.Equal(t, "gw-prod-1", cfg.Backend.GatewayClientID)
	assert.Equal(t, 7*time.Second, cfg.Backend.HTTPTimeout)
	assert.Equal(t, 750*time.Millisecond, cfg.Backend.PushReconnectBaseBackoff)
	assert.Equal(t, time.Minute, cfg.Backend.PushReconnectMaxBackoff)
}

func TestLoadFromEnvRejectsMissingBackendValues(t *testing.T) {
	cases := []struct {
		name    string
		mutate  func(t *testing.T)
		wantErr string
	}{
		{
			name:    "http url missing",
			mutate:  func(t *testing.T) { os.Unsetenv(backendHTTPURLEnvVar) },
			wantErr: backendHTTPURLEnvVar,
		},
		{
			name:    "grpc url missing",
			mutate:  func(t *testing.T) { os.Unsetenv(backendGRPCPushURLEnvVar) },
			wantErr: backendGRPCPushURLEnvVar,
		},
		{
			name:    "gateway client id missing",
			mutate:  func(t *testing.T) { os.Unsetenv(backendGatewayClientIDEnvVar) },
			wantErr: backendGatewayClientIDEnvVar,
		},
		{
			name:    "http url not absolute",
			mutate:  func(t *testing.T) { t.Setenv(backendHTTPURLEnvVar, "/relative") },
			wantErr: backendHTTPURLEnvVar,
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			configEnvMu.Lock()
			defer configEnvMu.Unlock()

			resetEnv(t)
			setBaseRequiredEnv(t)
			tc.mutate(t)

			_, err := LoadFromEnv()
			require.Error(t, err)
			assert.Contains(t, err.Error(), tc.wantErr)
		})
	}
}

func TestLoadFromEnvRejectsInvalidPushBackoff(t *testing.T) {
	configEnvMu.Lock()
	defer configEnvMu.Unlock()

	resetEnv(t)
	setBaseRequiredEnv(t)
	t.Setenv(backendPushReconnectBaseBackoffEnvVar, "1s")
	t.Setenv(backendPushReconnectMaxBackoffEnvVar, "500ms")

	_, err := LoadFromEnv()
	require.Error(t, err)
	assert.Contains(t, err.Error(), backendPushReconnectMaxBackoffEnvVar)
}

func TestLoadFromEnvAppliesPublicAndAuthGRPCDefaults(t *testing.T) {
	configEnvMu.Lock()
	defer configEnvMu.Unlock()

	resetEnv(t)
	setBaseRequiredEnv(t)

	cfg, err := LoadFromEnv()
	require.NoError(t, err)

	assert.Equal(t, defaultPublicHTTPAddr, cfg.PublicHTTP.Addr)
	assert.Equal(t, defaultPublicHTTPReadHeaderTimeout, cfg.PublicHTTP.ReadHeaderTimeout)
	assert.Equal(t, defaultPublicHTTPReadTimeout, cfg.PublicHTTP.ReadTimeout)
	assert.Equal(t, defaultPublicHTTPIdleTimeout, cfg.PublicHTTP.IdleTimeout)
	assert.Equal(t, defaultPublicAuthUpstreamTimeout, cfg.PublicHTTP.AuthUpstreamTimeout)
	assert.Empty(t, cfg.PublicHTTP.CORSAllowedOrigins, "default disables CORS")

	assert.Equal(t, defaultAuthenticatedGRPCAddr, cfg.AuthenticatedGRPC.Addr)
	assert.Equal(t, defaultAuthenticatedGRPCConnectionTimeout, cfg.AuthenticatedGRPC.ConnectionTimeout)
	assert.Equal(t, defaultAuthenticatedGRPCDownstreamTimeout, cfg.AuthenticatedGRPC.DownstreamTimeout)
	assert.Equal(t, defaultAuthenticatedGRPCFreshnessWindow, cfg.AuthenticatedGRPC.FreshnessWindow)
}

func TestLoadFromEnvParsesCORSAllowedOrigins(t *testing.T) {
	configEnvMu.Lock()
	defer configEnvMu.Unlock()

	resetEnv(t)
	setBaseRequiredEnv(t)
	t.Setenv(publicHTTPCORSAllowedOriginsEnvVar, "https://www.galaxy.lan, , https://staging.galaxy.lan")

	cfg, err := LoadFromEnv()
	require.NoError(t, err)
	assert.Equal(t,
		[]string{"https://www.galaxy.lan", "https://staging.galaxy.lan"},
		cfg.PublicHTTP.CORSAllowedOrigins,
		"comma-separated list is split, whitespace-trimmed, and empty segments dropped")
}

// resetEnv clears every env var the gateway config might read so that
// individual tests can build the exact environment they need without
// leakage from a previous test.
func resetEnv(t *testing.T) {
	t.Helper()

	for _, name := range []string{
		shutdownTimeoutEnvVar,
		logLevelEnvVar,
		publicHTTPAddrEnvVar,
		publicHTTPReadHeaderTimeoutEnvVar,
		publicHTTPReadTimeoutEnvVar,
		publicHTTPIdleTimeoutEnvVar,
		publicAuthUpstreamTimeoutEnvVar,
		publicHTTPCORSAllowedOriginsEnvVar,
		backendHTTPURLEnvVar,
		backendGRPCPushURLEnvVar,
		backendGatewayClientIDEnvVar,
		backendHTTPTimeoutEnvVar,
		backendPushReconnectBaseBackoffEnvVar,
		backendPushReconnectMaxBackoffEnvVar,
		adminHTTPAddrEnvVar,
		adminHTTPReadHeaderTimeoutEnvVar,
		adminHTTPReadTimeoutEnvVar,
		adminHTTPIdleTimeoutEnvVar,
		authenticatedGRPCAddrEnvVar,
		authenticatedGRPCConnectionTimeoutEnvVar,
		authenticatedGRPCDownstreamTimeoutEnvVar,
		authenticatedGRPCFreshnessWindowEnvVar,
		gatewayRedisMasterAddrEnvVar,
		gatewayRedisPasswordEnvVar,
		replayRedisKeyPrefixEnvVar,
		replayRedisReserveTimeoutEnvVar,
		responseSignerPrivateKeyPEMPathEnvVar,
	} {
		os.Unsetenv(name)
	}
}

func setBaseRequiredEnv(t *testing.T) {
	t.Helper()

	t.Setenv(gatewayRedisMasterAddrEnvVar, defaultTestRedisMasterAddrValue)
	t.Setenv(gatewayRedisPasswordEnvVar, defaultTestRedisPasswordValue)
	t.Setenv(backendHTTPURLEnvVar, defaultTestBackendHTTPURL)
	t.Setenv(backendGRPCPushURLEnvVar, defaultTestBackendGRPCPushURL)
	t.Setenv(backendGatewayClientIDEnvVar, defaultTestBackendClientID)
	t.Setenv(responseSignerPrivateKeyPEMPathEnvVar, writeTestResponseSignerPEMFile(t))
}

func writeTestResponseSignerPEMFile(t *testing.T) string {
	t.Helper()

	seed := sha256.Sum256([]byte("gateway-config-test-response-signer"))
	privateKey := ed25519.NewKeyFromSeed(seed[:])

	encodedPrivateKey, err := x509.MarshalPKCS8PrivateKey(privateKey)
	require.NoError(t, err)

	path := filepath.Join(t.TempDir(), "response-signer.pem")
	require.NoError(t, os.WriteFile(path, pem.EncodeToMemory(&pem.Block{
		Type:  "PRIVATE KEY",
		Bytes: encodedPrivateKey,
	}), 0o600))

	return path
}
