package config

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

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

func TestLoadDatabaseAutoMigrate(t *testing.T) {
	t.Setenv("MESHX_AGS_DATABASE_DSN", "file:ags.db")
	t.Setenv("MESHX_DATABASE_DSN", "file:meshx.db")

	cfg, err := LoadResourceRuntimeFromEnv()
	require.NoError(t, err)
	require.False(t, cfg.Database.DisableAutoMigrate)

	t.Setenv("MESHX_DATABASE_AUTO_MIGRATE", "false")
	cfg, err = LoadResourceRuntimeFromEnv()
	require.NoError(t, err)
	require.True(t, cfg.Database.DisableAutoMigrate)
}

func TestKnowledgeQADebugModelOutputIsExplicitlyEnabled(t *testing.T) {
	t.Setenv("MESHX_AGS_DATABASE_DSN", "file:ags.db")
	t.Setenv("MESHX_DATABASE_DSN", "file:meshx.db")
	t.Setenv("MESHX_KNOWLEDGE_QA_DEBUG_MODEL_OUTPUT", "")

	cfg, err := LoadResourceRuntimeFromEnv()
	require.NoError(t, err)
	require.False(t, cfg.KnowledgeQA.DebugModelOutput)

	t.Setenv("MESHX_KNOWLEDGE_QA_DEBUG_MODEL_OUTPUT", "true")
	cfg, err = LoadResourceRuntimeFromEnv()
	require.NoError(t, err)
	require.True(t, cfg.KnowledgeQA.DebugModelOutput)
}

func TestKnowledgeQAWebSearchConfig(t *testing.T) {
	t.Setenv("MESHX_AGS_DATABASE_DSN", "file:ags.db")
	t.Setenv("MESHX_KNOWLEDGE_QA_WEB_SEARCH_PROVIDER", "searxng")

	cfg, err := LoadResourceRuntimeFromEnv()
	require.NoError(t, err)
	require.Equal(t, KnowledgeQAWebSearchProviderSearXNG, cfg.KnowledgeQA.WebSearchProvider)
	require.Equal(t, DefaultKnowledgeQASearXNGBaseURL, cfg.KnowledgeQA.SearXNGBaseURL)

	t.Setenv("MESHX_KNOWLEDGE_QA_SEARXNG_BASE_URL", "https://search.example.test/searxng/")
	cfg, err = LoadResourceRuntimeFromEnv()
	require.NoError(t, err)
	require.Equal(t, "https://search.example.test/searxng", cfg.KnowledgeQA.SearXNGBaseURL)
}

func TestKnowledgeQAWebSearchConfigInfersSearXNGProviderFromBaseURL(t *testing.T) {
	t.Setenv("MESHX_AGS_DATABASE_DSN", "file:ags.db")
	t.Setenv("MESHX_KNOWLEDGE_QA_SEARXNG_BASE_URL", "http://localhost:8080")

	cfg, err := LoadResourceRuntimeFromEnv()
	require.NoError(t, err)
	require.Equal(t, KnowledgeQAWebSearchProviderSearXNG, cfg.KnowledgeQA.WebSearchProvider)
	require.Equal(t, "http://localhost:8080", cfg.KnowledgeQA.SearXNGBaseURL)
}

func TestKnowledgeQAWebSearchConfigRejectsInvalidValues(t *testing.T) {
	for _, test := range []struct {
		name     string
		provider string
		baseURL  string
		wantErr  string
	}{
		{
			name: "unknown provider", provider: "tavily",
			wantErr: "MESHX_KNOWLEDGE_QA_WEB_SEARCH_PROVIDER",
		},
		{
			name: "invalid searxng base url", provider: "searxng", baseURL: "not-a-url",
			wantErr: "MESHX_KNOWLEDGE_QA_SEARXNG_BASE_URL",
		},
	} {
		t.Run(test.name, func(t *testing.T) {
			t.Setenv("MESHX_AGS_DATABASE_DSN", "file:ags.db")
			t.Setenv("MESHX_KNOWLEDGE_QA_WEB_SEARCH_PROVIDER", test.provider)
			t.Setenv("MESHX_KNOWLEDGE_QA_SEARXNG_BASE_URL", test.baseURL)

			_, err := LoadResourceRuntimeFromEnv()
			require.ErrorContains(t, err, test.wantErr)
		})
	}
}

func TestLoadUsesAgentTeamRuntimeConfig(t *testing.T) {
	t.Setenv("MESHX_AGS_DATABASE_DSN", "file:ags.db")
	t.Setenv("AGENTTEAM_CORE_STORAGE_DSN", "legacy-agentteam-core-dsn")
	t.Setenv("AGENTTEAM_HTTP_ADDRESS", "127.0.0.1:3001")
	t.Setenv("AGENTTEAM_PUBLIC_BASE_URL", "https://agentteam.example.test/app")
	t.Setenv("AGENTTEAM_DATABASE_DSN", " agentteam-core-dsn ")
	t.Setenv("AGENTTEAM_MAAS_BASE_URL", "https://maas.example.test")

	cfg, err := LoadResourceRuntimeFromEnv()
	require.NoError(t, err)
	require.Equal(t, "127.0.0.1:3001", cfg.HTTPAddress)
	require.Equal(t, "https://agentteam.example.test/app", cfg.PublicBaseURL)
	require.Equal(t, "https://agentteam.example.test/app", cfg.AGS.BaseURL)
	require.Equal(t, "agentteam-core-dsn", cfg.AgentTeamCore.DatabaseDSN)
	require.Equal(t, "https://maas.example.test", cfg.KnowledgeQA.MaaSBaseURL)
}

