package config

import (
	"encoding/base64"
	"fmt"
	"reflect"
	"strings"
	"testing"
	"time"
)

func tenantTestKey(fill byte) string {
	return base64.StdEncoding.EncodeToString([]byte(strings.Repeat(string([]byte{fill}), 32)))
}

func TestNormalizeMaterializesLegacyConfigAsExplicitTenant(t *testing.T) {
	cfg, err := Normalize(Config{
		TenantID:            "ClawMem",
		BaseURL:             "https://git.clawmem.ai/",
		TenantDomainAliases: []string{"https://api.git.clawmem.ai/"},
		DBdsn:               "root@tcp(db.example:4000)/clawmem",
		GitRepoDir:          t.TempDir(),
		ConsoleBaseURL:      "https://clawmem.ai",
		Environment:         "production",
	})
	if err != nil {
		t.Fatalf("Normalize: %v", err)
	}
	if len(cfg.Tenants) != 1 {
		t.Fatalf("tenants = %#v", cfg.Tenants)
	}
	got := cfg.Tenants[0]
	if got.ID != "clawmem" || got.BaseURL != "https://git.clawmem.ai" || got.DBdsn != "root@tcp(db.example:4000)/clawmem" {
		t.Fatalf("legacy tenant = %#v", got)
	}
	if fields := configuredLegacyTenantFields(cfg); len(fields) != 0 {
		t.Fatalf("normalized legacy config retained flat fields: %v", fields)
	}
	second, err := Normalize(cfg)
	if err != nil {
		t.Fatalf("second Normalize: %v", err)
	}
	if !reflect.DeepEqual(second, cfg) {
		t.Fatalf("legacy normalization is not idempotent:\nfirst:  %#v\nsecond: %#v", cfg, second)
	}
	if len(got.DomainAliases) != 1 || got.DomainAliases[0] != "https://api.git.clawmem.ai" {
		t.Fatalf("legacy tenant domain aliases = %#v", got.DomainAliases)
	}
	if got.Workflow.Timeout != 2*time.Minute || got.Workflow.Image != "bash:5.2" {
		t.Fatalf("legacy tenant workflow defaults = %#v", got.Workflow)
	}
}

func TestNormalizeRepositorySyncDefaultsAndRequiredCredentials(t *testing.T) {
	value := TenantRepositorySyncConfig{Enabled: true, GitHubAppID: 42, GitHubPrivateKey: "test-private-key"}
	if err := normalizeTenantRepositorySync(&value); err != nil {
		t.Fatal(err)
	}
	if value.GitHubAPIURL != "https://api.github.com" || value.PollInterval != 5*time.Second || value.LeaseTTL != 2*time.Minute || value.FullAuditInterval != 7*24*time.Hour || value.MaxConcurrency != 8 || value.MaxAssetBytes != 2<<30 {
		t.Fatalf("repository sync defaults = %#v", value)
	}
	missing := TenantRepositorySyncConfig{Enabled: true}
	if err := normalizeTenantRepositorySync(&missing); err == nil {
		t.Fatal("enabled repository sync without GitHub App credentials was accepted")
	}
}

