package config

import (
	"bytes"
	"encoding/base64"
	"os"
	"path/filepath"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/AgentTeam/internal/versioning"
)

func minimalValidConfig() Config {
	return Config{
		Database: DatabaseConfig{
			URL:            "root:@tcp(127.0.0.1:4000)/agentteam?tls=false",
			ConnectTimeout: time.Second,
		},
		Archive: ArchiveConfig{
			Driver:         archiveDriverFilesystem,
			FilesystemRoot: ".data/archive",
		},
		Mail: MailConfig{Driver: mailDriverNoop},
		Auth: AuthConfig{
			VerificationTokenTTL: time.Second,
			SessionTTL:           time.Second,
		},
		Messages: MessagesConfig{HotRetentionDays: 30},
		UnreadRuntime: UnreadRuntimeConfig{
			SnapshotFlushInterval:  time.Second,
			SnapshotFlushBatchSize: 1,
			HydrateMaxConcurrency:  1,
			HydrateBatchSize:       1,
		},
		HTTP: HTTPConfig{ShutdownTimeout: time.Second},
		Billing: BillingConfig{
			Alipay: AlipayConfig{RequestTimeout: time.Second},
		},
		Daemon: DaemonConfig{
			LatestVersionCacheTTL: time.Hour,
		},
	}
}

func setRequiredBillingEnv(t *testing.T) {
	t.Helper()
	t.Setenv("AGENTTEAM_BILLING_ENABLED", "true")
	t.Setenv("AGENTTEAM_ALIPAY_APP_ID", "2021000000000000")
	t.Setenv("AGENTTEAM_ALIPAY_APP_PRIVATE_KEY", "private-key")
	t.Setenv("AGENTTEAM_ALIPAY_PUBLIC_KEY", "public-key")
	t.Setenv("AGENTTEAM_ALIPAY_GATEWAY_URL", "https://openapi.alipay.com/gateway.do")
	t.Setenv("AGENTTEAM_MAAS_BASE_URL", "http://127.0.0.1:8080")
	t.Setenv("AGENTTEAM_MAAS_INTERNAL_BEARER_TOKEN", "agentteam-maas-dev-internal-token")
}

func TestLoadFromEnvReadsDaemonMinimumVersion(t *testing.T) {
	t.Setenv("AGENTTEAM_DATABASE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")
	t.Setenv("AGENTTEAM_DAEMON_MIN_VERSION", " v1.0.2-rc.11 ")
	t.Setenv("AGENTTEAM_DAEMON_LATEST_VERSION_URL", "https://static.example.com/agentteam/stable.json")
	t.Setenv("AGENTTEAM_DAEMON_LATEST_VERSION_OVERRIDE", "v9.9.9")

	cfg, err := LoadFromEnv()
	require.NoError(t, err)
	require.Equal(t, "v1.0.2-rc.11", cfg.Daemon.MinDaemonVersion)
	require.Equal(t, "https://static.example.com/agentteam/stable.json", cfg.Daemon.LatestVersionURL)
	require.Equal(t, "v9.9.9", cfg.Daemon.LatestVersionOverride)
}

func TestLoadFromEnvDefaultsDaemonLatestVersionURL(t *testing.T) {
	t.Setenv("AGENTTEAM_DATABASE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")

	cfg, err := LoadFromEnv()
	require.NoError(t, err)
	require.Equal(t, versioning.DefaultStableManifestURL, cfg.Daemon.LatestVersionURL)
}

func TestLoadFromEnvSupportsExplicitCoreConfig(t *testing.T) {
	t.Setenv("AGENTTEAM_DATABASE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")

	cfg, err := LoadFromEnv()
	require.NoError(t, err)
	require.Equal(t, ServerProfileAll, cfg.Server.Profile)
	require.Equal(t, "root:@tcp(127.0.0.1:4000)/agentteam?tls=false", cfg.Database.URL)
	require.Equal(t, 0, cfg.Database.Pool.MaxOpenConns)
	require.Equal(t, 2, cfg.Database.Pool.MaxIdleConns)
	require.Zero(t, cfg.Database.Pool.ConnMaxLifetime)
	require.Zero(t, cfg.Database.Pool.ConnMaxIdleTime)
	require.Equal(t, defaultOrgCredentialsKeyFile, cfg.Feishu.OrgCredentialsKeyFile)
	require.Equal(t, defaultAppCredentialsKeyFile, cfg.AppCredentials.KeyFile)
}

func TestLoadFromEnvIgnoresRetiredGlobalExtensionEgressExceptions(t *testing.T) {
	t.Setenv("AGENTTEAM_DATABASE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")
	baseline, err := LoadFromEnv()
	require.NoError(t, err)
	t.Setenv("AGENTTEAM_EXTENSION_EGRESS_ALLOWED_"+"ORIGINS", "https://private.example")
	t.Setenv("AGENTTEAM_EXTENSION_EGRESS_ALLOWED_"+"CIDRS", "10.0.0.0/8")
	withRetiredValues, err := LoadFromEnv()
	require.NoError(t, err)
	require.Equal(t, baseline, withRetiredValues,
		"retired global egress variables must not affect runtime config")
}

func TestLoadFromEnvDefaultsReadyzDependencyTimeout(t *testing.T) {
	t.Setenv("AGENTTEAM_DATABASE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")

	cfg, err := LoadFromEnv()
	require.NoError(t, err)
	require.Equal(t, 2*time.Second, cfg.HTTP.ReadyzDependencyTimeout)
	require.Equal(t, 2*time.Minute, cfg.HTTP.StartupSchemaTimeout)
}

func TestHTTPConfigEffectiveReadyzDependencyTimeoutDefaultsZero(t *testing.T) {
	require.Equal(t, 2*time.Second, (HTTPConfig{}).EffectiveReadyzDependencyTimeout())
	require.Equal(t, 7*time.Second, (HTTPConfig{ReadyzDependencyTimeout: 7 * time.Second}).EffectiveReadyzDependencyTimeout())
}

func TestHTTPConfigEffectiveStartupSchemaTimeoutDefaultsZero(t *testing.T) {
	require.Equal(t, 2*time.Minute, (HTTPConfig{}).EffectiveStartupSchemaTimeout())
	require.Equal(t, 7*time.Minute, (HTTPConfig{StartupSchemaTimeout: 7 * time.Minute}).EffectiveStartupSchemaTimeout())
}

func TestLoadFromEnvReadsReadyzDependencyTimeout(t *testing.T) {
	t.Setenv("AGENTTEAM_DATABASE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")
	t.Setenv("AGENTTEAM_READYZ_DEPENDENCY_TIMEOUT", "8s")

	cfg, err := LoadFromEnv()
	require.NoError(t, err)
	require.Equal(t, 8*time.Second, cfg.HTTP.ReadyzDependencyTimeout)
}

func TestLoadFromEnvReadsStartupSchemaTimeout(t *testing.T) {
	t.Setenv("AGENTTEAM_DATABASE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")
	t.Setenv("AGENTTEAM_STARTUP_SCHEMA_TIMEOUT", "7m")

	cfg, err := LoadFromEnv()
	require.NoError(t, err)
	require.Equal(t, 7*time.Minute, cfg.HTTP.StartupSchemaTimeout)
}

func TestLoadFromEnvRejectsInvalidStartupSchemaTimeout(t *testing.T) {
	t.Setenv("AGENTTEAM_DATABASE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")
	t.Setenv("AGENTTEAM_STARTUP_SCHEMA_TIMEOUT", "0s")

	_, err := LoadFromEnv()
	require.EqualError(t, err, "AGENTTEAM_STARTUP_SCHEMA_TIMEOUT must be positive")
}

func TestValidateRejectsNegativeStartupSchemaTimeout(t *testing.T) {
	cfg := minimalValidConfig()
	cfg.HTTP.StartupSchemaTimeout = -time.Second

	require.EqualError(t, cfg.Validate(), "AGENTTEAM_STARTUP_SCHEMA_TIMEOUT must be non-negative")
}

func TestLoadFromEnvRejectsInvalidReadyzDependencyTimeout(t *testing.T) {
	t.Setenv("AGENTTEAM_DATABASE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")
	t.Setenv("AGENTTEAM_READYZ_DEPENDENCY_TIMEOUT", "0s")

	_, err := LoadFromEnv()
	require.EqualError(t, err, "AGENTTEAM_READYZ_DEPENDENCY_TIMEOUT must be positive")
}

func TestLoadFromEnvReadsWebServingConfig(t *testing.T) {
	t.Setenv("AGENTTEAM_DATABASE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")
	t.Setenv("AGENTTEAM_WEB_SERVE_ENABLED", "true")
	t.Setenv("AGENTTEAM_WEB_DIST_DIR", " /opt/staging-agentteam/current/web ")

	cfg, err := LoadFromEnv()
	require.NoError(t, err)
	require.True(t, cfg.Web.ServeWeb)
	require.Equal(t, "/opt/staging-agentteam/current/web", cfg.Web.DistDir)
}

func TestLoadFromEnvReadsServerProfile(t *testing.T) {
	t.Setenv("AGENTTEAM_DATABASE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")
	t.Setenv("AGENTTEAM_SERVER_PROFILE", "workspace")
	t.Setenv("AGENTTEAM_WORKSPACE_GROUP_ID", "workspace-default")

	cfg, err := LoadFromEnv()
	require.NoError(t, err)
	require.Equal(t, ServerProfileWorkspace, cfg.Server.Profile)
	require.Equal(t, "workspace-default", cfg.Server.WorkspaceGroupID)
	require.Equal(t, DefaultWorkspaceGroupID, cfg.Server.DefaultWorkspaceGroupID)
}

// TestServerProfileExtensionValidatesWithoutWorkspaceGroupID asserts the
// extension profile is accepted by Validate without a workspace group id (it is
// a narrow runtime, not a workspace data-plane), and that an unknown profile
// fails closed.
func TestServerProfileExtensionValidatesWithoutWorkspaceGroupID(t *testing.T) {
	require.NoError(t, ServerConfig{Profile: ServerProfileExtension}.Validate())
	require.NoError(t, ServerConfig{Profile: ServerProfileExtension, WorkspaceGroupID: "unused"}.Validate())

	err := ServerConfig{Profile: "nope"}.Validate()
	require.Error(t, err)
}

func TestLoadFromEnvReadsExtensionProfile(t *testing.T) {
	t.Setenv("AGENTTEAM_DATABASE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")
	t.Setenv("AGENTTEAM_SERVER_PROFILE", "extension")

	cfg, err := LoadFromEnv()
	require.NoError(t, err)
	require.Equal(t, ServerProfileExtension, cfg.Server.Profile)
}