func TestLoadUsesLegacyAgentTeamDatabaseDSN(t *testing.T) {
	t.Setenv("MESHX_AGS_DATABASE_DSN", "file:ags.db")
	t.Setenv("AGENTTEAM_DATABASE_DSN", "")
	t.Setenv("AGENTTEAM_CORE_STORAGE_DSN", " legacy-agentteam-core-dsn ")

	cfg, err := LoadResourceRuntimeFromEnv()
	require.NoError(t, err)
	require.Equal(t, "legacy-agentteam-core-dsn", cfg.AgentTeamCore.DatabaseDSN)
}

func TestLoadUsesAgentTeamRuntimeDefaultsWhenEnvIsUnset(t *testing.T) {
	for _, key := range []string{"AGENTTEAM_HTTP_ADDRESS", "AGENTTEAM_PUBLIC_BASE_URL"} {
		value, existed := os.LookupEnv(key)
		require.NoError(t, os.Unsetenv(key))
		t.Cleanup(func() {
			if existed {
				require.NoError(t, os.Setenv(key, value))
				return
			}
			require.NoError(t, os.Unsetenv(key))
		})
	}
	t.Setenv("MESHX_AGS_DATABASE_DSN", "file:ags.db")

	cfg, err := LoadResourceRuntimeFromEnv()
	require.NoError(t, err)
	require.Equal(t, ":3001", cfg.HTTPAddress)
	require.Equal(t, "http://localhost:3001", cfg.PublicBaseURL)
	require.Equal(t, "http://localhost:3001", cfg.AGS.BaseURL)
}

func TestLoadRejectsInvalidAgentTeamRuntimeConfig(t *testing.T) {
	t.Setenv("MESHX_AGS_DATABASE_DSN", "file:ags.db")
	for _, test := range []struct {
		name    string
		env     string
		value   string
		wantErr string
	}{
		{name: "HTTP address", env: "AGENTTEAM_HTTP_ADDRESS", value: "not-an-address", wantErr: "AGENTTEAM_HTTP_ADDRESS"},
		{name: "public URL", env: "AGENTTEAM_PUBLIC_BASE_URL", value: "not-a-url", wantErr: "AGENTTEAM_PUBLIC_BASE_URL"},
		{name: "MaaS URL", env: "AGENTTEAM_MAAS_BASE_URL", value: "not-a-url", wantErr: "AGENTTEAM_MAAS_BASE_URL"},
	} {
		t.Run(test.name, func(t *testing.T) {
			t.Setenv(test.env, test.value)
			_, err := LoadResourceRuntimeFromEnv()
			require.ErrorContains(t, err, test.wantErr)
		})
	}
}

func TestAutomationRoleConfigNormalizesRoles(t *testing.T) {
	cfg, err := NormalizeResourceRuntime(ResourceRuntimeConfig{
		HTTPAddress: ":0",
		AGS:         AGSConfig{DatabaseDSN: "file:ags.db"},
		Automation:  AutomationConfig{Roles: []string{" API ", "reconciler", "api"}, ReconcilerInterval: time.Second, ReconcilerBatchSize: 7},
	})
	require.NoError(t, err)
	require.Equal(t, []string{AutomationRoleAPI, AutomationRoleReconciler}, cfg.Automation.Roles)
	require.True(t, cfg.Automation.HasRole(AutomationRoleAPI))
	require.True(t, cfg.Automation.HasRole(AutomationRoleReconciler))
	require.False(t, cfg.Automation.HasRole(AutomationRoleRunnerDispatch))
	require.Equal(t, time.Second, cfg.Automation.ReconcilerInterval)
	require.Equal(t, 7, cfg.Automation.ReconcilerBatchSize)
}