func TestStructuredTenantsResolveIndependentSecretsAndProviders(t *testing.T) {
	t.Setenv("CLAWMEM_DB_DSN", "clawmem@tcp(db.example:4000)/clawmem")
	t.Setenv("ENGINEERING_DB_DSN", "engineering@tcp(db.example:4000)/engineering")
	t.Setenv("CLAWMEM_SECRET_KEY", tenantTestKey('a'))
	t.Setenv("ENGINEERING_SECRET_KEY", tenantTestKey('b'))
	t.Setenv("ENGINEERING_EMBEDDING_KEY", "engineering-embedding-key")

	raw := `[
		{
			"id": "clawmem",
			"base_url": "https://git.clawmem.ai",
			"db_dsn_env": "CLAWMEM_DB_DSN",
			"git_repo_dir": "` + t.TempDir() + `",
			"secret_encryption_key_env": "CLAWMEM_SECRET_KEY",
			"browser": {
				"console_base_url": "https://clawmem.ai",
				"cors_allowed_origins": ["https://clawmem.ai"]
			},
			"embedding": {},
			"oidc": {},
			"connected_login": {},
			"workflow": {}
		},
		{
			"id": "engineering",
			"base_url": "https://git.ags.ai",
			"domain_aliases": ["https://code.ags.ai"],
			"db_dsn_env": "ENGINEERING_DB_DSN",
			"git_repo_dir": "` + t.TempDir() + `",
			"secret_encryption_key_env": "ENGINEERING_SECRET_KEY",
			"browser": {"cors_allowed_origins": []},
			"embedding": {"api_key_env": "ENGINEERING_EMBEDDING_KEY", "model": "tenant-model"},
			"oidc": {},
			"connected_login": {},
			"workflow": {"timeout": "3m"}
		}
	]`
	tenants, err := parseTenantConfigJSON(raw)
	if err != nil {
		t.Fatalf("parseTenantConfigJSON: %v", err)
	}
	cfg, err := Normalize(Config{
		Environment: "production",
		Tenants:     tenants,
	})
	if err != nil {
		t.Fatalf("Normalize: %v", err)
	}
	if len(cfg.Tenants) != 2 {
		t.Fatalf("tenants = %#v", cfg.Tenants)
	}
	engineering := cfg.Tenants[1]
	if engineering.DBdsn != "engineering@tcp(db.example:4000)/engineering" {
		t.Fatalf("engineering DB DSN = %q", engineering.DBdsn)
	}
	if engineering.Embedding.APIKey != "engineering-embedding-key" || engineering.Embedding.Model != "tenant-model" {
		t.Fatalf("engineering embedding = %#v", engineering.Embedding)
	}
	if engineering.Workflow.Timeout != 3*time.Minute {
		t.Fatalf("engineering workflow timeout = %s", engineering.Workflow.Timeout)
	}
	if len(engineering.Browser.CORSAllowedOrigins) != 0 {
		t.Fatalf("explicit empty CORS policy = %#v", engineering.Browser.CORSAllowedOrigins)
	}

}

func TestNormalizeStructuredConfigIsCanonicalAndIdempotent(t *testing.T) {
	first, err := Normalize(Config{
		Environment: "production",
		Tenants: []TenantConfig{{
			ID:         "ags",
			BaseURL:    "https://git.ags.example/",
			DBdsn:      "root@tcp(db.example:4000)/ags",
			GitRepoDir: t.TempDir(),
		}},
	})
	if err != nil {
		t.Fatalf("first Normalize: %v", err)
	}
	if fields := configuredLegacyTenantFields(first); len(fields) != 0 {
		t.Fatalf("normalized structured config retained legacy fields: %v", fields)
	}

	second, err := Normalize(first)
	if err != nil {
		t.Fatalf("second Normalize: %v", err)
	}
	if !reflect.DeepEqual(second, first) {
		t.Fatalf("Normalize is not idempotent:\nfirst:  %#v\nsecond: %#v", first, second)
	}
}

func TestStructuredTenantsDoNotInheritLegacyResources(t *testing.T) {
	_, err := Normalize(Config{
		Environment: "production",
		DBdsn:       "global@tcp(db.example:4000)/global",
		Tenants: []TenantConfig{{
			ID:         "engineering",
			BaseURL:    "https://git.ags.ai",
			GitRepoDir: t.TempDir(),
		}},
	})
	if err == nil || !strings.Contains(err.Error(), "legacy tenant fields") {
		t.Fatalf("Normalize error = %v, want legacy-field conflict", err)
	}

	_, err = Normalize(Config{
		Environment: "production",
		Tenants: []TenantConfig{{
			ID:         "engineering",
			BaseURL:    "https://git.ags.ai",
			GitRepoDir: t.TempDir(),
		}},
	})
	if err == nil || !strings.Contains(err.Error(), "requires a database DSN") {
		t.Fatalf("Normalize error = %v, want missing tenant DB", err)
	}

	cfg := Config{
		Environment:         "production",
		TenantDomainAliases: []string{"https://api.git.ags.ai"},
		Tenants: []TenantConfig{{
			ID:         "engineering",
			BaseURL:    "https://git.ags.ai",
			DBdsn:      "engineering@tcp(db.example:4000)/engineering",
			GitRepoDir: t.TempDir(),
		}},
	}
	_, err = Normalize(cfg)
	if err == nil || !strings.Contains(err.Error(), "legacy tenant fields") {
		t.Fatalf("Normalize error = %v, want domain-alias conflict", err)
	}
}