func TestLoadFromEnvIgnoresRetiredMeshXCompatibilityUpstream(t *testing.T) {
	for _, profile := range []string{ServerProfileAll, ServerProfileCentral, ServerProfileWorkspace} {
		t.Run(profile, func(t *testing.T) {
			t.Setenv("AGENTTEAM_DATABASE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")
			t.Setenv("AGENTTEAM_ENV", "production")
			t.Setenv("AGENTTEAM_PUBLIC_BASE_URL", "https://agentteam.example.test")
			t.Setenv("AGENTTEAM_SERVER_PROFILE", profile)
			t.Setenv("AGENTTEAM_MESHX_BASE_URL", "://not-a-valid-url")
			if profile == ServerProfileWorkspace {
				t.Setenv("AGENTTEAM_WORKSPACE_GROUP_ID", "workspace-default")
			}

			cfg, err := LoadFromEnv()
			require.NoError(t, err)
			require.Equal(t, profile, cfg.Server.Profile)
		})
	}
}

func TestLoadFromEnvReadsDefaultWorkspaceGroupID(t *testing.T) {
	t.Setenv("AGENTTEAM_DATABASE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")
	t.Setenv("AGENTTEAM_DEFAULT_WORKSPACE_GROUP_ID", " workspace-vip ")
	t.Setenv("AGENTTEAM_INTERNAL_ADMIN_TOKEN", " internal-token ")

	cfg, err := LoadFromEnv()
	require.NoError(t, err)
	require.Equal(t, "workspace-vip", cfg.Server.DefaultWorkspaceGroupID)
	require.Equal(t, "internal-token", cfg.Server.InternalAdminToken)
}

func TestLoadFromEnvReadsAppNotificationInternalServices(t *testing.T) {
	t.Setenv("AGENTTEAM_DATABASE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")
	t.Setenv("AGENTTEAM_APP_NOTIFICATION_INTERNAL_SERVICES_JSON", `[
		{
			"identityId":" agentteam.approval ",
			"secret":" secret-1 ",
			"serviceDefinitionSlug":" approval-app ",
			"allowedBusinessNamespaces":["agentteam.approval", "agentteam.approval"],
			"allowedTargetKinds":["human_dm"],
			"allowedContentKinds":["message", "interactive_card"]
		}
	]`)

	cfg, err := LoadFromEnv()
	require.NoError(t, err)
	require.Equal(t, []AppNotificationInternalServiceConfig{{
		IdentityID: "agentteam.approval", Secret: "secret-1", ServiceDefinitionSlug: "approval-app",
		AllowedBusinessNamespaces: []string{"agentteam.approval"},
		AllowedTargetKinds:        []string{"human_dm"},
		AllowedContentKinds:       []string{"message", "interactive_card"},
	}}, cfg.Server.AppNotificationInternalServices)
}

func TestLoadFromEnvRejectsDuplicateAppNotificationInternalServiceSecret(t *testing.T) {
	t.Setenv("AGENTTEAM_DATABASE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")
	t.Setenv("AGENTTEAM_APP_NOTIFICATION_INTERNAL_SERVICES_JSON", `[
		{"identityId":"one","secret":"same","serviceDefinitionSlug":"one","allowedTargetKinds":["human_dm"],"allowedContentKinds":["message"]},
		{"identityId":"two","secret":"same","serviceDefinitionSlug":"two","allowedTargetKinds":["human_dm"],"allowedContentKinds":["message"]}
	]`)

	_, err := LoadFromEnv()
	require.EqualError(t, err, "AGENTTEAM_APP_NOTIFICATION_INTERNAL_SERVICES_JSON contains a duplicate service secret")
}

func TestLoadFromEnvRejectsAppNotificationInternalServiceSecretWithWhitespace(t *testing.T) {
	t.Setenv("AGENTTEAM_APP_NOTIFICATION_INTERNAL_SERVICES_JSON", `[
		{"identityId":"approval","secret":"secret with space","serviceDefinitionSlug":"approval-app","allowedTargetKinds":["human_dm"],"allowedContentKinds":["message"]}
	]`)
	_, err := LoadFromEnv()
	require.EqualError(t, err, "AGENTTEAM_APP_NOTIFICATION_INTERNAL_SERVICES_JSON entry 0 secret must be a single HTTP token")
}

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

	cfg := ServerConfig{
		Profile: ServerProfileAll,
		AppNotificationInternalServices: []AppNotificationInternalServiceConfig{{
			IdentityID:            "agentteam.approval",
			Secret:                "secret with space",
			ServiceDefinitionSlug: "approval-app",
			AllowedTargetKinds:    []string{"human_dm"},
			AllowedContentKinds:   []string{"message"},
		}},
	}
	require.EqualError(t, cfg.Validate(), "App notification internal service entry 0 secret must be a single HTTP token")

	cfg.AppNotificationInternalServices[0].Secret = "shared-secret"
	cfg.AppNotificationInternalServices = append(cfg.AppNotificationInternalServices, AppNotificationInternalServiceConfig{
		IdentityID:            "agentteam.task",
		Secret:                "shared-secret",
		ServiceDefinitionSlug: "task-app",
		AllowedTargetKinds:    []string{"bound_scope"},
		AllowedContentKinds:   []string{"interactive_card"},
	})
	require.EqualError(t, cfg.Validate(), "App notification internal services contain a duplicate service secret")
}

func TestLoadFromEnvRejectsWorkspaceProfileWithoutGroup(t *testing.T) {
	t.Setenv("AGENTTEAM_DATABASE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")
	t.Setenv("AGENTTEAM_SERVER_PROFILE", "workspace")

	_, err := LoadFromEnv()
	require.EqualError(t, err, "AGENTTEAM_WORKSPACE_GROUP_ID is required when AGENTTEAM_SERVER_PROFILE=workspace")
}

func TestLoadFromEnvRejectsUnsupportedOperatorFacingProfile(t *testing.T) {
	for _, profile := range []string{"control", "ingress", "runtime", "worker"} {
		t.Run(profile, func(t *testing.T) {
			t.Setenv("AGENTTEAM_DATABASE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")
			t.Setenv("AGENTTEAM_SERVER_PROFILE", profile)

			_, err := LoadFromEnv()
			require.EqualError(t, err, `AGENTTEAM_SERVER_PROFILE must be one of "all", "central", "workspace", or "extension"`)
		})
	}
}

func TestLoadFromEnvAllowsCentralProfileWithoutGroup(t *testing.T) {
	t.Setenv("AGENTTEAM_DATABASE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")
	t.Setenv("AGENTTEAM_SERVER_PROFILE", "central")

	cfg, err := LoadFromEnv()
	require.NoError(t, err)
	require.Equal(t, ServerProfileCentral, cfg.Server.Profile)
}

func TestLoadFromEnvReadsObservabilityConfig(t *testing.T) {
	t.Setenv("AGENTTEAM_DATABASE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")
	t.Setenv("AGENTTEAM_OBSERVABILITY_METRICS_ADDRESS", ":2112")
	t.Setenv("AGENTTEAM_OBSERVABILITY_OTLP_ENDPOINT", "http://otel-collector:4318")
	t.Setenv("AGENTTEAM_OBSERVABILITY_OTLP_HEADERS", "Authorization=Bearer test-token, x-otlp-tenant = agentteam ")
	t.Setenv("AGENTTEAM_OBSERVABILITY_TRACE_SAMPLE_RATIO", "0.25")
	t.Setenv("AGENTTEAM_OBSERVABILITY_DB_STATEMENT_ENABLED", "true")

	cfg, err := LoadFromEnv()
	require.NoError(t, err)
	require.Equal(t, ":2112", cfg.Observability.MetricsAddress)
	require.Equal(t, "http://otel-collector:4318", cfg.Observability.OTLPEndpoint)
	require.Equal(t, map[string]string{
		"Authorization": "Bearer test-token",
		"x-otlp-tenant": "agentteam",
	}, cfg.Observability.OTLPHeaders)
	require.Equal(t, 0.25, cfg.Observability.TraceSampleRatio)
	require.True(t, cfg.Observability.DBStatementEnabled)
}

func TestLoadFromEnvDefaultsObservabilityConfig(t *testing.T) {
	t.Setenv("AGENTTEAM_DATABASE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")

	cfg, err := LoadFromEnv()
	require.NoError(t, err)
	require.Equal(t, "127.0.0.1:2112", cfg.Observability.MetricsAddress)
	require.Empty(t, cfg.Observability.OTLPEndpoint)
	require.Empty(t, cfg.Observability.OTLPHeaders)
	require.Equal(t, 1.0, cfg.Observability.TraceSampleRatio)
	require.False(t, cfg.Observability.DBStatementEnabled)
}

func TestLoadFromEnvRejectsInvalidObservabilityDBStatementEnabled(t *testing.T) {
	t.Setenv("AGENTTEAM_DATABASE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")
	t.Setenv("AGENTTEAM_OBSERVABILITY_DB_STATEMENT_ENABLED", "yes")

	_, err := LoadFromEnv()
	require.EqualError(t, err, "AGENTTEAM_OBSERVABILITY_DB_STATEMENT_ENABLED must be true or false")
}

func TestLoadFromEnvUsesProductionTraceSampleDefault(t *testing.T) {
	t.Setenv("AGENTTEAM_DATABASE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")
	t.Setenv("AGENTTEAM_ENV", "production")
	t.Setenv("AGENTTEAM_TURNSTILE_SITE_KEY", "site-key")
	t.Setenv("AGENTTEAM_TURNSTILE_SECRET_KEY", "secret-key")
	t.Setenv("AGENTTEAM_PUBLIC_BASE_URL", "https://agentteam.example.test")
	t.Setenv("AGENTTEAM_DAEMON_CONNECT_BASE_URL", "https://agentteam.example.test")

	cfg, err := LoadFromEnv()
	require.NoError(t, err)
	require.Equal(t, 0.05, cfg.Observability.TraceSampleRatio)
}

func TestLoadFromEnvAllowsDisablingObservabilityMetrics(t *testing.T) {
	t.Setenv("AGENTTEAM_DATABASE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")
	t.Setenv("AGENTTEAM_OBSERVABILITY_METRICS_ADDRESS", "")

	cfg, err := LoadFromEnv()
	require.NoError(t, err)
	require.Empty(t, cfg.Observability.MetricsAddress)
}

func TestValidateRejectsObservabilityMetricsAddressMatchingHTTP(t *testing.T) {
	cfg := minimalValidConfig()
	cfg.HTTP.Address = ":3001"
	cfg.Observability.MetricsAddress = ":3001"

	err := cfg.Validate()
	require.EqualError(t, err, "AGENTTEAM_OBSERVABILITY_METRICS_ADDRESS must not equal AGENTTEAM_HTTP_ADDRESS")
}