func TestAutomationRoleConfigDefaultsAndRejectsUnknownRoles(t *testing.T) {
	cfg, err := NormalizeResourceRuntime(ResourceRuntimeConfig{HTTPAddress: ":0", AGS: AGSConfig{DatabaseDSN: "file:ags.db"}})
	require.NoError(t, err)
	require.True(t, cfg.Automation.HasRole(AutomationRoleAPI))
	require.True(t, cfg.Automation.HasRole(AutomationRoleScheduler))
	require.True(t, cfg.Automation.HasRole(AutomationRoleRunnerDispatch))
	require.True(t, cfg.Automation.HasRole(AutomationRoleEventer))
	require.True(t, cfg.Automation.HasRole(AutomationRoleReconciler))
	require.Equal(t, 30*time.Second, cfg.Automation.ReconcilerInterval)
	require.Equal(t, 100, cfg.Automation.ReconcilerBatchSize)
	require.Equal(t, "server-default", cfg.Automation.ServerRunnerID)
	require.Equal(t, []string{"ubuntu-latest"}, cfg.Automation.ServerRunnerLabels)
	require.Equal(t, 1, cfg.Automation.ServerRunnerConcurrency)
	require.Equal(t, ".data/meshx/automation-runner", cfg.Automation.ServerRunnerWorkDir)
	require.Equal(t, time.Second, cfg.Automation.ServerRunnerPollInterval)
	require.Equal(t, 30*time.Minute, cfg.Automation.ServerRunnerJobTimeout)
	require.Equal(t, AutomationExecutorDocker, cfg.Automation.Executor)
	require.Empty(t, cfg.Automation.DockerImages)
	require.Equal(t, "none", cfg.Automation.DockerNetwork)
	require.Equal(t, float64(1), cfg.Automation.DockerCPUs)
	require.EqualValues(t, 1024*1024*1024, cfg.Automation.DockerMemoryBytes)
	require.EqualValues(t, 256, cfg.Automation.DockerPidsLimit)
	require.Equal(t, defaultAutomationDockerUser(), cfg.Automation.DockerUser)
	require.Empty(t, cfg.Automation.DockerMounts)
	require.EqualValues(t, 512*1024*1024, cfg.Automation.CheckoutMaxBytes)
	require.Equal(t, 100000, cfg.Automation.CheckoutMaxFiles)
	require.Equal(t, 5*time.Minute, cfg.Automation.CheckoutTimeout)
	require.Equal(t, 256*1024, cfg.Automation.MaxAutomationSourceBytes)
	require.Equal(t, 256, cfg.Automation.MaxAutomationJobs)
	require.Equal(t, 200, cfg.Automation.MaxAutomationStepsPerJob)
	require.Equal(t, 64*1024, cfg.Automation.MaxRunInputBytes)
	require.Equal(t, 128*1024, cfg.Automation.MaxJobInputBytes)
	require.Equal(t, 64*1024, cfg.Automation.MaxLogChunkBytes)
	require.EqualValues(t, DefaultMaxArtifactBytes, cfg.Automation.MaxArtifactBytes)
	require.Equal(t, 8, cfg.Automation.MaxRunnerLabels)
	require.Equal(t, 1000, cfg.Automation.MaxActiveRunsPerWorkspace)
	require.Equal(t, 5000, cfg.Automation.WikiPublishMaxPages)
	require.Equal(t, 10000, cfg.Automation.WikiPublishMaxEntries)
	require.EqualValues(t, 2*1024*1024, cfg.Automation.WikiPublishMaxPageBytes)
	require.Equal(t, 10000, cfg.Automation.TreePublishMaxFiles)
	require.Equal(t, 20000, cfg.Automation.TreePublishMaxEntries)
	require.EqualValues(t, 16*1024*1024, cfg.Automation.TreePublishMaxFileBytes)
	require.EqualValues(t, 256*1024*1024, cfg.Automation.TreePublishMaxBytes)
	require.Empty(t, cfg.Automation.ActionsRunnerBaseURL)
	require.Equal(t, AutomationBlobDriverFilesystem, cfg.Automation.BlobDriver)
	require.Equal(t, ".data/meshx/automation-blobs", cfg.Automation.BlobFilesystemRoot)
	require.False(t, cfg.Auth.TrustedJWTClaims)
	require.False(t, cfg.Auth.AllowMock)
	require.Equal(t, DefaultAuthHeader, cfg.Auth.Header)
	require.Equal(t, DefaultPublicBaseURL, cfg.PublicBaseURL)
	require.Equal(t, DefaultPublicBaseURL, cfg.AGS.BaseURL)
	require.Empty(t, cfg.AgentTeamCore.DatabaseDSN)
	require.False(t, cfg.ContentStore.Enabled)
	require.Equal(t, ".data/meshx/content-store", cfg.ContentStore.FilesystemRoot)
	require.Equal(t, DefaultResourceActivityRefreshAfter, cfg.ResourceActivity.RefreshAfter)

	_, err = NormalizeResourceRuntime(ResourceRuntimeConfig{HTTPAddress: ":0", AGS: AGSConfig{DatabaseDSN: "file:ags.db"}, Automation: AutomationConfig{Roles: []string{"api", "unknown"}}})
	require.Error(t, err)
	require.Contains(t, err.Error(), "unknown role")
}

func TestContentStoreEnabledDoesNotRequireDeployableTestBinding(t *testing.T) {
	cfg, err := NormalizeResourceRuntime(ResourceRuntimeConfig{
		HTTPAddress: ":0", AGS: AGSConfig{DatabaseDSN: "file:ags.db"},
		ContentStore: ContentStoreConfig{Enabled: true, FilesystemRoot: "custom-content-root"},
	})
	require.NoError(t, err)
	require.True(t, cfg.ContentStore.Enabled)
	require.Equal(t, "custom-content-root", cfg.ContentStore.FilesystemRoot)
}