func TestLegacyTenantEnvironmentConflictsWithStructuredTenants(t *testing.T) {
	for _, name := range []string{"TENANT_DOMAIN_ALIASES"} {
		t.Run(name, func(t *testing.T) {
			t.Setenv(name, "configured")
			found := false
			for _, configured := range configuredLegacyTenantEnvironment() {
				if configured == name {
					found = true
					break
				}
			}
			if !found {
				t.Fatalf("%s was not classified as legacy tenant configuration", name)
			}
		})
	}
}

func TestNormalizeRejectsTenantResourceAndDomainCollisions(t *testing.T) {
	base := []TenantConfig{
		{
			ID:                  "clawmem",
			BaseURL:             "https://git.clawmem.ai",
			DBdsn:               "clawmem@tcp(db.example:4000)/clawmem",
			GitRepoDir:          t.TempDir(),
			SecretEncryptionKey: tenantTestKey('a'),
		},
		{
			ID:                  "engineering",
			BaseURL:             "https://git.ags.ai",
			DBdsn:               "engineering@tcp(db.example:4000)/engineering",
			GitRepoDir:          t.TempDir(),
			SecretEncryptionKey: tenantTestKey('b'),
		},
	}

	tests := []struct {
		name   string
		mutate func([]TenantConfig)
		want   string
	}{
		{
			name: "domain alias",
			mutate: func(values []TenantConfig) {
				values[1].DomainAliases = []string{"https://git.clawmem.ai"}
			},
			want: "tenant host",
		},
		{
			name: "browser cookie hostname across ports",
			mutate: func(values []TenantConfig) {
				values[0].BaseURL = "https://shared.example.test:8443"
				values[1].BaseURL = "https://shared.example.test:9443"
			},
			want: "tenant cookie host",
		},
		{
			name: "database",
			mutate: func(values []TenantConfig) {
				values[1].DBdsn = values[0].DBdsn
			},
			want: "same DB DSN",
		},
		{
			name: "Git root",
			mutate: func(values []TenantConfig) {
				values[1].GitRepoDir = values[0].GitRepoDir + "/nested"
			},
			want: "Git roots",
		},
		{
			name: "secret key",
			mutate: func(values []TenantConfig) {
				values[1].SecretEncryptionKey = values[0].SecretEncryptionKey
			},
			want: "share a secret encryption key",
		},
		{
			name: "development admin token",
			mutate: func(values []TenantConfig) {
				values[0].AdminToken = "shared-admin-token"
				values[1].AdminToken = "shared-admin-token"
			},
			want: "share a development admin token",
		},
		{
			name: "connected-login client secret",
			mutate: func(values []TenantConfig) {
				for index := range values {
					values[index].ConnectedLogin = TenantConnectedLoginConfig{
						Enabled:      true,
						Origin:       "https://login.example",
						APIOrigin:    "https://login-api.example",
						ClientID:     fmt.Sprintf("connected-client-%d", index),
						ClientSecret: "shared-connected-secret",
					}
				}
			},
			want: "share a connected-login client secret",
		},
		{
			name: "OIDC client secret",
			mutate: func(values []TenantConfig) {
				for index := range values {
					values[index].OIDC = TenantOIDCConfig{
						Enabled:      true,
						Issuer:       "https://oidc.example",
						ClientID:     fmt.Sprintf("oidc-client-%d", index),
						ClientSecret: "shared-oidc-secret",
					}
				}
			},
			want: "share an OIDC client secret",
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			values := append([]TenantConfig(nil), base...)
			tc.mutate(values)
			_, err := Normalize(Config{Environment: "production", Tenants: values})
			if err == nil || !strings.Contains(err.Error(), tc.want) {
				t.Fatalf("Normalize error = %v, want %q", err, tc.want)
			}
		})
	}
}