func TestValidateRejectsInvalidObservabilityTraceSampleRatio(t *testing.T) {
	cfg := minimalValidConfig()
	cfg.Observability.TraceSampleRatio = 1.5

	err := cfg.Validate()
	require.EqualError(t, err, "AGENTTEAM_OBSERVABILITY_TRACE_SAMPLE_RATIO must be in [0, 1]")
}

func TestValidateRejectsInvalidObservabilityOTLPEndpoint(t *testing.T) {
	tests := []struct {
		name     string
		endpoint string
		wantErr  string
	}{
		{
			name:     "unsupported scheme",
			endpoint: "ftp://collector",
			wantErr:  "AGENTTEAM_OBSERVABILITY_OTLP_ENDPOINT must use http(s)",
		},
		{
			name:     "missing host",
			endpoint: "http:///v1/traces",
			wantErr:  "AGENTTEAM_OBSERVABILITY_OTLP_ENDPOINT must include a host",
		},
		{
			name:     "userinfo",
			endpoint: "https://token@collector.example.test",
			wantErr:  "AGENTTEAM_OBSERVABILITY_OTLP_ENDPOINT must not include user info",
		},
		{
			name:     "query",
			endpoint: "https://collector.example.test?token=secret",
			wantErr:  "AGENTTEAM_OBSERVABILITY_OTLP_ENDPOINT must not include a query string",
		},
		{
			name:     "fragment",
			endpoint: "https://collector.example.test#traces",
			wantErr:  "AGENTTEAM_OBSERVABILITY_OTLP_ENDPOINT must not include a fragment",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cfg := minimalValidConfig()
			cfg.Observability.OTLPEndpoint = tt.endpoint

			err := cfg.Validate()
			require.EqualError(t, err, tt.wantErr)
		})
	}
}

func TestLoadFromEnvRejectsInvalidObservabilityOTLPHeaders(t *testing.T) {
	tests := []struct {
		name    string
		headers string
		wantErr string
	}{
		{
			name:    "missing equals",
			headers: "Authorization",
			wantErr: "AGENTTEAM_OBSERVABILITY_OTLP_HEADERS must use key=value pairs separated by commas",
		},
		{
			name:    "empty key",
			headers: "=Bearer test-token",
			wantErr: "AGENTTEAM_OBSERVABILITY_OTLP_HEADERS contains an invalid header name",
		},
		{
			name:    "bad key",
			headers: "bad key=value",
			wantErr: "AGENTTEAM_OBSERVABILITY_OTLP_HEADERS contains an invalid header name",
		},
		{
			name:    "empty value",
			headers: "Authorization=",
			wantErr: "AGENTTEAM_OBSERVABILITY_OTLP_HEADERS contains an invalid header value",
		},
		{
			name:    "duplicate key",
			headers: "Authorization=one,authorization=two",
			wantErr: "AGENTTEAM_OBSERVABILITY_OTLP_HEADERS contains duplicate header \"Authorization\"",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Setenv("AGENTTEAM_DATABASE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")
			t.Setenv("AGENTTEAM_OBSERVABILITY_OTLP_HEADERS", tt.headers)

			_, err := LoadFromEnv()
			require.EqualError(t, err, tt.wantErr)
		})
	}
}

func TestValidateRejectsInvalidObservabilityOTLPHeaders(t *testing.T) {
	cfg := minimalValidConfig()
	cfg.Observability.OTLPHeaders = map[string]string{"bad key": "value"}

	err := cfg.Validate()
	require.EqualError(t, err, "AGENTTEAM_OBSERVABILITY_OTLP_HEADERS contains an invalid header name")
}

func TestValidateRejectsDuplicateObservabilityOTLPHeaders(t *testing.T) {
	cfg := minimalValidConfig()
	cfg.Observability.OTLPHeaders = map[string]string{
		"Authorization": "one",
		"authorization": "two",
	}

	err := cfg.Validate()
	require.ErrorContains(t, err, "AGENTTEAM_OBSERVABILITY_OTLP_HEADERS contains duplicate header")
}

func TestLoadFromEnvReadsDatabasePoolConfig(t *testing.T) {
	t.Setenv("AGENTTEAM_DATABASE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")
	t.Setenv("AGENTTEAM_DATABASE_MAX_OPEN_CONNS", "40")
	t.Setenv("AGENTTEAM_DATABASE_CONN_MAX_LIFETIME", "5m")
	t.Setenv("AGENTTEAM_DATABASE_CONN_MAX_IDLE_TIME", "90s")

	cfg, err := LoadFromEnv()
	require.NoError(t, err)
	require.Equal(t, 40, cfg.Database.Pool.MaxOpenConns)
	require.Equal(t, 40, cfg.Database.Pool.MaxIdleConns)
	require.Equal(t, 5*time.Minute, cfg.Database.Pool.ConnMaxLifetime)
	require.Equal(t, 90*time.Second, cfg.Database.Pool.ConnMaxIdleTime)
}

func TestLoadFromEnvHonorsExplicitDatabaseMaxIdleConns(t *testing.T) {
	t.Setenv("AGENTTEAM_DATABASE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")
	t.Setenv("AGENTTEAM_DATABASE_MAX_OPEN_CONNS", "40")
	t.Setenv("AGENTTEAM_DATABASE_MAX_IDLE_CONNS", "8")

	cfg, err := LoadFromEnv()
	require.NoError(t, err)
	require.Equal(t, 40, cfg.Database.Pool.MaxOpenConns)
	require.Equal(t, 8, cfg.Database.Pool.MaxIdleConns)
}

func TestLoadFromEnvDefaultsUnreadRuntimeConfig(t *testing.T) {
	t.Setenv("AGENTTEAM_CORE_STORAGE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")

	cfg, err := LoadFromEnv()
	require.NoError(t, err)
	require.Equal(t, 30*time.Second, cfg.UnreadRuntime.SnapshotFlushInterval)
	require.Equal(t, 1000, cfg.UnreadRuntime.SnapshotFlushBatchSize)
	require.Equal(t, 2, cfg.UnreadRuntime.HydrateMaxConcurrency)
	require.Equal(t, 100, cfg.UnreadRuntime.HydrateBatchSize)
}

func TestLoadFromEnvReadsUnreadRuntimeConfig(t *testing.T) {
	t.Setenv("AGENTTEAM_CORE_STORAGE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")
	t.Setenv("AGENTTEAM_UNREAD_RUNTIME_SNAPSHOT_FLUSH_INTERVAL", "45s")
	t.Setenv("AGENTTEAM_UNREAD_RUNTIME_SNAPSHOT_FLUSH_BATCH_SIZE", "250")
	t.Setenv("AGENTTEAM_UNREAD_RUNTIME_HYDRATE_MAX_CONCURRENCY", "4")
	t.Setenv("AGENTTEAM_UNREAD_RUNTIME_HYDRATE_BATCH_SIZE", "25")

	cfg, err := LoadFromEnv()
	require.NoError(t, err)
	require.Equal(t, 45*time.Second, cfg.UnreadRuntime.SnapshotFlushInterval)
	require.Equal(t, 250, cfg.UnreadRuntime.SnapshotFlushBatchSize)
	require.Equal(t, 4, cfg.UnreadRuntime.HydrateMaxConcurrency)
	require.Equal(t, 25, cfg.UnreadRuntime.HydrateBatchSize)
}

func TestLoadFromEnvRejectsInvalidUnreadRuntimeConfig(t *testing.T) {
	tests := []struct {
		name    string
		key     string
		value   string
		wantErr string
	}{
		{
			name:    "flush interval",
			key:     "AGENTTEAM_UNREAD_RUNTIME_SNAPSHOT_FLUSH_INTERVAL",
			value:   "0s",
			wantErr: "AGENTTEAM_UNREAD_RUNTIME_SNAPSHOT_FLUSH_INTERVAL must be positive",
		},
		{
			name:    "flush batch size",
			key:     "AGENTTEAM_UNREAD_RUNTIME_SNAPSHOT_FLUSH_BATCH_SIZE",
			value:   "0",
			wantErr: "AGENTTEAM_UNREAD_RUNTIME_SNAPSHOT_FLUSH_BATCH_SIZE must be positive",
		},
		{
			name:    "hydrate concurrency",
			key:     "AGENTTEAM_UNREAD_RUNTIME_HYDRATE_MAX_CONCURRENCY",
			value:   "-1",
			wantErr: "AGENTTEAM_UNREAD_RUNTIME_HYDRATE_MAX_CONCURRENCY must be positive",
		},
		{
			name:    "hydrate batch size",
			key:     "AGENTTEAM_UNREAD_RUNTIME_HYDRATE_BATCH_SIZE",
			value:   "0",
			wantErr: "AGENTTEAM_UNREAD_RUNTIME_HYDRATE_BATCH_SIZE must be positive",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Setenv("AGENTTEAM_CORE_STORAGE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")
			t.Setenv(tt.key, tt.value)

			_, err := LoadFromEnv()
			require.EqualError(t, err, tt.wantErr)
		})
	}
}

func TestLoadFromEnvRequiresExplicitCoreStorageDSN(t *testing.T) {
	t.Setenv("AGENTTEAM_DATABASE_DSN", "")

	_, err := LoadFromEnv()
	require.EqualError(t, err, "AGENTTEAM_DATABASE_DSN is required")
}

func TestLoadFromEnvDefaultsRegistrationToInviteOnly(t *testing.T) {
	t.Setenv("AGENTTEAM_DATABASE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")

	cfg, err := LoadFromEnv()
	require.NoError(t, err)
	require.Equal(t, registrationModeInviteOnly, cfg.Auth.RegistrationMode)
	require.Equal(t, registrationModeInviteOnly, cfg.Auth.RegistrationModeOrDefault())
}

func TestLoadFromEnvParsesRegistrationAllowlistEmails(t *testing.T) {
	t.Setenv("AGENTTEAM_DATABASE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")
	t.Setenv("AGENTTEAM_AUTH_REGISTRATION_MODE", registrationModeInviteOnly)
	t.Setenv("AGENTTEAM_AUTH_REGISTRATION_ALLOWLIST_EMAILS", "alpha@example.com, beta@example.com")

	cfg, err := LoadFromEnv()
	require.NoError(t, err)
	require.Equal(t, []string{"alpha@example.com", "beta@example.com"}, cfg.Auth.RegistrationAllowlistEmails)
}