func TestResourceActivityRefreshAfterConfig(t *testing.T) {
	cfg, err := NormalizeResourceRuntime(ResourceRuntimeConfig{HTTPAddress: ":0", AGS: AGSConfig{DatabaseDSN: "file:ags.db"}})
	require.NoError(t, err)
	require.Equal(t, 15*time.Minute, cfg.ResourceActivity.RefreshAfter)

	cfg, err = NormalizeResourceRuntime(ResourceRuntimeConfig{
		HTTPAddress: ":0",
		AGS:         AGSConfig{DatabaseDSN: "file:ags.db"},
		ResourceActivity: ResourceActivityConfig{
			RefreshAfter: 30 * time.Minute,
		},
	})
	require.NoError(t, err)
	require.Equal(t, 30*time.Minute, cfg.ResourceActivity.RefreshAfter)

	cfg, err = NormalizeResourceRuntime(ResourceRuntimeConfig{
		HTTPAddress: ":0",
		AGS:         AGSConfig{DatabaseDSN: "file:ags.db"},
		ResourceActivity: ResourceActivityConfig{
			RefreshAfter: -time.Minute,
		},
	})
	require.NoError(t, err)
	require.Equal(t, DefaultResourceActivityRefreshAfter, cfg.ResourceActivity.RefreshAfter)

	t.Setenv("MESHX_AGS_DATABASE_DSN", "file:ags.db")
	t.Setenv("MESHX_RESOURCE_ACTIVITY_REFRESH_AFTER", "30m")
	cfg, err = LoadResourceRuntimeFromEnv()
	require.NoError(t, err)
	require.Equal(t, 30*time.Minute, cfg.ResourceActivity.RefreshAfter)
}