func TestNormalizeTenantConfigsAllowsSharedProviderAndDatabaseCredentials(t *testing.T) {
	values := []TenantConfig{
		{
			ID:                  "tenant-a",
			BaseURL:             "https://tenant-a.example.test",
			DBdsn:               "shared:password@tcp(db.example:4000)/tenant_a",
			GitRepoDir:          t.TempDir(),
			SecretEncryptionKey: tenantTestKey('a'),
			Embedding: TenantEmbeddingConfig{
				APIKey: "shared-embedding-key",
			},
		},
		{
			ID:                  "tenant-b",
			BaseURL:             "https://tenant-b.example.test",
			DBdsn:               "shared:password@tcp(db.example:4000)/tenant_b",
			GitRepoDir:          t.TempDir(),
			SecretEncryptionKey: tenantTestKey('b'),
			Embedding: TenantEmbeddingConfig{
				APIKey: "shared-embedding-key",
			},
		},
	}

	normalized, err := Normalize(Config{Environment: "production", Tenants: values})
	if err != nil {
		t.Fatalf("Normalize: %v", err)
	}
	if normalized.Tenants[0].Embedding.APIKey != normalized.Tenants[1].Embedding.APIKey {
		t.Fatal("shared embedding API key was not preserved")
	}
}

func TestTenantConfigJSONRejectsUnknownAndAmbiguousSecretFields(t *testing.T) {
	_, err := parseTenantConfigJSON(`[{"id":"engineering","base_url":"https://git.ags.ai","unknown":true}]`)
	if err == nil || !strings.Contains(err.Error(), "unknown field") {
		t.Fatalf("unknown field error = %v", err)
	}

	t.Setenv("DB_DSN_REF", "root@tcp(db.example:4000)/engineering")
	_, err = parseTenantConfigJSON(`[{
		"id":"engineering",
		"base_url":"https://git.ags.ai",
		"db_dsn":"direct",
		"db_dsn_env":"DB_DSN_REF"
	}]`)
	if err == nil || !strings.Contains(err.Error(), "cannot both be set") {
		t.Fatalf("ambiguous secret error = %v", err)
	}
}

func TestStructuredTenantEnabledFlagFailsClosed(t *testing.T) {
	values, err := parseTenantConfigJSON(`[
		{
			"id": "active",
			"base_url": "https://active.example.test",
			"db_dsn": "active@tcp(db.example:4000)/active",
			"git_repo_dir": "/tmp/active",
			"secret_encryption_key": "` + tenantTestKey('a') + `",
			"browser": {},
			"embedding": {},
			"oidc": {},
			"connected_login": {},
			"workflow": {}
		},
		{
			"id": "disabled",
			"enabled": false,
			"base_url": "https://disabled.example.test",
			"domain_aliases": ["https://disabled-alias.example.test"],
			"db_dsn_env": "DISABLED_DB_DSN_MUST_NOT_BE_READ",
			"git_repo_dir": "/unmounted/disabled",
			"secret_encryption_key_env": "DISABLED_SECRET_MUST_NOT_BE_READ",
			"browser": {},
			"embedding": {},
			"oidc": {},
			"connected_login": {},
			"workflow": {"timeout": "not-a-duration"}
		}
	]`)
	if err != nil {
		t.Fatalf("parseTenantConfigJSON: %v", err)
	}
	if values[0].Disabled || !values[1].Disabled {
		t.Fatalf("enabled flags parsed as disabled states: %#v", values)
	}
	if values[1].DBdsn != "" || values[1].GitRepoDir != "" || values[1].SecretEncryptionKey != "" || values[1].Workflow.Timeout != 0 {
		t.Fatalf("disabled tenant retained resource configuration: %#v", values[1])
	}
	if len(values[1].DomainAliases) != 1 || values[1].DomainAliases[0] != "https://disabled-alias.example.test" {
		t.Fatalf("disabled tenant domain reservation = %#v", values[1].DomainAliases)
	}

	normalized, err := Normalize(Config{Environment: "production", Tenants: values})
	if err != nil {
		t.Fatalf("Normalize with inert disabled tenant: %v", err)
	}
	if got := normalized.Tenants[1]; got.DBdsn != "" || got.GitRepoDir != "" || got.SecretEncryptionKey != "" {
		t.Fatalf("normalized disabled tenant retained resources: %#v", got)
	}
	collision := append([]TenantConfig(nil), values...)
	collision[0].DomainAliases = []string{"https://disabled-alias.example.test"}
	if _, err := Normalize(Config{Environment: "production", Tenants: collision}); err == nil || !strings.Contains(err.Error(), "tenant host") {
		t.Fatalf("disabled tenant domain was not reserved: %v", err)
	}

	values[0].Disabled = true
	if _, err := Normalize(Config{Environment: "production", Tenants: values}); err == nil ||
		!strings.Contains(err.Error(), "at least one tenant must be enabled") {
		t.Fatalf("all-disabled Normalize error = %v", err)
	}
}