func TestLoadFromEnvDefaultsPasswordAuthEnabled(t *testing.T) {
	t.Setenv("AGENTTEAM_DATABASE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")

	cfg, err := LoadFromEnv()
	require.NoError(t, err)
	require.True(t, cfg.Auth.PasswordAuthEnabled())
}

func TestLoadFromEnvDefaultsBillingDisabled(t *testing.T) {
	t.Setenv("AGENTTEAM_DATABASE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")

	cfg, err := LoadFromEnv()
	require.NoError(t, err)
	require.False(t, cfg.Billing.Enabled)
}

func TestLoadFromEnvReadsBillingEnabled(t *testing.T) {
	t.Setenv("AGENTTEAM_DATABASE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")
	setRequiredBillingEnv(t)

	cfg, err := LoadFromEnv()
	require.NoError(t, err)
	require.True(t, cfg.Billing.Enabled)
}

func TestLoadFromEnvAllowsHTTPAlipayGatewayInDevelopment(t *testing.T) {
	t.Setenv("AGENTTEAM_ENV", "development")
	t.Setenv("AGENTTEAM_DATABASE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")
	setRequiredBillingEnv(t)
	t.Setenv("AGENTTEAM_ALIPAY_GATEWAY_URL", "http://127.0.0.1:18082/gateway.do")

	cfg, err := LoadFromEnv()
	require.NoError(t, err)
	require.Equal(t, "http://127.0.0.1:18082/gateway.do", cfg.Billing.Alipay.GatewayURL)
}

func TestLoadFromEnvRejectsHTTPAlipayGatewayInProduction(t *testing.T) {
	t.Setenv("AGENTTEAM_ENV", "production")
	t.Setenv("AGENTTEAM_DATABASE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")
	setRequiredBillingEnv(t)
	t.Setenv("AGENTTEAM_ALIPAY_GATEWAY_URL", "http://127.0.0.1:18082/gateway.do")

	_, err := LoadFromEnv()
	require.EqualError(t, err, "AGENTTEAM_ALIPAY_GATEWAY_URL must use https in production")
}

func TestLoadFromEnvRequiresBillingDependenciesWhenEnabled(t *testing.T) {
	t.Setenv("AGENTTEAM_DATABASE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")
	t.Setenv("AGENTTEAM_BILLING_ENABLED", "true")

	_, err := LoadFromEnv()
	require.EqualError(t, err, "AGENTTEAM_ALIPAY_APP_ID is required when AGENTTEAM_BILLING_ENABLED=true")

	t.Setenv("AGENTTEAM_ALIPAY_APP_ID", "2021000000000000")
	t.Setenv("AGENTTEAM_ALIPAY_APP_PRIVATE_KEY", "private-key")
	t.Setenv("AGENTTEAM_ALIPAY_PUBLIC_KEY", "public-key")
	t.Setenv("AGENTTEAM_ALIPAY_GATEWAY_URL", "https://openapi.alipay.com/gateway.do")
	t.Setenv("AGENTTEAM_MAAS_BASE_URL", "http://127.0.0.1:8080")

	_, err = LoadFromEnv()
	require.EqualError(t, err, "AGENTTEAM_MAAS_INTERNAL_BEARER_TOKEN is required when AGENTTEAM_BILLING_ENABLED=true")
}

func TestLoadFromEnvReadsAlipayConfig(t *testing.T) {
	t.Setenv("AGENTTEAM_DATABASE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")
	t.Setenv("AGENTTEAM_ALIPAY_APP_ID", "2021000000000000")
	t.Setenv("AGENTTEAM_ALIPAY_APP_PRIVATE_KEY", "private-key")
	t.Setenv("AGENTTEAM_ALIPAY_PUBLIC_KEY", "public-key")
	t.Setenv("AGENTTEAM_ALIPAY_GATEWAY_URL", "https://openapi.alipay.com/gateway.do")
	t.Setenv("AGENTTEAM_ALIPAY_REQUEST_TIMEOUT", "3s")

	cfg, err := LoadFromEnv()
	require.NoError(t, err)
	require.Equal(t, "2021000000000000", cfg.Billing.Alipay.AppID)
	require.Equal(t, "private-key", cfg.Billing.Alipay.AppPrivateKey)
	require.Equal(t, "public-key", cfg.Billing.Alipay.AlipayPublicKey)
	require.Equal(t, "https://openapi.alipay.com/gateway.do", cfg.Billing.Alipay.GatewayURL)
	require.Equal(t, 3*time.Second, cfg.Billing.Alipay.RequestTimeout)
}

func TestLoadFromEnvRejectsNegativeAlipayRequestTimeout(t *testing.T) {
	t.Setenv("AGENTTEAM_DATABASE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")
	t.Setenv("AGENTTEAM_ALIPAY_REQUEST_TIMEOUT", "-1s")

	_, err := LoadFromEnv()
	require.EqualError(t, err, "AGENTTEAM_ALIPAY_REQUEST_TIMEOUT must be non-negative")
}

func TestLoadFromEnvRejectsInvalidBillingEnabled(t *testing.T) {
	t.Setenv("AGENTTEAM_DATABASE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")
	t.Setenv("AGENTTEAM_BILLING_ENABLED", "off")

	_, err := LoadFromEnv()
	require.EqualError(t, err, "AGENTTEAM_BILLING_ENABLED must be true or false")
}

func TestLoadFromEnvDefaultsTeamsEnabled(t *testing.T) {
	t.Setenv("AGENTTEAM_DATABASE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")

	cfg, err := LoadFromEnv()
	require.NoError(t, err)
	require.True(t, cfg.Teams.Enabled)
}

func TestLoadFromEnvDefaultsAgentToolSettingsVisible(t *testing.T) {
	t.Setenv("AGENTTEAM_DATABASE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")

	cfg, err := LoadFromEnv()
	require.NoError(t, err)
	require.True(t, cfg.AgentTools.SettingsEnabled)
}

func TestLoadFromEnvDefaultsEntryVisibilityFlagsVisible(t *testing.T) {
	t.Setenv("AGENTTEAM_DATABASE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")

	cfg, err := LoadFromEnv()
	require.NoError(t, err)
	require.True(t, cfg.MeshX.KnowledgeBasesEnabled)
}

func TestLoadFromEnvReadsEntryVisibilityFlagsDisabled(t *testing.T) {
	t.Setenv("AGENTTEAM_DATABASE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")
	t.Setenv("AGENTTEAM_MESHX_KNOWLEDGE_BASES_ENABLED", "false")

	cfg, err := LoadFromEnv()
	require.NoError(t, err)
	require.False(t, cfg.MeshX.KnowledgeBasesEnabled)
}

func TestLoadFromEnvReadsAgentToolSettingsEnabled(t *testing.T) {
	t.Setenv("AGENTTEAM_DATABASE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")
	t.Setenv("AGENTTEAM_AGENT_TOOLS_SETTINGS_ENABLED", "true")

	cfg, err := LoadFromEnv()
	require.NoError(t, err)
	require.True(t, cfg.AgentTools.SettingsEnabled)
}

func TestLoadFromEnvReadsAgentToolSettingsDisabled(t *testing.T) {
	t.Setenv("AGENTTEAM_DATABASE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")
	t.Setenv("AGENTTEAM_AGENT_TOOLS_SETTINGS_ENABLED", "false")

	cfg, err := LoadFromEnv()
	require.NoError(t, err)
	require.False(t, cfg.AgentTools.SettingsEnabled)
}

func TestLoadFromEnvRejectsInvalidAgentToolSettingsEnabled(t *testing.T) {
	t.Setenv("AGENTTEAM_DATABASE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")
	t.Setenv("AGENTTEAM_AGENT_TOOLS_SETTINGS_ENABLED", "off")

	_, err := LoadFromEnv()
	require.EqualError(t, err, "AGENTTEAM_AGENT_TOOLS_SETTINGS_ENABLED must be true or false")
}

func TestLoadFromEnvRejectsInvalidEntryVisibilityFlags(t *testing.T) {
	t.Setenv("AGENTTEAM_DATABASE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")
	t.Setenv("AGENTTEAM_MESHX_KNOWLEDGE_BASES_ENABLED", "off")

	_, err := LoadFromEnv()
	require.EqualError(t, err, "AGENTTEAM_MESHX_KNOWLEDGE_BASES_ENABLED must be true or false")
}

func TestLoadFromEnvReadsPasswordAuthDisabled(t *testing.T) {
	t.Setenv("AGENTTEAM_DATABASE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")
	t.Setenv("AGENTTEAM_PASSWORD_AUTH_ENABLED", "false")
	t.Setenv("AGENTTEAM_PINGCAP_AUTH_ENABLED", "true")
	t.Setenv("AGENTTEAM_PINGCAP_AUTH_ISSUER", "https://auth.example.com")
	t.Setenv("AGENTTEAM_PINGCAP_AUTH_CLIENT_ID", "agentteam-web")

	cfg, err := LoadFromEnv()
	require.NoError(t, err)
	require.False(t, cfg.Auth.PasswordAuthEnabled())
}

func TestLoadFromEnvReadsSplitPingCAPAuthClients(t *testing.T) {
	t.Setenv("AGENTTEAM_DATABASE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")
	t.Setenv("AGENTTEAM_PINGCAP_AUTH_ENABLED", "true")
	t.Setenv("AGENTTEAM_PINGCAP_AUTH_ISSUER", "https://auth.example.com")
	t.Setenv("AGENTTEAM_PINGCAP_AUTH_WEB_CLIENT_ID", "agentteam-web")
	t.Setenv("AGENTTEAM_PINGCAP_AUTH_WEB_CLIENT_SECRET", "web-secret")
	t.Setenv("AGENTTEAM_PINGCAP_AUTH_DESKTOP_CLIENT_ID", "agentteam-desktop")

	cfg, err := LoadFromEnv()
	require.NoError(t, err)
	require.Equal(t, "agentteam-web", cfg.PingCAPAuth.WebClientID)
	require.Equal(t, "web-secret", cfg.PingCAPAuth.WebClientSecret)
	require.Equal(t, "agentteam-desktop", cfg.PingCAPAuth.DesktopClientID)
}