func TestAutomationLimitConfigLoadsEnv(t *testing.T) {
	t.Setenv("MESHX_AGS_DATABASE_DSN", "file:ags.db")
	t.Setenv("MESHX_MAX_AUTOMATION_SOURCE_BYTES", "111")
	t.Setenv("MESHX_MAX_AUTOMATION_JOBS", "12")
	t.Setenv("MESHX_MAX_AUTOMATION_STEPS_PER_JOB", "13")
	t.Setenv("MESHX_MAX_RUN_INPUT_BYTES", "14")
	t.Setenv("MESHX_MAX_JOB_INPUT_BYTES", "15")
	t.Setenv("MESHX_MAX_LOG_CHUNK_BYTES", "16")
	t.Setenv("MESHX_MAX_ARTIFACT_BYTES", "17")
	t.Setenv("MESHX_MAX_RUNNER_LABELS", "3")
	t.Setenv("MESHX_MAX_ACTIVE_RUNS_PER_WORKSPACE", "19")
	t.Setenv("MESHX_AUTOMATION_WIKI_PUBLISH_MAX_PAGES", "3000")
	t.Setenv("MESHX_AUTOMATION_WIKI_PUBLISH_MAX_ENTRIES", "6000")
	t.Setenv("MESHX_AUTOMATION_WIKI_PUBLISH_MAX_PAGE_BYTES", "1048576")
	t.Setenv("MESHX_AUTOMATION_TREE_PUBLISH_MAX_FILES", "7000")
	t.Setenv("MESHX_AUTOMATION_TREE_PUBLISH_MAX_ENTRIES", "9000")
	t.Setenv("MESHX_AUTOMATION_TREE_PUBLISH_MAX_FILE_BYTES", "8388608")
	t.Setenv("MESHX_AUTOMATION_TREE_PUBLISH_MAX_BYTES", "67108864")
	t.Setenv("MESHX_AUTOMATION_ACTIONS_RUNNER_BASE_URL", "https://runner.example.test/meshx/")
	t.Setenv("MESHX_AUTOMATION_BLOB_DRIVER", "filesystem")
	t.Setenv("MESHX_AUTOMATION_BLOB_FILESYSTEM_ROOT", "/tmp/meshx-automation-blobs")
	t.Setenv("MESHX_AUTOMATION_SERVER_RUNNER_ENABLED", "false")
	t.Setenv("MESHX_AUTOMATION_SERVER_RUNNER_ID", "server-ci")
	t.Setenv("MESHX_AUTOMATION_SERVER_RUNNER_LABELS", "ubuntu-latest,self-hosted")
	t.Setenv("MESHX_AUTOMATION_SERVER_RUNNER_CONCURRENCY", "3")
	t.Setenv("MESHX_AUTOMATION_SERVER_RUNNER_WORKDIR", "/tmp/meshx-automation-runner")
	t.Setenv("MESHX_AUTOMATION_SERVER_RUNNER_POLL_INTERVAL", "2s")
	t.Setenv("MESHX_AUTOMATION_SERVER_RUNNER_JOB_TIMEOUT", "5m")
	t.Setenv("MESHX_AUTOMATION_EXECUTOR", "docker")
	t.Setenv("MESHX_AUTOMATION_DOCKER_IMAGES", "Ubuntu-Latest=ubuntu:24.04,node-22=node:22")
	t.Setenv("MESHX_AUTOMATION_DOCKER_NETWORK", "bridge")
	t.Setenv("MESHX_AUTOMATION_DOCKER_CPUS", "2.5")
	t.Setenv("MESHX_AUTOMATION_DOCKER_MEMORY_BYTES", "2048")
	t.Setenv("MESHX_AUTOMATION_DOCKER_PIDS_LIMIT", "32")
	t.Setenv("MESHX_AUTOMATION_DOCKER_USER", "automation")
	t.Setenv("MESHX_AUTOMATION_DOCKER_MOUNTS", "~/tidb-wiki:/mnt/agentteam/tidb-wiki:ro,/var/tmp/tidb.insights:/mnt/agentteam/tidb.insights:rw")
	t.Setenv("MESHX_AUTOMATION_CHECKOUT_MAX_BYTES", "12345")
	t.Setenv("MESHX_AUTOMATION_CHECKOUT_MAX_FILES", "321")
	t.Setenv("MESHX_AUTOMATION_CHECKOUT_TIMEOUT", "7m")
	t.Setenv("MESHX_AUTOMATION_CREDENTIALS_MASTER_KEY", "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=")

	cfg, err := LoadResourceRuntimeFromEnv()
	require.NoError(t, err)
	require.Equal(t, 111, cfg.Automation.MaxAutomationSourceBytes)
	require.Equal(t, 12, cfg.Automation.MaxAutomationJobs)
	require.Equal(t, 13, cfg.Automation.MaxAutomationStepsPerJob)
	require.Equal(t, 14, cfg.Automation.MaxRunInputBytes)
	require.Equal(t, 15, cfg.Automation.MaxJobInputBytes)
	require.Equal(t, 16, cfg.Automation.MaxLogChunkBytes)
	require.EqualValues(t, 17, cfg.Automation.MaxArtifactBytes)
	require.Equal(t, 3, cfg.Automation.MaxRunnerLabels)
	require.Equal(t, 19, cfg.Automation.MaxActiveRunsPerWorkspace)
	require.Equal(t, 3000, cfg.Automation.WikiPublishMaxPages)
	require.Equal(t, 6000, cfg.Automation.WikiPublishMaxEntries)
	require.EqualValues(t, 1048576, cfg.Automation.WikiPublishMaxPageBytes)
	require.Equal(t, 7000, cfg.Automation.TreePublishMaxFiles)
	require.Equal(t, 9000, cfg.Automation.TreePublishMaxEntries)
	require.EqualValues(t, 8388608, cfg.Automation.TreePublishMaxFileBytes)
	require.EqualValues(t, 67108864, cfg.Automation.TreePublishMaxBytes)
	require.Equal(t, "https://runner.example.test/meshx", cfg.Automation.ActionsRunnerBaseURL)
	require.Equal(t, AutomationBlobDriverFilesystem, cfg.Automation.BlobDriver)
	require.Equal(t, "/tmp/meshx-automation-blobs", cfg.Automation.BlobFilesystemRoot)
	require.False(t, cfg.Automation.ServerRunnerEnabled)
	require.Equal(t, "server-ci", cfg.Automation.ServerRunnerID)
	require.Equal(t, []string{"ubuntu-latest", "self-hosted"}, cfg.Automation.ServerRunnerLabels)
	require.Equal(t, 3, cfg.Automation.ServerRunnerConcurrency)
	require.Equal(t, "/tmp/meshx-automation-runner", cfg.Automation.ServerRunnerWorkDir)
	require.Equal(t, 2*time.Second, cfg.Automation.ServerRunnerPollInterval)
	require.Equal(t, 5*time.Minute, cfg.Automation.ServerRunnerJobTimeout)
	require.Equal(t, AutomationExecutorDocker, cfg.Automation.Executor)
	require.Equal(t, map[string]string{"ubuntu-latest": "ubuntu:24.04", "node-22": "node:22"}, cfg.Automation.DockerImages)
	home, err := os.UserHomeDir()
	require.NoError(t, err)
	require.Equal(t, []AutomationDockerMount{
		{Source: filepath.Join(home, "tidb-wiki"), Target: "/mnt/agentteam/tidb-wiki", ReadOnly: true},
		{Source: "/var/tmp/tidb.insights", Target: "/mnt/agentteam/tidb.insights", ReadOnly: false},
	}, cfg.Automation.DockerMounts)
	require.Equal(t, "bridge", cfg.Automation.DockerNetwork)
	require.Equal(t, 2.5, cfg.Automation.DockerCPUs)
	require.EqualValues(t, 2048, cfg.Automation.DockerMemoryBytes)
	require.EqualValues(t, 32, cfg.Automation.DockerPidsLimit)
	require.Equal(t, "automation", cfg.Automation.DockerUser)
	require.EqualValues(t, 12345, cfg.Automation.CheckoutMaxBytes)
	require.Equal(t, 321, cfg.Automation.CheckoutMaxFiles)
	require.Equal(t, 7*time.Minute, cfg.Automation.CheckoutTimeout)
	automationCredentialKey, err := cfg.Automation.CredentialEncryptionKey()
	require.NoError(t, err)
	require.Equal(t, []byte("0123456789abcdef0123456789abcdef"), automationCredentialKey)
	require.False(t, cfg.Auth.TrustedJWTClaims)
	require.True(t, cfg.Auth.AllowMock)
	require.Equal(t, DefaultAuthHeader, cfg.Auth.Header)
}