func TestLoadFromEnvRejectsInvalidPasswordAuthEnabled(t *testing.T) {
	t.Setenv("AGENTTEAM_DATABASE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")
	t.Setenv("AGENTTEAM_PASSWORD_AUTH_ENABLED", "off")

	_, err := LoadFromEnv()
	require.EqualError(t, err, "AGENTTEAM_PASSWORD_AUTH_ENABLED must be true or false")
}

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

	cfg := minimalValidConfig()
	cfg.Auth.PasswordAuthDisabled = true

	err := cfg.Validate()
	require.EqualError(t, err, "AGENTTEAM_PINGCAP_AUTH_ENABLED is required when AGENTTEAM_PASSWORD_AUTH_ENABLED=false")
}

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

	cfg := minimalValidConfig()
	cfg.Auth.PasswordAuthDisabled = true
	cfg.PingCAPAuth.Enabled = true

	err := cfg.Validate()
	require.EqualError(t, err, "AGENTTEAM_PINGCAP_AUTH_ISSUER is required when PingCAP auth is the only login method")

	cfg.PingCAPAuth.Issuer = "https://auth.example.com"
	err = cfg.Validate()
	require.EqualError(t, err, "AGENTTEAM_PINGCAP_AUTH_WEB_CLIENT_ID or AGENTTEAM_PINGCAP_AUTH_CLIENT_ID is required when PingCAP auth is the only login method")

	cfg.PingCAPAuth.ClientID = "agentteam-web"
	require.NoError(t, cfg.Validate())
}

func TestLoadFromEnvReadsLLMOutboundProxy(t *testing.T) {
	t.Setenv("AGENTTEAM_DATABASE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")
	t.Setenv("AGENTTEAM_LLM_OUTBOUND_PROXY", "127.0.0.1:1080")

	cfg, err := LoadFromEnv()
	require.NoError(t, err)
	require.Equal(t, "127.0.0.1:1080", cfg.LLM.OutboundProxy)

	proxyURL, err := cfg.LLM.OutboundProxyURL()
	require.NoError(t, err)
	require.NotNil(t, proxyURL)
	require.Equal(t, "socks5", proxyURL.Scheme)
	require.Equal(t, "127.0.0.1:1080", proxyURL.Host)
}

func TestLoadFromEnvReadsMaaSDirectLLMConfig(t *testing.T) {
	t.Setenv("AGENTTEAM_DATABASE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")
	t.Setenv("AGENTTEAM_MAAS_BASE_URL", "http://127.0.0.1:8080")
	t.Setenv("AGENTTEAM_MAAS_RUNTIME_BASE_URL", "http://127.0.0.1:8080/maas/v1")
	t.Setenv("AGENTTEAM_MAAS_INTERNAL_BEARER_TOKEN", "agentteam-maas-dev-internal-token")
	t.Setenv("AGENTTEAM_MAAS_CAPABILITY_ED25519_PRIVATE_KEY_FILE", "/tmp/capability-ed25519.json")

	cfg, err := LoadFromEnv()
	require.NoError(t, err)
	require.True(t, cfg.LLM.MaaSDirectConfigured())
	require.Equal(t, "http://127.0.0.1:8080", cfg.LLM.MaaSBaseURL)
	require.Equal(t, "http://127.0.0.1:8080/maas/v1", cfg.LLM.MaaSRuntimeBaseURL)
	require.Equal(t, "agentteam-maas-dev-internal-token", cfg.LLM.MaaSInternalBearerToken)
	require.Equal(t, "/tmp/capability-ed25519.json", cfg.LLM.MaaSCapabilityPrivateKeyFile)
}

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

	cfg := Config{
		Database: DatabaseConfig{
			ConnectTimeout: time.Second,
		},
		Archive: ArchiveConfig{
			Driver:         archiveDriverFilesystem,
			FilesystemRoot: ".data/archive",
		},
		Mail: MailConfig{Driver: mailDriverNoop},
		Auth: AuthConfig{
			VerificationTokenTTL: time.Second,
			SessionTTL:           time.Second,
		},
		Messages: MessagesConfig{
			HotRetentionDays: 30,
		},
		HTTP: HTTPConfig{ShutdownTimeout: time.Second},
		Daemon: DaemonConfig{
			LatestVersionCacheTTL: time.Hour,
		},
	}

	err := cfg.Validate()
	require.EqualError(t, err, "AGENTTEAM_DATABASE_DSN is required")
}

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

	cfg := Config{}
	err := cfg.Validate()
	require.EqualError(t, err, "AGENTTEAM_DATABASE_DSN is required")
}

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

	cfg := minimalValidConfig()
	cfg.Database.Pool.MaxOpenConns = 3
	cfg.Database.Pool.MaxIdleConns = 4

	err := cfg.Validate()
	require.EqualError(t, err, "AGENTTEAM_DATABASE_MAX_IDLE_CONNS must be less than or equal to AGENTTEAM_DATABASE_MAX_OPEN_CONNS when AGENTTEAM_DATABASE_MAX_OPEN_CONNS is set")

	cfg.Database.Pool.MaxIdleConns = 3
	cfg.Database.Pool.ConnMaxLifetime = -time.Second
	err = cfg.Validate()
	require.EqualError(t, err, "AGENTTEAM_DATABASE_CONN_MAX_LIFETIME must be non-negative")

	cfg.Database.Pool.ConnMaxLifetime = 0
	cfg.Database.Pool.ConnMaxIdleTime = -time.Second
	err = cfg.Validate()
	require.EqualError(t, err, "AGENTTEAM_DATABASE_CONN_MAX_IDLE_TIME must be non-negative")
}

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

	cfg := Config{
		Database: DatabaseConfig{URL: "root:@tcp(127.0.0.1:4000)/agentteam?tls=false"},
		Archive:  ArchiveConfig{Driver: archiveDriverS3},
		Mail:     MailConfig{Driver: mailDriverNoop},
		Auth: AuthConfig{
			VerificationTokenTTL: 1,
			SessionTTL:           1,
		},
		HTTP: HTTPConfig{ShutdownTimeout: 1},
	}

	err := cfg.Validate()
	require.EqualError(t, err, "AGENTTEAM_ARCHIVE_S3_REGION is required when archive driver is s3")
}

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

	cfg := Config{
		Database: DatabaseConfig{
			URL:            "root:@tcp(127.0.0.1:4000)/agentteam?tls=false",
			ConnectTimeout: 1,
		},
		Archive: ArchiveConfig{
			Driver:         archiveDriverFilesystem,
			FilesystemRoot: ".data/archive",
		},
		Mail: MailConfig{Driver: mailDriverNoop},
		Auth: AuthConfig{
			VerificationTokenTTL: 1,
			SessionTTL:           1,
		},
		Messages: MessagesConfig{
			HotRetentionDays: 30,
		},
		HTTP: HTTPConfig{ShutdownTimeout: 1},
		Daemon: DaemonConfig{
			LatestVersionCacheTTL: time.Hour,
		},
	}

	require.NoError(t, cfg.Validate())
}

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

	cfg := Config{
		Database: DatabaseConfig{
			URL:            "root:@tcp(127.0.0.1:4000)/agentteam?tls=false",
			ConnectTimeout: 1,
		},
		LLM: LLMConfig{
			OutboundProxy: "ftp://proxy.example.com:21",
		},
		Archive: ArchiveConfig{
			Driver:         archiveDriverFilesystem,
			FilesystemRoot: ".data/archive",
		},
		Mail: MailConfig{Driver: mailDriverNoop},
		Auth: AuthConfig{
			VerificationTokenTTL: 1,
			SessionTTL:           1,
		},
		Messages: MessagesConfig{
			HotRetentionDays: 30,
		},
		HTTP: HTTPConfig{ShutdownTimeout: 1},
		Daemon: DaemonConfig{
			LatestVersionCacheTTL: time.Hour,
		},
	}

	err := cfg.Validate()
	require.EqualError(t, err, "AGENTTEAM_LLM_OUTBOUND_PROXY must use http, https, socks5, or socks5h")
}

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

	cfg := minimalValidConfig()
	cfg.LLM.MaaSBaseURL = "http://127.0.0.1:8080"

	err := cfg.Validate()
	require.EqualError(t, err, "AGENTTEAM_MAAS_RUNTIME_BASE_URL is required when MaaS direct LLM is configured")

	cfg.LLM.MaaSRuntimeBaseURL = "http://127.0.0.1:8080/maas/v1"
	cfg.LLM.MaaSInternalBearerToken = "token"

	err = cfg.Validate()
	require.EqualError(t, err, "AGENTTEAM_MAAS_CAPABILITY_ED25519_PRIVATE_KEY_FILE is required when MaaS direct LLM is configured")
}

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

	cfg := minimalValidConfig()
	cfg.LLM.MaaSBaseURL = "http://127.0.0.1:8080"
	cfg.LLM.MaaSRuntimeBaseURL = "http://localhost:8080/maas/v1"
	cfg.LLM.MaaSInternalBearerToken = "token"
	cfg.LLM.MaaSCapabilityPrivateKeyFile = "/tmp/capability-ed25519.json"

	require.NoError(t, cfg.Validate())
}

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

	cases := []struct {
		name    string
		baseURL string
	}{
		{name: "same namespace service", baseURL: "http://agentteam-maas:8080"},
		{name: "namespace service", baseURL: "http://agentteam-maas.prod-agentteam-shared-free.svc:8080"},
		{name: "cluster local service", baseURL: "http://agentteam-maas.prod-agentteam-shared-free.svc.cluster.local:8080"},
	}
	for _, tc := range cases {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			cfg := minimalValidConfig()
			cfg.Environment = "production"
			cfg.LLM.MaaSBaseURL = tc.baseURL
			cfg.LLM.MaaSRuntimeBaseURL = "https://maas.agentteam.pingcap.com/maas/v1"
			cfg.LLM.MaaSInternalBearerToken = "token"
			cfg.LLM.MaaSCapabilityPrivateKeyFile = "/tmp/capability-ed25519.json"

			require.NoError(t, cfg.Validate())
		})
	}
}

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

	cfg := minimalValidConfig()
	cfg.Environment = "production"
	cfg.Billing.Enabled = true
	cfg.Billing.Alipay.AppID = "2021000000000000"
	cfg.Billing.Alipay.AppPrivateKey = "private-key"
	cfg.Billing.Alipay.AlipayPublicKey = "public-key"
	cfg.Billing.Alipay.GatewayURL = "https://openapi.alipay.com/gateway.do"
	cfg.LLM.MaaSBaseURL = "http://agentteam-maas:8080"
	cfg.LLM.MaaSInternalBearerToken = "token"

	require.NoError(t, cfg.Validate())
	require.False(t, cfg.LLM.MaaSDirectConfigured())
}

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

	cfg := minimalValidConfig()
	cfg.Environment = "production"
	cfg.LLM.MaaSBaseURL = "http://maas.example.test:8080"
	cfg.LLM.MaaSRuntimeBaseURL = "https://maas.agentteam.pingcap.com/maas/v1"
	cfg.LLM.MaaSInternalBearerToken = "token"
	cfg.LLM.MaaSCapabilityPrivateKeyFile = "/tmp/capability-ed25519.json"

	err := cfg.Validate()
	require.EqualError(t, err, "AGENTTEAM_MAAS_BASE_URL must use https in production")
}

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

	cfg := minimalValidConfig()
	cfg.Environment = "production"
	cfg.LLM.MaaSBaseURL = "http://agentteam-maas:8080"
	cfg.LLM.MaaSRuntimeBaseURL = "http://agentteam-maas:8080/maas/v1"
	cfg.LLM.MaaSInternalBearerToken = "token"
	cfg.LLM.MaaSCapabilityPrivateKeyFile = "/tmp/capability-ed25519.json"

	err := cfg.Validate()
	require.EqualError(t, err, "AGENTTEAM_MAAS_RUNTIME_BASE_URL must use https unless it targets loopback")
}

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

	cfg := minimalValidConfig()
	cfg.LLM.MaaSBaseURL = "http://127.0.0.1:8080"
	cfg.LLM.MaaSRuntimeBaseURL = "http://maas.example.test/maas/v1"
	cfg.LLM.MaaSInternalBearerToken = "token"
	cfg.LLM.MaaSCapabilityPrivateKeyFile = "/tmp/capability-ed25519.json"

	err := cfg.Validate()
	require.EqualError(t, err, "AGENTTEAM_MAAS_RUNTIME_BASE_URL must use https unless it targets loopback")
}

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

	cfg := minimalValidConfig()
	cfg.Billing.Enabled = true
	cfg.Billing.Alipay.AppID = "2021000000000000"
	cfg.Billing.Alipay.AppPrivateKey = "private-key"
	cfg.Billing.Alipay.AlipayPublicKey = "public-key"
	cfg.Billing.Alipay.GatewayURL = "https://openapi.alipay.com/gateway.do"
	cfg.LLM.MaaSBaseURL = "http://127.0.0.1:8080"
	cfg.LLM.MaaSInternalBearerToken = "token"

	require.NoError(t, cfg.Validate())
	require.False(t, cfg.LLM.MaaSDirectConfigured())
}

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

	cfg := minimalValidConfig()
	cfg.LLM.MaaSBaseURL = "http://127.0.0.1:8080"
	cfg.LLM.MaaSRuntimeBaseURL = "http://127.0.0.1:8080/api"
	cfg.LLM.MaaSInternalBearerToken = "token"
	cfg.LLM.MaaSCapabilityPrivateKeyFile = "/tmp/capability-ed25519.json"

	err := cfg.Validate()
	require.EqualError(t, err, "AGENTTEAM_MAAS_RUNTIME_BASE_URL path must end with /maas/v1")
}

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

	cfg := Config{
		Database: DatabaseConfig{
			URL:            "root:@tcp(127.0.0.1:4000)/agentteam?tls=false",
			ConnectTimeout: 1,
		},
		Archive: ArchiveConfig{
			Driver:         archiveDriverFilesystem,
			FilesystemRoot: ".data/archive",
		},
		Mail: MailConfig{Driver: mailDriverNoop},
		Auth: AuthConfig{
			VerificationTokenTTL: 1,
			SessionTTL:           1,
		},
		Messages: MessagesConfig{
			HotRetentionDays: 30,
		},
		HTTP: HTTPConfig{ShutdownTimeout: 1},
		Daemon: DaemonConfig{
			ConnectBaseURL:        "not-a-url",
			LatestVersionCacheTTL: time.Hour,
		},
	}

	err := cfg.Validate()
	require.EqualError(t, err, "AGENTTEAM_DAEMON_CONNECT_BASE_URL must use http(s)")
}

func TestLoadFromEnvDefaultsMeshXDisabled(t *testing.T) {
	t.Setenv("AGENTTEAM_DATABASE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")

	cfg, err := LoadFromEnv()
	require.NoError(t, err)
	require.False(t, cfg.MeshX.Enabled)
}

func TestLoadFromEnvReadsMeshXEnabled(t *testing.T) {
	t.Setenv("AGENTTEAM_DATABASE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")
	t.Setenv("AGENTTEAM_MESHX_ENABLED", "true")

	cfg, err := LoadFromEnv()
	require.NoError(t, err)
	require.True(t, cfg.MeshX.Enabled)
}

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

	cfg := Config{
		PublicBaseURL: "not-a-url",
		Database: DatabaseConfig{
			URL:            "root:@tcp(127.0.0.1:4000)/agentteam?tls=false",
			ConnectTimeout: 1,
		},
		Archive: ArchiveConfig{
			Driver:         archiveDriverFilesystem,
			FilesystemRoot: ".data/archive",
		},
		Mail: MailConfig{Driver: mailDriverNoop},
		Auth: AuthConfig{
			VerificationTokenTTL: 1,
			SessionTTL:           1,
		},
		Messages: MessagesConfig{
			HotRetentionDays: 30,
		},
		HTTP: HTTPConfig{ShutdownTimeout: 1},
		Daemon: DaemonConfig{
			LatestVersionCacheTTL: time.Hour,
		},
	}

	err := cfg.Validate()
	require.EqualError(t, err, "AGENTTEAM_PUBLIC_BASE_URL must use http(s)")
}

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

	cfg := minimalValidConfig()
	cfg.Daemon.LatestVersionURL = "ftp://static.example.com/agentteam/stable.json"

	err := cfg.Validate()
	require.EqualError(t, err, "AGENTTEAM_DAEMON_LATEST_VERSION_URL must use http(s)")
}

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

	cfg := Config{
		Environment:   "production",
		PublicBaseURL: "http://agentteam.pingcap.com",
		Database: DatabaseConfig{
			URL:            "root:@tcp(127.0.0.1:4000)/agentteam?tls=false",
			ConnectTimeout: 1,
		},
		Archive: ArchiveConfig{
			Driver:         archiveDriverFilesystem,
			FilesystemRoot: ".data/archive",
		},
		Mail: MailConfig{Driver: mailDriverNoop},
		Auth: AuthConfig{
			VerificationTokenTTL: 1,
			SessionTTL:           1,
		},
		Messages: MessagesConfig{
			HotRetentionDays: 30,
		},
		HTTP: HTTPConfig{ShutdownTimeout: 1},
		Daemon: DaemonConfig{
			LatestVersionCacheTTL: time.Hour,
		},
	}

	err := cfg.Validate()
	require.EqualError(t, err, "AGENTTEAM_PUBLIC_BASE_URL must use https in production")
}

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

	cfg := Config{
		Environment:   "production",
		PublicBaseURL: "https://agentteam.pingcap.com",
		Database: DatabaseConfig{
			URL:            "root:@tcp(127.0.0.1:4000)/agentteam?tls=false",
			ConnectTimeout: 1,
		},
		Archive: ArchiveConfig{
			Driver:         archiveDriverFilesystem,
			FilesystemRoot: ".data/archive",
		},
		Mail: MailConfig{Driver: mailDriverNoop},
		Auth: AuthConfig{
			VerificationTokenTTL: 1,
			SessionTTL:           1,
		},
		Messages: MessagesConfig{
			HotRetentionDays: 30,
		},
		HTTP: HTTPConfig{ShutdownTimeout: 1},
		Daemon: DaemonConfig{
			ConnectBaseURL:        "http://agentteam.pingcap.com",
			LatestVersionCacheTTL: time.Hour,
		},
	}

	err := cfg.Validate()
	require.EqualError(t, err, "AGENTTEAM_DAEMON_CONNECT_BASE_URL must use https in production")
}

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

	cfg := Config{
		Database: DatabaseConfig{
			URL:            "root:@tcp(127.0.0.1:4000)/agentteam?tls=false",
			ConnectTimeout: 1,
		},
		Archive: ArchiveConfig{
			Driver:         archiveDriverFilesystem,
			FilesystemRoot: ".data/archive",
		},
		Mail: MailConfig{
			Driver:      mailDriverSES,
			FromAddress: "noreply@example.com",
			SESRegion:   "ap-northeast-1",
		},
		Auth: AuthConfig{
			VerificationTokenTTL: 1,
			SessionTTL:           1,
		},
		Messages: MessagesConfig{
			HotRetentionDays: 30,
		},
		HTTP: HTTPConfig{ShutdownTimeout: 1},
		Daemon: DaemonConfig{
			LatestVersionCacheTTL: time.Hour,
		},
	}

	require.NoError(t, cfg.Validate())
}

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

	cfg := Config{
		Database: DatabaseConfig{
			URL:            "root:@tcp(127.0.0.1:4000)/agentteam?tls=false",
			ConnectTimeout: 1,
		},
		Archive: ArchiveConfig{
			Driver:         archiveDriverFilesystem,
			FilesystemRoot: ".data/archive",
		},
		Mail: MailConfig{
			Driver:       mailDriverSMTP,
			FromAddress:  "agentteam@pingcap.com",
			SMTPHost:     "smtp.feishu.cn",
			SMTPPort:     587,
			SMTPUsername: "agentteam@pingcap.com",
			SMTPPassword: "secret",
			SMTPSecurity: mailSMTPSecuritySTARTTLS,
		},
		Auth: AuthConfig{
			VerificationTokenTTL: 1,
			SessionTTL:           1,
		},
		Messages: MessagesConfig{
			HotRetentionDays: 30,
		},
		HTTP: HTTPConfig{ShutdownTimeout: 1},
		Daemon: DaemonConfig{
			LatestVersionCacheTTL: time.Hour,
		},
	}

	require.NoError(t, cfg.Validate())
}

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

	cfg := Config{
		Database: DatabaseConfig{
			URL:            "root:@tcp(127.0.0.1:4000)/agentteam?tls=false",
			ConnectTimeout: 1,
		},
		Archive: ArchiveConfig{
			Driver:         archiveDriverFilesystem,
			FilesystemRoot: ".data/archive",
		},
		Mail: MailConfig{Driver: mailDriverNoop},
		MobileVerify: MobileVerifyConfig{
			Driver:           mobileVerifyDriverTwilio,
			TwilioAuthToken:  "secret",
			TwilioFromNumber: "+15557654321",
		},
		Auth: AuthConfig{
			VerificationTokenTTL: 1,
			SessionTTL:           1,
		},
		Messages: MessagesConfig{
			HotRetentionDays: 30,
		},
		HTTP: HTTPConfig{ShutdownTimeout: 1},
		Daemon: DaemonConfig{
			LatestVersionCacheTTL: time.Hour,
		},
	}

	err := cfg.Validate()
	require.EqualError(t, err, "AGENTTEAM_MOBILE_VERIFY_TWILIO_ACCOUNT_SID is required when mobile verify driver is twilio")
}

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

	cfg := Config{
		Database: DatabaseConfig{
			URL:            "root:@tcp(127.0.0.1:4000)/agentteam?tls=false",
			ConnectTimeout: 1,
		},
		Archive: ArchiveConfig{
			Driver:         archiveDriverFilesystem,
			FilesystemRoot: ".data/archive",
		},
		Mail: MailConfig{Driver: mailDriverNoop},
		MobileVerify: MobileVerifyConfig{
			Driver:           mobileVerifyDriverTwilio,
			TwilioAccountSID: "AC123",
			TwilioFromNumber: "+15557654321",
		},
		Auth: AuthConfig{
			VerificationTokenTTL: 1,
			SessionTTL:           1,
		},
		Messages: MessagesConfig{
			HotRetentionDays: 30,
		},
		HTTP: HTTPConfig{ShutdownTimeout: 1},
		Daemon: DaemonConfig{
			LatestVersionCacheTTL: time.Hour,
		},
	}

	err := cfg.Validate()
	require.EqualError(t, err, "AGENTTEAM_MOBILE_VERIFY_TWILIO_AUTH_TOKEN is required when mobile verify driver is twilio")
}

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

	cfg := Config{
		Database: DatabaseConfig{
			URL:            "root:@tcp(127.0.0.1:4000)/agentteam?tls=false",
			ConnectTimeout: 1,
		},
		Archive: ArchiveConfig{
			Driver:         archiveDriverFilesystem,
			FilesystemRoot: ".data/archive",
		},
		Mail: MailConfig{Driver: mailDriverNoop},
		MobileVerify: MobileVerifyConfig{
			Driver:           mobileVerifyDriverTwilio,
			TwilioAccountSID: "AC123",
			TwilioAuthToken:  "secret",
		},
		Auth: AuthConfig{
			VerificationTokenTTL: 1,
			SessionTTL:           1,
		},
		Messages: MessagesConfig{
			HotRetentionDays: 30,
		},
		HTTP: HTTPConfig{ShutdownTimeout: 1},
		Daemon: DaemonConfig{
			LatestVersionCacheTTL: time.Hour,
		},
	}

	err := cfg.Validate()
	require.EqualError(t, err, "AGENTTEAM_MOBILE_VERIFY_TWILIO_FROM_NUMBER or AGENTTEAM_MOBILE_VERIFY_TWILIO_MESSAGING_SERVICE_SID is required when mobile verify driver is twilio")
}

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

	cfg := Config{
		Database: DatabaseConfig{
			URL:            "root:@tcp(127.0.0.1:4000)/agentteam?tls=false",
			ConnectTimeout: 1,
		},
		Archive: ArchiveConfig{
			Driver:         archiveDriverFilesystem,
			FilesystemRoot: ".data/archive",
		},
		Mail: MailConfig{Driver: mailDriverNoop},
		MobileVerify: MobileVerifyConfig{
			Driver:                    mobileVerifyDriverTwilio,
			TwilioAccountSID:          "AC123",
			TwilioAuthToken:           "secret",
			TwilioFromNumber:          "+15557654321",
			TwilioMessagingServiceSID: "MG123",
		},
		Auth: AuthConfig{
			VerificationTokenTTL: 1,
			SessionTTL:           1,
		},
		Messages: MessagesConfig{
			HotRetentionDays: 30,
		},
		HTTP: HTTPConfig{ShutdownTimeout: 1},
		Daemon: DaemonConfig{
			LatestVersionCacheTTL: time.Hour,
		},
	}

	err := cfg.Validate()
	require.EqualError(t, err, "AGENTTEAM_MOBILE_VERIFY_TWILIO_FROM_NUMBER and AGENTTEAM_MOBILE_VERIFY_TWILIO_MESSAGING_SERVICE_SID are mutually exclusive")
}

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

	cfg := Config{
		Database: DatabaseConfig{
			URL:            "root:@tcp(127.0.0.1:4000)/agentteam?tls=false",
			ConnectTimeout: 1,
		},
		Archive: ArchiveConfig{
			Driver:         archiveDriverFilesystem,
			FilesystemRoot: ".data/archive",
		},
		Mail: MailConfig{Driver: mailDriverNoop},
		MobileVerify: MobileVerifyConfig{
			Driver:           mobileVerifyDriverTwilio,
			TwilioAccountSID: "AC123",
			TwilioAuthToken:  "secret",
			TwilioFromNumber: "+15557654321",
		},
		Auth: AuthConfig{
			VerificationTokenTTL: 1,
			SessionTTL:           1,
		},
		Messages: MessagesConfig{
			HotRetentionDays: 30,
		},
		HTTP: HTTPConfig{ShutdownTimeout: 1},
		Daemon: DaemonConfig{
			LatestVersionCacheTTL: time.Hour,
		},
	}

	require.NoError(t, cfg.Validate())
}

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

	cfg := Config{
		Database: DatabaseConfig{
			URL:            "root:@tcp(127.0.0.1:4000)/agentteam?tls=false",
			ConnectTimeout: 1,
		},
		Archive: ArchiveConfig{
			Driver:         archiveDriverFilesystem,
			FilesystemRoot: ".data/archive",
		},
		Mail: MailConfig{Driver: mailDriverNoop},
		MobileVerify: MobileVerifyConfig{
			Driver:                    mobileVerifyDriverTwilio,
			TwilioAccountSID:          "AC123",
			TwilioAuthToken:           "secret",
			TwilioMessagingServiceSID: "MG123",
		},
		Auth: AuthConfig{
			VerificationTokenTTL: 1,
			SessionTTL:           1,
		},
		Messages: MessagesConfig{
			HotRetentionDays: 30,
		},
		HTTP: HTTPConfig{ShutdownTimeout: 1},
		Daemon: DaemonConfig{
			LatestVersionCacheTTL: time.Hour,
		},
	}

	require.NoError(t, cfg.Validate())
}

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

	cfg := Config{
		Database: DatabaseConfig{
			URL:            "root:@tcp(127.0.0.1:4000)/agentteam?tls=false",
			ConnectTimeout: 1,
		},
		Archive: ArchiveConfig{
			Driver:         archiveDriverFilesystem,
			FilesystemRoot: ".data/archive",
		},
		Mail: MailConfig{
			Driver:      mailDriverSES,
			FromAddress: "AgentTeam <noreply@example.com>",
			SESRegion:   "ap-northeast-1",
		},
		Auth: AuthConfig{
			VerificationTokenTTL: 1,
			SessionTTL:           1,
		},
		HTTP: HTTPConfig{ShutdownTimeout: 1},
	}

	err := cfg.Validate()
	require.EqualError(t, err, "AGENTTEAM_MAIL_FROM_ADDRESS must be a plain email address (e.g. noreply@example.com); got \"AgentTeam <noreply@example.com>\"")
}

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

	cfg := Config{
		Database: DatabaseConfig{
			URL:            "root:@tcp(127.0.0.1:4000)/agentteam?tls=false",
			ConnectTimeout: 1,
		},
		Archive: ArchiveConfig{
			Driver:         archiveDriverFilesystem,
			FilesystemRoot: ".data/archive",
		},
		Mail: MailConfig{
			Driver:      mailDriverSES,
			FromAddress: "example.com",
			SESRegion:   "ap-northeast-1",
		},
		Auth: AuthConfig{
			VerificationTokenTTL: 1,
			SessionTTL:           1,
		},
		HTTP: HTTPConfig{ShutdownTimeout: 1},
	}

	err := cfg.Validate()
	require.ErrorContains(t, err, "AGENTTEAM_MAIL_FROM_ADDRESS must be a valid email address:")
}

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

	cfg := Config{Mail: MailConfig{Driver: mailDriverNoop}}
	require.False(t, cfg.EmailVerificationRequired())

	cfg.Mail.Driver = mailDriverSES
	require.True(t, cfg.EmailVerificationRequired())

	cfg.Mail.Driver = mailDriverSMTP
	require.True(t, cfg.EmailVerificationRequired())
}

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

	cfg := Config{
		Database: DatabaseConfig{
			URL:            "root:@tcp(127.0.0.1:4000)/agentteam?tls=false",
			ConnectTimeout: 1,
		},
		Archive: ArchiveConfig{
			Driver:         archiveDriverFilesystem,
			FilesystemRoot: ".data/archive",
		},
		Mail: MailConfig{
			Driver:       mailDriverSMTP,
			FromAddress:  "agentteam@pingcap.com",
			SMTPPort:     587,
			SMTPUsername: "agentteam@pingcap.com",
			SMTPPassword: "secret",
			SMTPSecurity: mailSMTPSecuritySTARTTLS,
		},
		Auth: AuthConfig{
			VerificationTokenTTL: 1,
			SessionTTL:           1,
		},
		Messages: MessagesConfig{
			HotRetentionDays: 30,
		},
		HTTP: HTTPConfig{ShutdownTimeout: 1},
		Daemon: DaemonConfig{
			LatestVersionCacheTTL: time.Hour,
		},
	}

	err := cfg.Validate()
	require.EqualError(t, err, "AGENTTEAM_MAIL_SMTP_HOST is required when mail driver is smtp")
}

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

	cfg := Config{
		Database: DatabaseConfig{
			URL:            "root:@tcp(127.0.0.1:4000)/agentteam?tls=false",
			ConnectTimeout: 1,
		},
		Archive: ArchiveConfig{
			Driver:         archiveDriverFilesystem,
			FilesystemRoot: ".data/archive",
		},
		Mail: MailConfig{
			Driver:       mailDriverSMTP,
			FromAddress:  "agentteam@pingcap.com",
			SMTPHost:     "smtp.feishu.cn",
			SMTPPort:     587,
			SMTPUsername: "agentteam@pingcap.com",
			SMTPPassword: "secret",
			SMTPSecurity: "plaintext",
		},
		Auth: AuthConfig{
			VerificationTokenTTL: 1,
			SessionTTL:           1,
		},
		Messages: MessagesConfig{
			HotRetentionDays: 30,
		},
		HTTP: HTTPConfig{ShutdownTimeout: 1},
		Daemon: DaemonConfig{
			LatestVersionCacheTTL: time.Hour,
		},
	}

	err := cfg.Validate()
	require.EqualError(t, err, `AGENTTEAM_MAIL_SMTP_SECURITY must be one of "ssl" or "starttls" when mail driver is smtp`)
}

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

	cfg := Config{
		Database: DatabaseConfig{
			URL:            "root:@tcp(127.0.0.1:4000)/agentteam?tls=false",
			ConnectTimeout: 1,
		},
		Archive: ArchiveConfig{
			Driver:         archiveDriverFilesystem,
			FilesystemRoot: ".data/archive",
		},
		Mail: MailConfig{Driver: mailDriverNoop},
		Auth: AuthConfig{
			VerificationTokenTTL: 1,
			SessionTTL:           1,
		},
		Messages: MessagesConfig{
			HotRetentionDays: 30,
		},
		HTTP: HTTPConfig{ShutdownTimeout: 1},
		Daemon: DaemonConfig{
			LatestVersionCacheTTL: time.Hour,
		},
		Feishu: FeishuConfig{
			OrgCredentialsMasterKey: "not-base64",
		},
	}

	err := cfg.Validate()
	require.EqualError(t, err, "AGENTTEAM_ORG_CREDENTIALS_MASTER_KEY must be base64-encoded 32-byte key")
}

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

	cfg := Config{
		Database: DatabaseConfig{
			URL:            "root:@tcp(127.0.0.1:4000)/agentteam?tls=false",
			ConnectTimeout: 1,
		},
		Archive: ArchiveConfig{
			Driver:         archiveDriverFilesystem,
			FilesystemRoot: ".data/archive",
		},
		Mail: MailConfig{Driver: mailDriverNoop},
		Auth: AuthConfig{
			VerificationTokenTTL: 1,
			SessionTTL:           1,
		},
		Messages: MessagesConfig{
			HotRetentionDays: 30,
		},
		HTTP: HTTPConfig{ShutdownTimeout: 1},
		Daemon: DaemonConfig{
			LatestVersionCacheTTL: time.Hour,
		},
		Feishu: FeishuConfig{
			OrgCredentialsMasterKey: base64.StdEncoding.EncodeToString(bytes.Repeat([]byte{1}, 16)),
		},
	}

	err := cfg.Validate()
	require.EqualError(t, err, "AGENTTEAM_ORG_CREDENTIALS_MASTER_KEY must decode to exactly 32 bytes")
}

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

	cfg := Config{
		Database: DatabaseConfig{
			URL:            "root:@tcp(127.0.0.1:4000)/agentteam?tls=false",
			ConnectTimeout: 1,
		},
		Archive: ArchiveConfig{
			Driver:         archiveDriverFilesystem,
			FilesystemRoot: ".data/archive",
		},
		Mail: MailConfig{Driver: mailDriverNoop},
		Auth: AuthConfig{
			VerificationTokenTTL: 1,
			SessionTTL:           1,
		},
		Messages: MessagesConfig{
			HotRetentionDays: 30,
		},
		HTTP: HTTPConfig{ShutdownTimeout: 1},
		Daemon: DaemonConfig{
			LatestVersionCacheTTL: time.Hour,
		},
		Feishu: FeishuConfig{
			OrgCredentialsMasterKey: base64.StdEncoding.EncodeToString(bytes.Repeat([]byte{1}, 32)),
		},
	}

	require.NoError(t, cfg.Validate())

	key, err := cfg.Feishu.CredentialEncryptionKey()
	require.NoError(t, err)
	require.Len(t, key, 32)
}

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

	cfg := minimalValidConfig()
	cfg.AppCredentials.MasterKey = "not-base64"

	err := cfg.Validate()
	require.EqualError(t, err, "AGENTTEAM_APP_CREDENTIALS_MASTER_KEY must be base64-encoded 32-byte key")
}

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

	cfg := minimalValidConfig()
	cfg.AppCredentials.MasterKey = base64.StdEncoding.EncodeToString(bytes.Repeat([]byte{1}, 16))

	err := cfg.Validate()
	require.EqualError(t, err, "AGENTTEAM_APP_CREDENTIALS_MASTER_KEY must decode to exactly 32 bytes")
}

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

	cfg := minimalValidConfig()
	cfg.AppCredentials.MasterKey = base64.StdEncoding.EncodeToString(bytes.Repeat([]byte{1}, 32))

	require.NoError(t, cfg.Validate())

	key, err := cfg.AppCredentials.CredentialEncryptionKey()
	require.NoError(t, err)
	require.Len(t, key, 32)
}

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

	keyFilePath := filepath.Join(t.TempDir(), "secrets", "org_credentials_master_key")
	cfg := FeishuConfig{
		OrgCredentialsKeyFile: keyFilePath,
	}

	resolution, err := cfg.ResolveCredentialEncryptionKey()
	require.NoError(t, err)
	require.Equal(t, "generated", resolution.Source)
	require.Equal(t, keyFilePath, resolution.Path)
	require.Len(t, resolution.Key, 32)

	info, err := os.Stat(keyFilePath)
	require.NoError(t, err)
	require.Equal(t, os.FileMode(0o600), info.Mode().Perm())

	reloaded, err := cfg.ResolveCredentialEncryptionKey()
	require.NoError(t, err)
	require.Equal(t, "file", reloaded.Source)
	require.Equal(t, resolution.Key, reloaded.Key)
}

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

	keyFilePath := filepath.Join(t.TempDir(), "secrets", "org_credentials_master_key")
	fileKey := base64.StdEncoding.EncodeToString(bytes.Repeat([]byte{1}, 32))
	require.NoError(t, os.MkdirAll(filepath.Dir(keyFilePath), 0o700))
	require.NoError(t, os.WriteFile(keyFilePath, []byte(fileKey), 0o600))

	envKeyBytes := bytes.Repeat([]byte{2}, 32)
	cfg := FeishuConfig{
		OrgCredentialsMasterKey: base64.StdEncoding.EncodeToString(envKeyBytes),
		OrgCredentialsKeyFile:   keyFilePath,
	}

	resolution, err := cfg.ResolveCredentialEncryptionKey()
	require.NoError(t, err)
	require.Equal(t, "environment", resolution.Source)
	require.Empty(t, resolution.Path)
	require.Equal(t, envKeyBytes, resolution.Key)
}

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

	keyFilePath := filepath.Join(t.TempDir(), "secrets", "app_credentials_master_key")
	cfg := AppCredentialsConfig{
		KeyFile: keyFilePath,
	}

	resolution, err := cfg.ResolveCredentialEncryptionKey()
	require.NoError(t, err)
	require.Equal(t, "generated", resolution.Source)
	require.Equal(t, keyFilePath, resolution.Path)
	require.Len(t, resolution.Key, 32)

	info, err := os.Stat(keyFilePath)
	require.NoError(t, err)
	require.Equal(t, os.FileMode(0o600), info.Mode().Perm())

	reloaded, err := cfg.ResolveCredentialEncryptionKey()
	require.NoError(t, err)
	require.Equal(t, "file", reloaded.Source)
	require.Equal(t, resolution.Key, reloaded.Key)
}

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

	keyFilePath := filepath.Join(t.TempDir(), "secrets", "app_credentials_master_key")
	fileKey := base64.StdEncoding.EncodeToString(bytes.Repeat([]byte{1}, 32))
	require.NoError(t, os.MkdirAll(filepath.Dir(keyFilePath), 0o700))
	require.NoError(t, os.WriteFile(keyFilePath, []byte(fileKey), 0o600))

	envKeyBytes := bytes.Repeat([]byte{2}, 32)
	cfg := AppCredentialsConfig{
		MasterKey: base64.StdEncoding.EncodeToString(envKeyBytes),
		KeyFile:   keyFilePath,
	}

	resolution, err := cfg.ResolveCredentialEncryptionKey()
	require.NoError(t, err)
	require.Equal(t, "environment", resolution.Source)
	require.Empty(t, resolution.Path)
	require.Equal(t, envKeyBytes, resolution.Key)
}

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

	cfg := Config{
		Database: DatabaseConfig{
			URL:            "root:@tcp(127.0.0.1:4000)/agentteam?tls=false",
			ConnectTimeout: 1,
		},
		Archive: ArchiveConfig{
			Driver:         archiveDriverFilesystem,
			FilesystemRoot: ".data/archive",
		},
		MediaStorage: MediaStorageConfig{
			Driver:   mediaStorageDriverOSS,
			Region:   "cn-beijing",
			Bucket:   "agentteam-static",
			Endpoint: "https://oss-cn-beijing.aliyuncs.com",
		},
		Mail: MailConfig{Driver: mailDriverNoop},
		Auth: AuthConfig{
			VerificationTokenTTL: 1,
			SessionTTL:           1,
		},
		Messages: MessagesConfig{HotRetentionDays: 30},
		HTTP:     HTTPConfig{ShutdownTimeout: 1},
		Daemon: DaemonConfig{
			LatestVersionCacheTTL: time.Hour,
		},
	}

	err := cfg.Validate()
	require.EqualError(t, err, "AGENTTEAM_MEDIA_OSS_ACCESS_KEY_ID is required when media object storage driver is oss")
}