func TestAutomationCredentialsMasterKeyRequiresBase64Encoded32Bytes(t *testing.T) {
	for _, raw := range []string{"not-base64", "c2hvcnQ="} {
		_, err := NormalizeResourceRuntime(ResourceRuntimeConfig{HTTPAddress: ":0", AGS: AGSConfig{DatabaseDSN: "file:ags.db"}, Automation: AutomationConfig{CredentialsMasterKey: raw}})
		require.Error(t, err, raw)
		require.Contains(t, err.Error(), "MESHX_AUTOMATION_CREDENTIALS_MASTER_KEY")
	}
}

func TestSourceMirrorOutboundProxyLoadsAndNormalizes(t *testing.T) {
	t.Setenv("MESHX_AGS_DATABASE_DSN", "file:ags.db")
	t.Setenv("MESHX_KB_SOURCE_MIRROR_OUTBOUND_PROXY", "127.0.0.1:8888")

	cfg, err := LoadResourceRuntimeFromEnv()
	require.NoError(t, err)
	require.Equal(t, "socks5://127.0.0.1:8888", cfg.KB.SourceMirrorOutboundProxy)
}

func TestSourceMirrorOutboundProxyRejectsInvalidURL(t *testing.T) {
	_, err := NormalizeResourceRuntime(ResourceRuntimeConfig{
		HTTPAddress: ":0",
		AGS:         AGSConfig{DatabaseDSN: "file:ags.db"},
		KB:          KBConfig{SourceMirrorOutboundProxy: "ftp://proxy.example.test"},
	})
	require.Error(t, err)
	require.Contains(t, err.Error(), "MESHX_KB_SOURCE_MIRROR_OUTBOUND_PROXY")
}

func TestKBWikiGenerationConfigIsExplicitAndNormalized(t *testing.T) {
	t.Setenv("MESHX_AGS_DATABASE_DSN", "file:ags.db")
	t.Setenv("MESHX_KB_SOURCE_MIRROR_WORK_DIR", "/var/lib/meshx/mirrors")
	t.Setenv("MESHX_KB_WIKI_GENERATION_ENABLED", "true")
	t.Setenv("MESHX_KB_WIKI_GENERATION_BINARY", " /opt/agentteam-insight ")
	t.Setenv("MESHX_KB_WIKI_GENERATION_WORK_DIR", " /var/lib/meshx/wiki ")
	t.Setenv("MESHX_KB_WIKI_GENERATION_TIMEOUT", "45m")

	cfg, err := LoadResourceRuntimeFromEnv()
	require.NoError(t, err)
	require.True(t, cfg.KB.WikiGenerationEnabled)
	require.Equal(t, "/opt/agentteam-insight", cfg.KB.WikiGenerationBinary)
	require.Equal(t, "/var/lib/meshx/wiki", cfg.KB.WikiGenerationWorkDir)
	require.Equal(t, 45*time.Minute, cfg.KB.WikiGenerationTimeout)

	cfg, err = NormalizeResourceRuntime(ResourceRuntimeConfig{
		HTTPAddress: ":0", AGS: AGSConfig{DatabaseDSN: "file:ags.db"},
		KB: KBConfig{SourceMirrorWorkDir: "/var/lib/meshx/mirrors"},
	})
	require.NoError(t, err)
	require.False(t, cfg.KB.WikiGenerationEnabled)
	require.Equal(t, "agentteam-insight", cfg.KB.WikiGenerationBinary)
	require.Equal(t, "/var/lib/meshx/wiki-generation", cfg.KB.WikiGenerationWorkDir)
	require.Equal(t, 2*time.Hour, cfg.KB.WikiGenerationTimeout)
}

func TestAGSEmbeddingConfigLoadsEnvAndNormalizes(t *testing.T) {
	t.Setenv("MESHX_AGS_DATABASE_DSN", "file:ags.db")
	t.Setenv("EMBEDDING_API_KEY", " test-key ")
	t.Setenv("EMBEDDING_BASE_URL", " https://embedding.example/v1 ")
	t.Setenv("EMBEDDING_MODEL", " text-embedding-3-small ")
	t.Setenv("EMBEDDING_DIMENSIONS", "1536")

	cfg, err := LoadResourceRuntimeFromEnv()
	require.NoError(t, err)
	require.Equal(t, "test-key", cfg.AGS.EmbeddingAPIKey)
	require.Equal(t, "https://embedding.example/v1", cfg.AGS.EmbeddingBaseURL)
	require.Equal(t, "text-embedding-3-small", cfg.AGS.EmbeddingModel)
	require.Equal(t, 1536, cfg.AGS.EmbeddingDimensions)

	_, err = NormalizeResourceRuntime(ResourceRuntimeConfig{
		HTTPAddress: ":0",
		AGS: AGSConfig{
			DatabaseDSN:         "file:ags.db",
			EmbeddingDimensions: -1,
		},
	})
	require.Error(t, err)
	require.Contains(t, err.Error(), "MESHX_AGS_EMBEDDING_DIMENSIONS")

	for _, dimensions := range []int{0, MaxEmbeddingDimensions} {
		_, err = NormalizeResourceRuntime(ResourceRuntimeConfig{HTTPAddress: ":0", AGS: AGSConfig{DatabaseDSN: "file:ags.db", EmbeddingDimensions: dimensions}})
		require.NoError(t, err)
	}
	_, err = NormalizeResourceRuntime(ResourceRuntimeConfig{HTTPAddress: ":0", AGS: AGSConfig{DatabaseDSN: "file:ags.db", EmbeddingDimensions: MaxEmbeddingDimensions + 1}})
	require.ErrorContains(t, err, "MESHX_AGS_EMBEDDING_DIMENSIONS")
}