func TestLoadFromEnvDefaultsMediaStorageToDisabled(t *testing.T) {
	t.Setenv("AGENTTEAM_DATABASE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")

	cfg, err := LoadFromEnv()
	require.NoError(t, err)
	require.Equal(t, mediaStorageDriverDisabled, cfg.MediaStorage.Driver)
	require.Equal(t, "agentteam/objects", cfg.MediaStorage.Prefix)
}

func TestLoadFromEnvReadsMediaPublicBaseURL(t *testing.T) {
	t.Setenv("AGENTTEAM_DATABASE_DSN", "root:@tcp(127.0.0.1:4000)/agentteam?tls=false")
	t.Setenv("AGENTTEAM_MEDIA_PUBLIC_BASE_URL", "https://media.agentteam.pingcap.com")

	cfg, err := LoadFromEnv()
	require.NoError(t, err)
	require.Equal(t, "https://media.agentteam.pingcap.com", cfg.MediaStorage.PublicBaseURL)
}

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

	cfg := Config{
		Database: DatabaseConfig{
			URL:            "root:@tcp(127.0.0.1:4000)/agentteam?tls=false",
			ConnectTimeout: 1,
		},
		Archive: ArchiveConfig{
			Driver:         archiveDriverFilesystem,
			FilesystemRoot: ".data/archive",
		},
		MediaStorage: MediaStorageConfig{
			PublicBaseURL: "not-a-url",
		},
		Mail: MailConfig{Driver: mailDriverNoop},
		Auth: AuthConfig{
			VerificationTokenTTL: 1,
			SessionTTL:           1,
		},
		Messages: MessagesConfig{HotRetentionDays: 30},
		HTTP:     HTTPConfig{ShutdownTimeout: 1},
		Daemon: DaemonConfig{
			LatestVersionCacheTTL: time.Hour,
		},
	}

	err := cfg.Validate()
	require.EqualError(t, err, "AGENTTEAM_MEDIA_PUBLIC_BASE_URL must use http(s)")
}