func TestDefaultAGSBaseURLUsesAgentTeamPublicBaseURL(t *testing.T) {
	cfg, err := NormalizeResourceRuntime(ResourceRuntimeConfig{HTTPAddress: "127.0.0.1:9090", AGS: AGSConfig{DatabaseDSN: "file:ags.db"}})
	require.NoError(t, err)
	require.Equal(t, DefaultPublicBaseURL, cfg.AGS.BaseURL)
}

func TestAutomationBlobConfigRejectsUnknownDriver(t *testing.T) {
	_, err := NormalizeResourceRuntime(ResourceRuntimeConfig{HTTPAddress: ":0", AGS: AGSConfig{DatabaseDSN: "file:ags.db"}, Automation: AutomationConfig{BlobDriver: "unknown"}})
	require.Error(t, err)
	require.Contains(t, err.Error(), "unsupported automation blob driver")
}

func TestAutomationActionsRunnerBaseURLRequiresAbsoluteHTTPURL(t *testing.T) {
	for _, raw := range []string{"runner.example.test", "ssh://runner.example.test", "https://user:pass@runner.example.test", "https://runner.example.test/path?x=1", "https://runner.example.test/path#frag"} {
		_, err := NormalizeResourceRuntime(ResourceRuntimeConfig{HTTPAddress: ":0", AGS: AGSConfig{DatabaseDSN: "file:ags.db"}, Automation: AutomationConfig{ActionsRunnerBaseURL: raw}})
		require.Error(t, err, raw)
		require.Contains(t, err.Error(), "MESHX_AUTOMATION_ACTIONS_RUNNER_BASE_URL")
	}
}

func TestAgentTeamPublicBaseURLRequiresAbsoluteHTTPURL(t *testing.T) {
	for _, raw := range []string{"meshx.example.test", "ssh://meshx.example.test", "https://user:pass@meshx.example.test", "https://meshx.example.test/path?x=1", "https://meshx.example.test/path#frag"} {
		_, err := NormalizeResourceRuntime(ResourceRuntimeConfig{HTTPAddress: ":0", PublicBaseURL: raw, AGS: AGSConfig{DatabaseDSN: "file:ags.db"}})
		require.Error(t, err, raw)
		require.Contains(t, err.Error(), "AGENTTEAM_PUBLIC_BASE_URL")
	}
}

func TestTrustedJWTClaimsRequiresProtectedListener(t *testing.T) {
	_, err := NormalizeResourceRuntime(ResourceRuntimeConfig{
		HTTPAddress: ":0",
		Auth:        ResourceRuntimeAuthConfig{Enabled: true, TrustedJWTClaims: true},
		AGS:         AGSConfig{DatabaseDSN: "file:ags.db"},
	})
	require.Error(t, err)
	require.Contains(t, err.Error(), "MESHX_AUTH_TRUSTED_JWT_CLAIMS_PROTECTED_LISTENER")
}

func TestAutomationDockerExecutorConfigRejectsInvalidValues(t *testing.T) {
	_, err := NormalizeResourceRuntime(ResourceRuntimeConfig{HTTPAddress: ":0", AGS: AGSConfig{DatabaseDSN: "file:ags.db"}, Automation: AutomationConfig{Executor: "local-shell"}})
	require.Error(t, err)
	require.Contains(t, err.Error(), "unsupported automation executor")

	_, err = NormalizeResourceRuntime(ResourceRuntimeConfig{HTTPAddress: ":0", AGS: AGSConfig{DatabaseDSN: "file:ags.db"}, Automation: AutomationConfig{DockerImages: map[string]string{" ": "ubuntu:24.04"}}})
	require.Error(t, err)
	require.Contains(t, err.Error(), "labels")

	_, err = NormalizeResourceRuntime(ResourceRuntimeConfig{HTTPAddress: ":0", AGS: AGSConfig{DatabaseDSN: "file:ags.db"}, Automation: AutomationConfig{DockerImages: map[string]string{"ubuntu-latest": ""}}})
	require.Error(t, err)
	require.Contains(t, err.Error(), "image")

	_, err = NormalizeResourceRuntime(ResourceRuntimeConfig{HTTPAddress: ":0", AGS: AGSConfig{DatabaseDSN: "file:ags.db"}, Automation: AutomationConfig{DockerNetwork: "container:other"}})
	require.Error(t, err)
	require.Contains(t, err.Error(), "must not join another container")

	_, err = NormalizeResourceRuntime(ResourceRuntimeConfig{HTTPAddress: ":0", AGS: AGSConfig{DatabaseDSN: "file:ags.db"}, Automation: AutomationConfig{DockerMounts: []AutomationDockerMount{{Source: "relative", Target: "/mnt/source", ReadOnly: true}}}})
	require.Error(t, err)
	require.Contains(t, err.Error(), "must be absolute")

	_, err = NormalizeResourceRuntime(ResourceRuntimeConfig{HTTPAddress: ":0", AGS: AGSConfig{DatabaseDSN: "file:ags.db"}, Automation: AutomationConfig{DockerMounts: []AutomationDockerMount{{Source: "/tmp/source", Target: "/workspace/source", ReadOnly: true}}}})
	require.Error(t, err)
	require.Contains(t, err.Error(), "must not be /workspace")
}

func TestAutomationDockerImagesEnvRejectsInvalidEntries(t *testing.T) {
	t.Setenv("MESHX_AGS_DATABASE_DSN", "file:ags.db")
	t.Setenv("MESHX_AUTOMATION_DOCKER_IMAGES", "ubuntu-latest")

	_, err := LoadResourceRuntimeFromEnv()
	require.Error(t, err)
	require.Contains(t, err.Error(), "MESHX_AUTOMATION_DOCKER_IMAGES")
}

func TestTrustedJWTClaimsDefaultsVerifiedHeader(t *testing.T) {
	cfg, err := NormalizeResourceRuntime(ResourceRuntimeConfig{
		HTTPAddress: ":0",
		Auth:        ResourceRuntimeAuthConfig{Enabled: true, TrustedJWTClaims: true, TrustedJWTClaimsProtectedListener: true},
		AGS:         AGSConfig{DatabaseDSN: "file:ags.db"},
	})
	require.NoError(t, err)
	require.Equal(t, DefaultTrustedJWTHeader, cfg.Auth.Header)
	require.False(t, cfg.Auth.AllowMock)
	require.True(t, cfg.Auth.TrustedJWTClaimsProtectedListener)

	_, err = NormalizeResourceRuntime(ResourceRuntimeConfig{
		HTTPAddress: ":0",
		Auth:        ResourceRuntimeAuthConfig{Enabled: true, Header: "Authorization", TrustedJWTClaims: true, TrustedJWTClaimsProtectedListener: true},
		AGS:         AGSConfig{DatabaseDSN: "file:ags.db"},
	})
	require.Error(t, err)
	require.Contains(t, err.Error(), "MESHX_AUTH_JWT_HEADER")

	_, err = NormalizeResourceRuntime(ResourceRuntimeConfig{
		HTTPAddress: ":0",
		Auth:        ResourceRuntimeAuthConfig{Enabled: true, Header: " Authorization ", TrustedJWTClaims: true, TrustedJWTClaimsProtectedListener: true},
		AGS:         AGSConfig{DatabaseDSN: "file:ags.db"},
	})
	require.Error(t, err)
	require.Contains(t, err.Error(), "MESHX_AUTH_JWT_HEADER")
}

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

	for _, header := range []string{"", DefaultTrustedJWTHeader, "x-agentteam-verified-jwt", "  X-AgentTeam-Verified-JWT  "} {
		normalized, err := NormalizeSplitTrustedJWTHeader(header)
		require.NoError(t, err)
		require.Equal(t, DefaultTrustedJWTHeader, normalized)
	}

	_, err := NormalizeSplitTrustedJWTHeader("X-Client-Controlled-Identity")
	require.EqualError(t, err, `split deployment MESHX_AUTH_JWT_HEADER must be "X-AgentTeam-Verified-JWT"`)
}

func TestTrustedJWTClaimsCanBeExplicitlyDisabledByEnv(t *testing.T) {
	t.Setenv("MESHX_AGS_DATABASE_DSN", "file:ags.db")
	t.Setenv("MESHX_AUTH_TRUSTED_JWT_CLAIMS", "false")

	cfg, err := LoadResourceRuntimeFromEnv()
	require.NoError(t, err)
	require.False(t, cfg.Auth.TrustedJWTClaims)
	require.Equal(t, DefaultAuthHeader, cfg.Auth.Header)
	require.True(t, cfg.Auth.AllowMock)
}

func TestTrustedJWTClaimsEnabledByEnvRequiresProtectedListener(t *testing.T) {
	t.Setenv("MESHX_AGS_DATABASE_DSN", "file:ags.db")
	t.Setenv("MESHX_AUTH_TRUSTED_JWT_CLAIMS", "true")

	_, err := LoadResourceRuntimeFromEnv()
	require.Error(t, err)
	require.Contains(t, err.Error(), "MESHX_AUTH_TRUSTED_JWT_CLAIMS_PROTECTED_LISTENER")
}

func TestTrustedJWTClaimsCanBeExplicitlyEnabledByEnv(t *testing.T) {
	t.Setenv("MESHX_AGS_DATABASE_DSN", "file:ags.db")
	t.Setenv("MESHX_AUTH_TRUSTED_JWT_CLAIMS", "true")
	t.Setenv("MESHX_AUTH_TRUSTED_JWT_CLAIMS_PROTECTED_LISTENER", "true")

	cfg, err := LoadResourceRuntimeFromEnv()
	require.NoError(t, err)
	require.True(t, cfg.Auth.TrustedJWTClaims)
	require.True(t, cfg.Auth.TrustedJWTClaimsProtectedListener)
	require.Equal(t, DefaultTrustedJWTHeader, cfg.Auth.Header)
	require.False(t, cfg.Auth.AllowMock)
}
