package server

import (
	"bytes"
	"context"
	"encoding/base64"
	"encoding/json"
	"errors"
	"net/http"
	"net/http/httptest"
	"net/url"
	"path/filepath"
	"sync"
	"testing"
	"time"

	"github.com/go-chi/chi/v5"
	"gorm.io/gorm"

	agsauth "github.com/pingcap/agent-git-service/auth"
	"github.com/pingcap/agent-git-service/config"
	"github.com/pingcap/agent-git-service/internal/db"
	"github.com/pingcap/agent-git-service/internal/gitstore"
	"github.com/pingcap/agent-git-service/internal/rest"
	"github.com/pingcap/agent-git-service/internal/router"
	"github.com/pingcap/agent-git-service/internal/service"
	"github.com/pingcap/agent-git-service/internal/tenant"
)

type headerAuthenticator struct {
	header   string
	identity agsauth.Identity
}

func (a headerAuthenticator) Authenticate(r *http.Request) (agsauth.Identity, bool, error) {
	value := r.Header.Get(a.header)
	if value == "" {
		return agsauth.Identity{}, false, nil
	}
	if value == "bad" {
		return agsauth.Identity{}, false, errors.New("bad embedded identity")
	}
	return a.identity, true, nil
}

func serverTenantContext(t *testing.T, srv *Server, id tenant.ID) (context.Context, *service.TenantResources) {
	t.Helper()
	resources, err := srv.deps.Registry.ForTenant(id)
	if err != nil {
		t.Fatalf("tenant %q resources: %v", id, err)
	}
	return tenant.WithInfo(context.Background(), resources.Info), resources
}

func setRequestHost(req *http.Request, host string) {
	req.Host = host
}

func cleanupTestServer(t *testing.T, srv *Server) {
	t.Helper()
	t.Cleanup(func() {
		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()
		if err := srv.Shutdown(ctx); err != nil {
			t.Errorf("Shutdown: %v", err)
		}
	})
}

func TestInitTenantServiceDeps_EnablesGenericOIDC(t *testing.T) {
	fixture := newTenantServiceTestFixture(t, config.Config{
		TenantID:              "test",
		BaseURL:               "http://localhost:8080",
		DBdsn:                 createBootstrapDSN(t, "server_oidc", "test"),
		GitRepoDir:            t.TempDir(),
		ListenMode:            "production",
		Environment:           "production",
		OIDCProvider:          "casdoor",
		OIDCIssuer:            "http://localhost:8891/",
		OIDCClientID:          "oidc-client-id",
		OIDCAllowInsecureHTTP: true,
		WorkflowExecImage:     "bash:5.2",
		WorkflowExecTimeout:   2 * time.Minute,
		WorkflowExecCPUs:      "1.0",
		WorkflowExecMemory:    "256m",
		WorkflowExecPidsLimit: 128,
		WorkflowExecNoFile:    1024,
		WorkflowExecTmpfsSize: "64m",
	})
	tenantCtx, resources := fixture.tenant(t, "test")
	provider, err := fixture.deps.svc.OIDCForCtx(tenantCtx)
	if err != nil {
		t.Fatalf("resolve tenant OIDC provider: %v", err)
	}
	if provider != resources.OIDC {
		t.Fatal("service did not resolve the registry-owned OIDC provider")
	}
	if got := provider.Provider(); got != "casdoor" {
		t.Fatalf("expected generic OIDC provider casdoor, got %q", got)
	}
}

func TestInitTenantServiceDeps_EnablesConnectedLogin(t *testing.T) {
	fixture := newTenantServiceTestFixture(t, config.Config{
		TenantID:                            "test",
		BaseURL:                             "https://ags.example.com",
		DBdsn:                               createBootstrapDSN(t, "server_connected_login", "test"),
		GitRepoDir:                          t.TempDir(),
		ListenMode:                          "production",
		Environment:                         "production",
		ConnectedLoginProvider:              "provider",
		ConnectedLoginOrigin:                "https://app.provider.example",
		ConnectedLoginAPIOrigin:             "https://api.provider.example",
		ConnectedLoginClientID:              "connected-client",
		ConnectedLoginClientSecret:          "connected-secret",
		ConnectedLoginLoginPath:             "/oauth/login",
		ConnectedLoginSubjectNamespaceClaim: "workspace_id",
		WorkflowExecImage:                   "bash:5.2",
		WorkflowExecTimeout:                 2 * time.Minute,
		WorkflowExecCPUs:                    "1.0",
		WorkflowExecMemory:                  "256m",
		WorkflowExecPidsLimit:               128,
		WorkflowExecNoFile:                  1024,
		WorkflowExecTmpfsSize:               "64m",
	})

	tenantCtx, resources := fixture.tenant(t, "test")
	provider, err := fixture.deps.svc.ConnectedLoginForCtx(tenantCtx)
	if err != nil {
		t.Fatalf("resolve tenant connected-login provider: %v", err)
	}
	if provider != resources.ConnectedLogin {
		t.Fatal("service did not resolve the registry-owned connected-login provider")
	}
	loginURL, err := url.Parse(provider.LoginURL("csrf-state"))
	if err != nil {
		t.Fatalf("parse login URL: %v", err)
	}
	if loginURL.Scheme != "https" || loginURL.Host != "app.provider.example" || loginURL.Path != "/oauth/login" {
		t.Fatalf("unexpected login URL: %s", loginURL.String())
	}
	if got := loginURL.Query().Get("client_id"); got != "connected-client" {
		t.Fatalf("client_id: got %q", got)
	}
	if got := loginURL.Query().Get("return_to"); got != "https://ags.example.com/auth/connected/callback" {
		t.Fatalf("return_to: got %q", got)
	}
	if got := loginURL.Query().Get("state"); got != "csrf-state" {
		t.Fatalf("state: got %q", got)
	}
}

func TestInitTenantServiceDeps_WiresWikiCatalogTransactionDatabase(t *testing.T) {
	fixture := newTenantServiceTestFixture(t, config.Config{
		TenantID:    "test",
		BaseURL:     "http://localhost:8080",
		DBdsn:       createBootstrapDSN(t, "server_wiki_catalog_db", "test"),
		GitRepoDir:  t.TempDir(),
		ListenMode:  "production",
		Environment: "production",
	})
	tenantCtx, resources := fixture.tenant(t, "test")
	if resources.WikiCatalog.DBFor == nil {
		t.Fatal("production wiki catalog has no context database resolver")
	}
	if err := fixture.deps.svc.Transaction(tenantCtx, func(txCtx context.Context, tx *gorm.DB) error {
		resolved := resources.WikiCatalog.DBFor(txCtx)
		if resolved.Error != nil {
			return resolved.Error
		}
		if resolved.Statement.ConnPool != tx.Statement.ConnPool {
			return errors.New("wiki catalog escaped the service transaction")
		}
		return nil
	}); err != nil {
		t.Fatalf("service transaction: %v", err)
	}
}

func TestInitTenantServiceDeps_AllowsSharedProviderAndDatabaseCredentials(t *testing.T) {
	fixture := newTenantServiceTestFixture(t, config.Config{
		Tenants: []config.TenantConfig{
			{
				ID:                  "tenant-a",
				BaseURL:             "https://tenant-a.example.test",
				DBdsn:               createBootstrapDSN(t, "server_shared_credentials_a", "tenant-a"),
				GitRepoDir:          t.TempDir(),
				SecretEncryptionKey: base64.StdEncoding.EncodeToString(bytes.Repeat([]byte{'a'}, 32)),
				Embedding:           config.TenantEmbeddingConfig{APIKey: "shared-embedding-key"},
			},
			{
				ID:                  "tenant-b",
				BaseURL:             "https://tenant-b.example.test",
				DBdsn:               createBootstrapDSN(t, "server_shared_credentials_b", "tenant-b"),
				GitRepoDir:          t.TempDir(),
				SecretEncryptionKey: base64.StdEncoding.EncodeToString(bytes.Repeat([]byte{'b'}, 32)),
				Embedding:           config.TenantEmbeddingConfig{APIKey: "shared-embedding-key"},
			},
		},
		ListenMode:  "production",
		Environment: "production",
	})
	_, resourcesA := fixture.tenant(t, "tenant-a")
	_, resourcesB := fixture.tenant(t, "tenant-b")

	type databaseIdentity struct {
		Principal string `gorm:"column:principal"`
		Database  string `gorm:"column:database_name"`
	}
	readIdentity := func(resources *service.TenantResources) databaseIdentity {
		t.Helper()
		var identity databaseIdentity
		if err := resources.DB.Raw("SELECT CURRENT_USER() AS principal, DATABASE() AS database_name").Scan(&identity).Error; err != nil {
			t.Fatalf("read database identity: %v", err)
		}
		return identity
	}
	identityA := readIdentity(resourcesA)
	identityB := readIdentity(resourcesB)
	if identityA.Principal != identityB.Principal {
		t.Fatalf("database principals differ: %q != %q", identityA.Principal, identityB.Principal)
	}
	if identityA.Database == identityB.Database {
		t.Fatalf("tenant databases unexpectedly match: %q", identityA.Database)
	}
}

func TestInitTenantServiceDeps_RejectsWikiCatalogContextFromAnotherTenant(t *testing.T) {
	fixture := newTenantServiceTestFixture(t, config.Config{
		Tenants: []config.TenantConfig{
			{
				ID:                  "tenant-a",
				BaseURL:             "https://tenant-a.example.test",
				DBdsn:               createBootstrapDSN(t, "server_wiki_catalog_owner_a", "tenant-a"),
				GitRepoDir:          t.TempDir(),
				SecretEncryptionKey: base64.StdEncoding.EncodeToString(bytes.Repeat([]byte{'a'}, 32)),
			},
			{
				ID:                  "tenant-b",
				BaseURL:             "https://tenant-b.example.test",
				DBdsn:               createBootstrapDSN(t, "server_wiki_catalog_owner_b", "tenant-b"),
				GitRepoDir:          t.TempDir(),
				SecretEncryptionKey: base64.StdEncoding.EncodeToString(bytes.Repeat([]byte{'b'}, 32)),
			},
		},
		ListenMode:  "production",
		Environment: "production",
	})
	ctxA, resourcesA := fixture.tenant(t, "tenant-a")
	ctxB, _ := fixture.tenant(t, "tenant-b")

	if resolved := resourcesA.WikiCatalog.DBFor(ctxA); resolved.Error != nil {
		t.Fatalf("same-tenant catalog database: %v", resolved.Error)
	}
	if resolved := resourcesA.WikiCatalog.DBFor(ctxB); !errors.Is(resolved.Error, service.ErrTenantResourceMismatch) {
		t.Fatalf("cross-tenant catalog database error = %v, want %v", resolved.Error, service.ErrTenantResourceMismatch)
	}
}

func TestMain_SignalDrivenShutdown(t *testing.T) {
	setupBootstrapEnv(t, map[string]string{
		"BASE_URL": "http://localhost:0",
		"PORT":     "0",
	})

	sigCh := make(chan struct{}, 1)
	done := make(chan error, 1)
	go func() {
		done <- run(sigCh, shutdownConfig{GracePeriod: 200 * time.Millisecond})
	}()

	sigCh <- struct{}{}

	select {
	case err := <-done:
		if err != nil {
			t.Fatalf("run failed: %v", err)
		}
	case <-time.After(15 * time.Second):
		t.Fatal("timed out waiting for shutdown")
	}
}

// ============================================================================
// Shutdown Tests
// ============================================================================

func TestShutdown_Graceful_Success(t *testing.T) {
	srvCtx, srvCancel := context.WithCancel(context.Background())
	svcDeps, registry, _ := newLifecycleTestService(t, srvCtx)

	testMux := http.NewServeMux()
	testMux.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
	})

	testServer := &http.Server{Addr: ":0", Handler: testMux}

	deps := &bootstrapDeps{
		SrvCtx:    srvCtx,
		SrvCancel: srvCancel,
		SvcDeps:   svcDeps,
		Registry:  registry,
		Servers:   []*http.Server{testServer},
	}

	// Start the server.
	go func() {
		_ = testServer.ListenAndServe()
	}()

	// Give server time to start.
	time.Sleep(100 * time.Millisecond)

	// Shutdown with generous grace period.
	result := shutdown(deps, shutdownConfig{GracePeriod: 5 * time.Second})

	if len(result.HTTPShutdownErrors) > 0 {
		t.Errorf("expected no HTTP shutdown errors, got: %v", result.HTTPShutdownErrors)
	}
	if !result.BgDrained {
		t.Error("expected background goroutines to be drained")
	}
	if !result.ContextCanceled {
		t.Error("expected context to be canceled")
	}
	if !result.ResourcesClosed {
		t.Error("expected tenant resources to close after a complete drain")
	}
	if err := result.Err(); err != nil {
		t.Fatalf("shutdown result: %v", err)
	}
}

func TestShutdown_BackgroundDrain_Timeout(t *testing.T) {
	srvCtx, srvCancel := context.WithCancel(context.Background())
	svcDeps, registry, _ := newLifecycleTestService(t, srvCtx)

	// Simulate a background worker that ignores server cancellation and never
	// finishes within the shutdown grace period.
	releaseWorker := make(chan struct{})
	var releaseOnce sync.Once
	release := func() { releaseOnce.Do(func() { close(releaseWorker) }) }
	t.Cleanup(release)
	svcDeps.Wg.Add(1)
	go func() {
		defer svcDeps.Wg.Done()
		<-releaseWorker
	}()

	testMux := http.NewServeMux()
	testServer := &http.Server{Addr: ":0", Handler: testMux}

	deps := &bootstrapDeps{
		SrvCtx:    srvCtx,
		SrvCancel: srvCancel,
		SvcDeps:   svcDeps,
		Registry:  registry,
		Servers:   []*http.Server{testServer},
	}

	go func() {
		_ = testServer.ListenAndServe()
	}()

	time.Sleep(100 * time.Millisecond)

	// Shutdown with very short grace period to trigger timeout.
	result := shutdown(deps, shutdownConfig{GracePeriod: 100 * time.Millisecond})

	if !result.BgDrainTimedOut {
		t.Error("expected background drain to timeout")
	}
	if !result.ContextCanceled {
		t.Error("expected context to be canceled despite timeout")
	}
	if result.ResourcesClosed {
		t.Fatal("tenant resources closed while a background worker still owned them")
	}
	if err := result.Err(); !errors.Is(err, context.DeadlineExceeded) {
		t.Fatalf("shutdown error = %v, want deadline exceeded", err)
	}

	release()
	retry := shutdown(deps, shutdownConfig{GracePeriod: time.Second})
	if err := retry.Err(); err != nil {
		t.Fatalf("retry shutdown after worker drain: %v", err)
	}
	if !retry.ResourcesClosed {
		t.Fatal("tenant resources stayed open after every owner drained")
	}
}

func TestShutdown_HTTPDrainTimeoutKeepsResourcesOpen(t *testing.T) {
	srvCtx, srvCancel := context.WithCancel(context.Background())
	svcDeps, registry, _ := newLifecycleTestService(t, srvCtx)
	requestStarted := make(chan struct{})
	releaseRequest := make(chan struct{})
	var releaseOnce sync.Once
	release := func() { releaseOnce.Do(func() { close(releaseRequest) }) }

	testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		close(requestStarted)
		<-releaseRequest
		w.WriteHeader(http.StatusNoContent)
	}))
	t.Cleanup(testServer.Close)
	t.Cleanup(release)

	requestDone := make(chan error, 1)
	go func() {
		response, err := http.Get(testServer.URL)
		if err == nil {
			_ = response.Body.Close()
		}
		requestDone <- err
	}()
	<-requestStarted

	deps := &bootstrapDeps{
		SrvCtx:    srvCtx,
		SrvCancel: srvCancel,
		SvcDeps:   svcDeps,
		Registry:  registry,
		Servers:   []*http.Server{testServer.Config},
	}
	result := shutdown(deps, shutdownConfig{GracePeriod: 50 * time.Millisecond})
	if len(result.HTTPShutdownErrors) == 0 {
		t.Fatal("expected the active HTTP request to exceed the shutdown deadline")
	}
	if result.ResourcesClosed {
		t.Fatal("tenant resources closed while an HTTP request still owned them")
	}

	release()
	if err := <-requestDone; err != nil {
		t.Fatalf("active request after release: %v", err)
	}
	retry := shutdown(deps, shutdownConfig{GracePeriod: time.Second})
	if err := retry.Err(); err != nil {
		t.Fatalf("retry shutdown after HTTP drain: %v", err)
	}
	if !retry.ResourcesClosed {
		t.Fatal("tenant resources stayed open after the HTTP request drained")
	}
}

// ============================================================================
// Existing Readyz Tests (unchanged)
// ============================================================================

func TestReadyz_SingleDB_Healthy(t *testing.T) {
	mainDB := openTestDB(t)

	handler := readyzHandler(readyzConfig{
		Databases: map[string]*gorm.DB{"main_db": mainDB},
	})
	req := httptest.NewRequest(http.MethodGet, "/readyz", nil)
	rec := httptest.NewRecorder()
	handler.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d", rec.Code)
	}
	var body map[string]any
	if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
		t.Fatalf("decode body: %v", err)
	}
	if body["status"] != "ready" {
		t.Errorf("expected status=ready, got %v", body["status"])
	}
	checks := body["checks"].(map[string]any)
	if len(checks) != 1 {
		t.Fatalf("expected only main_db check, got %v", checks)
	}
	mainCheck := checks["main_db"].(map[string]any)
	if mainCheck["status"] != "ok" {
		t.Errorf("expected main_db status=ok, got %v", mainCheck["status"])
	}
}

func TestReadyz_MainDBDown_Returns503(t *testing.T) {
	mainDB := openTestDB(t)
	// Close main DB to simulate failure.
	sqlDB, err := mainDB.DB()
	if err != nil {
		t.Fatalf("get sql.DB: %v", err)
	}
	sqlDB.Close()

	handler := readyzHandler(readyzConfig{
		Databases: map[string]*gorm.DB{"main_db": mainDB},
	})
	req := httptest.NewRequest(http.MethodGet, "/readyz", nil)
	rec := httptest.NewRecorder()
	handler.ServeHTTP(rec, req)

	if rec.Code != http.StatusServiceUnavailable {
		t.Fatalf("expected 503, got %d", rec.Code)
	}
	var body map[string]any
	if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
		t.Fatalf("decode body: %v", err)
	}
	if body["status"] != "not_ready" {
		t.Errorf("expected status=not_ready, got %v", body["status"])
	}
}

func TestRouterComposition_ReadyzAfterRegisterRoutes(t *testing.T) {
	mainDB := openTestDB(t)
	fixture := newTenantServiceTestFixture(t, config.Config{
		TenantID:    "test",
		BaseURL:     "http://localhost:8080",
		DBdsn:       createBootstrapDSN(t, "server_router_composition", "test"),
		GitRepoDir:  t.TempDir(),
		ListenMode:  "production",
		Environment: "production",
	})
	restDeps := &rest.Deps{Svc: fixture.deps.svc}

	r := chi.NewRouter()
	mux := router.RegisterRoutes(
		r,
		restDeps,
		fixture.deps.gitHandler,
		fixture.deps.gqlSrv,
		fixture.deps.oauthHandler,
	)
	r.Get("/readyz", readyzHandler(readyzConfig{
		Databases: map[string]*gorm.DB{"main_db": mainDB},
	}))

	req := httptest.NewRequest(http.MethodGet, "/readyz", nil)
	rec := httptest.NewRecorder()
	mux.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
	}
}

func TestNew_HandlerUsesHostAwareMuxAndPerServerTransformState(t *testing.T) {
	makeServer := func(t *testing.T, name, baseURL string) *Server {
		t.Helper()
		root := t.TempDir()
		srv, err := New(config.Config{
			TenantID:    name,
			DBdsn:       createBootstrapDSN(t, "server_"+name, name),
			GitRepoDir:  filepath.Join(root, "repos"),
			BaseURL:     baseURL,
			ListenMode:  "production",
			Environment: "production",
		})
		if err != nil {
			t.Fatalf("New(%s): %v", name, err)
		}
		cleanupTestServer(t, srv)
		return srv
	}

	alpha := makeServer(t, "alpha", "http://alpha.local")
	beta := makeServer(t, "beta", "http://beta.local")

	assertMeta := func(t *testing.T, srv *Server, host, want string) {
		t.Helper()
		req := httptest.NewRequest(http.MethodGet, "http://"+host+"/api/v3", nil)
		setRequestHost(req, host)
		rec := httptest.NewRecorder()
		srv.Handler().ServeHTTP(rec, req)
		if rec.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
		}
		var body map[string]any
		if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
			t.Fatalf("decode meta response: %v", err)
		}
		if got := body["openapi_url"]; got != want {
			t.Fatalf("expected openapi_url %q, got %v", want, got)
		}
	}

	assertMeta(t, alpha, "alpha.local", "http://alpha.local/api/v3/openapi.json")
	assertMeta(t, beta, "beta.local", "http://beta.local/api/v3/openapi.json")
	assertMeta(t, alpha, "alpha.local", "http://alpha.local/api/v3/openapi.json")
}

func TestNew_APIHostAliasRewriteRunsAfterTenantResolution(t *testing.T) {
	root := t.TempDir()
	srv, err := New(config.Config{
		TenantID:            "local",
		TenantDomainAliases: []string{"http://api.github.localhost"},
		DBdsn:               createBootstrapDSN(t, "server_api_host_alias", "local"),
		GitRepoDir:          filepath.Join(root, "repos"),
		BaseURL:             "http://github.localhost",
		ListenMode:          "production",
		Environment:         "production",
	})
	if err != nil {
		t.Fatalf("New: %v", err)
	}
	cleanupTestServer(t, srv)

	req := httptest.NewRequest(http.MethodGet, "http://api.github.localhost/", nil)
	setRequestHost(req, "api.github.localhost")
	rec := httptest.NewRecorder()
	srv.Handler().ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
	}
	var body map[string]any
	if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
		t.Fatalf("decode metadata response: %v", err)
	}
	if got := body["current_user_url"]; got != "http://github.localhost/api/v3/user" {
		t.Fatalf("current_user_url = %v, want canonical tenant URL", got)
	}
}

func TestNew_HandlerUsesDefaultPrefixInResponseURLs(t *testing.T) {
	root := t.TempDir()
	srv, err := New(config.Config{
		TenantID:    "test",
		DBdsn:       createBootstrapDSN(t, "server_rest_prefix", "test"),
		GitRepoDir:  filepath.Join(root, "repos"),
		BaseURL:     "http://embed.local",
		ListenMode:  "production",
		Environment: "production",
	})
	if err != nil {
		t.Fatalf("New: %v", err)
	}
	cleanupTestServer(t, srv)

	tenantCtx, resources := serverTenantContext(t, srv, "test")
	admin := db.User{Login: "admin", Name: "Admin", Type: "User", Status: "active"}
	if err := resources.DB.FirstOrCreate(&admin, db.User{Login: "admin"}).Error; err != nil {
		t.Fatalf("seed admin user: %v", err)
	}
	if _, err := srv.deps.SvcDeps.CreateRepo(tenantCtx, service.CreateRepoInput{
		OwnerLogin: "admin",
		Name:       "prefix-check",
		AutoInit:   true,
	}); err != nil {
		t.Fatalf("CreateRepo: %v", err)
	}

	req := httptest.NewRequest(http.MethodGet, "/api/v3/repos/admin/prefix-check", nil)
	setRequestHost(req, "embed.local")
	rec := httptest.NewRecorder()
	srv.Handler().ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
	}

	var body map[string]any
	if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
		t.Fatalf("decode repo response: %v", err)
	}
	if got := body["issues_url"]; got != "http://embed.local/api/v3/repos/admin/prefix-check/issues{/number}" {
		t.Fatalf("issues_url = %v, want default REST prefix", got)
	}
	if got := body["branches_url"]; got != "http://embed.local/api/v3/repos/admin/prefix-check/branches{/branch}" {
		t.Fatalf("branches_url = %v, want default REST prefix", got)
	}
}

func TestNew_HandlerRequiresGraphQLAuth(t *testing.T) {
	root := t.TempDir()
	srv, err := New(config.Config{
		TenantID:    "test",
		DBdsn:       createBootstrapDSN(t, "server_graphql_auth", "test"),
		GitRepoDir:  filepath.Join(root, "repos"),
		BaseURL:     "http://embed.local",
		ListenMode:  "production",
		Environment: "production",
	})
	if err != nil {
		t.Fatalf("New: %v", err)
	}
	cleanupTestServer(t, srv)

	query, err := json.Marshal(map[string]any{"query": `{ viewer { login } }`})
	if err != nil {
		t.Fatalf("marshal query: %v", err)
	}

	req := httptest.NewRequest(http.MethodPost, "/api/graphql", bytes.NewReader(query))
	setRequestHost(req, "embed.local")
	req.Header.Set("Content-Type", "application/json")
	rec := httptest.NewRecorder()
	srv.Handler().ServeHTTP(rec, req)
	if rec.Code != http.StatusUnauthorized {
		t.Fatalf("expected 401, got %d: %s", rec.Code, rec.Body.String())
	}

	_, resources := serverTenantContext(t, srv, "test")
	admin := db.User{Login: "admin", Name: "Admin", Type: "User", Status: "active"}
	if err := resources.DB.FirstOrCreate(&admin, db.User{Login: "admin"}).Error; err != nil {
		t.Fatalf("seed admin user: %v", err)
	}
	if err := resources.DB.Create(&db.Token{UserID: admin.ID, Value: "embed-token"}).Error; err != nil {
		t.Fatalf("seed token: %v", err)
	}

	authReq := httptest.NewRequest(http.MethodPost, "/api/graphql", bytes.NewReader(query))
	setRequestHost(authReq, "embed.local")
	authReq.Header.Set("Content-Type", "application/json")
	authReq.Header.Set("Authorization", "token embed-token")
	authRec := httptest.NewRecorder()
	srv.Handler().ServeHTTP(authRec, authReq)
	if authRec.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d: %s", authRec.Code, authRec.Body.String())
	}
}

func TestNew_EmbeddedIdentitySupportsRESTGraphQLAndGitHTTP(t *testing.T) {
	root := t.TempDir()
	srv, err := New(config.Config{
		TenantID:    "test",
		DBdsn:       createBootstrapDSN(t, "server_embedded_auth", "test"),
		GitRepoDir:  filepath.Join(root, "repos"),
		BaseURL:     "http://embed.local",
		ListenMode:  "production",
		Environment: "production",
	}, WithAuthenticator(headerAuthenticator{
		header: "X-Embedded-User",
		identity: agsauth.Identity{
			Provider: "meshx",
			Subject:  "subject-1",
			Login:    "gateway-user",
			Name:     "Gateway User",
			Email:    "gateway@example.com",
		},
	}))
	if err != nil {
		t.Fatalf("New: %v", err)
	}
	cleanupTestServer(t, srv)

	restReq := httptest.NewRequest(http.MethodGet, "/api/v3/user", nil)
	setRequestHost(restReq, "embed.local")
	restReq.Header.Set("X-Embedded-User", "ok")
	restRec := httptest.NewRecorder()
	srv.Handler().ServeHTTP(restRec, restReq)
	if restRec.Code != http.StatusOK {
		t.Fatalf("embedded REST auth: expected 200, got %d: %s", restRec.Code, restRec.Body.String())
	}
	var restBody map[string]any
	if err := json.Unmarshal(restRec.Body.Bytes(), &restBody); err != nil {
		t.Fatalf("decode REST body: %v", err)
	}
	if got := restBody["login"]; got != "gateway-user" {
		t.Fatalf("REST login = %v, want gateway-user", got)
	}

	tenantCtx, _ := serverTenantContext(t, srv, "test")
	principal, err := srv.deps.SvcDeps.ResolveEmbeddedPrincipal(tenantCtx, service.EmbeddedIdentity{
		Provider: "meshx",
		Subject:  "subject-1",
		Login:    "gateway-user",
		Name:     "Gateway User",
		Email:    "gateway@example.com",
	})
	if err != nil {
		t.Fatalf("ResolveEmbeddedPrincipal: %v", err)
	}
	user := principal.User()
	authCtx, err := service.ContextWithPrincipal(tenantCtx, principal)
	if err != nil {
		t.Fatalf("ContextWithPrincipal: %v", err)
	}
	if _, err := srv.deps.SvcDeps.CreateRepo(authCtx, service.CreateRepoInput{
		OwnerLogin: user.Login,
		Name:       "embedded-private",
		Private:    true,
		AutoInit:   true,
	}); err != nil {
		t.Fatalf("CreateRepo: %v", err)
	}
	if _, err := srv.deps.SvcDeps.CreateRepo(authCtx, service.CreateRepoInput{
		OwnerLogin: user.Login,
		Name:       "embedded-public",
		Private:    false,
		AutoInit:   true,
	}); err != nil {
		t.Fatalf("CreateRepo public: %v", err)
	}

	query, err := json.Marshal(map[string]any{"query": `{ viewer { login } }`})
	if err != nil {
		t.Fatalf("marshal GraphQL query: %v", err)
	}
	gqlReq := httptest.NewRequest(http.MethodPost, "/api/graphql", bytes.NewReader(query))
	setRequestHost(gqlReq, "embed.local")
	gqlReq.Header.Set("Content-Type", "application/json")
	gqlReq.Header.Set("X-Embedded-User", "ok")
	gqlRec := httptest.NewRecorder()
	srv.Handler().ServeHTTP(gqlRec, gqlReq)
	if gqlRec.Code != http.StatusOK {
		t.Fatalf("embedded GraphQL auth: expected 200, got %d: %s", gqlRec.Code, gqlRec.Body.String())
	}
	if !bytes.Contains(gqlRec.Body.Bytes(), []byte(`"login":"gateway-user"`)) {
		t.Fatalf("embedded GraphQL body missing resolved login: %s", gqlRec.Body.String())
	}

	gitReq := httptest.NewRequest(http.MethodGet, "/gateway-user/embedded-private.git/info/refs?service=git-upload-pack", nil)
	setRequestHost(gitReq, "embed.local")
	gitReq.Header.Set("X-Embedded-User", "ok")
	gitRec := httptest.NewRecorder()
	srv.Handler().ServeHTTP(gitRec, gitReq)
	if gitRec.Code != http.StatusOK {
		t.Fatalf("embedded Git auth: expected 200, got %d: %s", gitRec.Code, gitRec.Body.String())
	}

	createIssueBody, err := json.Marshal(map[string]any{
		"title": "embedded write",
		"body":  "created via embedded identity",
	})
	if err != nil {
		t.Fatalf("marshal create issue body: %v", err)
	}
	writeReq := httptest.NewRequest(http.MethodPost, "/api/v3/repos/gateway-user/embedded-public/issues", bytes.NewReader(createIssueBody))
	setRequestHost(writeReq, "embed.local")
	writeReq.Header.Set("Content-Type", "application/json")
	writeReq.Header.Set("X-Embedded-User", "ok")
	writeRec := httptest.NewRecorder()
	srv.Handler().ServeHTTP(writeRec, writeReq)
	if writeRec.Code != http.StatusCreated {
		t.Fatalf("embedded REST write auth: expected 201, got %d: %s", writeRec.Code, writeRec.Body.String())
	}
	var issueBody map[string]any
	if err := json.Unmarshal(writeRec.Body.Bytes(), &issueBody); err != nil {
		t.Fatalf("decode issue body: %v", err)
	}
	if got := issueBody["title"]; got != "embedded write" {
		t.Fatalf("issue title = %v, want embedded write", got)
	}

	rateReq := httptest.NewRequest(http.MethodGet, "/api/v3/rate_limit", nil)
	setRequestHost(rateReq, "embed.local")
	rateReq.Header.Set("X-Embedded-User", "ok")
	rateRec := httptest.NewRecorder()
	srv.Handler().ServeHTTP(rateRec, rateReq)
	if rateRec.Code != http.StatusOK {
		t.Fatalf("embedded rate_limit auth: expected 200, got %d: %s", rateRec.Code, rateRec.Body.String())
	}
	if got := rateRec.Header().Get("X-RateLimit-Limit"); got != "1000" {
		t.Fatalf("embedded rate_limit header = %q, want 1000", got)
	}

	if err := srv.deps.SvcDeps.StarRepo(authCtx, "gateway-user/embedded-private", user.Login); err != nil {
		t.Fatalf("StarRepo private: %v", err)
	}
	starredReq := httptest.NewRequest(http.MethodGet, "/api/v3/users/gateway-user/starred", nil)
	setRequestHost(starredReq, "embed.local")
	starredReq.Header.Set("X-Embedded-User", "ok")
	starredRec := httptest.NewRecorder()
	srv.Handler().ServeHTTP(starredRec, starredReq)
	if starredRec.Code != http.StatusOK {
		t.Fatalf("embedded starred auth: expected 200, got %d: %s", starredRec.Code, starredRec.Body.String())
	}
	var starredBody []map[string]any
	if err := json.Unmarshal(starredRec.Body.Bytes(), &starredBody); err != nil {
		t.Fatalf("decode starred body: %v", err)
	}
	if len(starredBody) != 1 {
		t.Fatalf("starred repo count = %d, want 1", len(starredBody))
	}
	if got := starredBody[0]["full_name"]; got != "gateway-user/embedded-private" {
		t.Fatalf("starred repo full_name = %v, want gateway-user/embedded-private", got)
	}
}

func TestNew_EmbeddedIdentityPreservesAnonymousOptionalRoutes(t *testing.T) {
	root := t.TempDir()
	srv, err := New(config.Config{
		TenantID:    "test",
		DBdsn:       createBootstrapDSN(t, "server_embedded_anon", "test"),
		GitRepoDir:  filepath.Join(root, "repos"),
		BaseURL:     "http://embed.local",
		ListenMode:  "production",
		Environment: "production",
	}, WithAuthenticator(headerAuthenticator{
		header: "X-Embedded-User",
		identity: agsauth.Identity{
			Provider: "meshx",
			Subject:  "subject-2",
			Login:    "public-owner",
		},
	}))
	if err != nil {
		t.Fatalf("New: %v", err)
	}
	cleanupTestServer(t, srv)

	tenantCtx, _ := serverTenantContext(t, srv, "test")
	principal, err := srv.deps.SvcDeps.ResolveEmbeddedPrincipal(tenantCtx, service.EmbeddedIdentity{
		Provider: "meshx",
		Subject:  "subject-2",
		Login:    "public-owner",
		Name:     "Public Owner",
	})
	if err != nil {
		t.Fatalf("ResolveEmbeddedPrincipal: %v", err)
	}
	owner := principal.User()
	authCtx, err := service.ContextWithPrincipal(tenantCtx, principal)
	if err != nil {
		t.Fatalf("ContextWithPrincipal: %v", err)
	}
	if _, err := srv.deps.SvcDeps.CreateRepo(authCtx, service.CreateRepoInput{
		OwnerLogin: owner.Login,
		Name:       "public-repo",
		Private:    false,
		AutoInit:   true,
	}); err != nil {
		t.Fatalf("CreateRepo: %v", err)
	}

	req := httptest.NewRequest(http.MethodGet, "/api/v3/repos/public-owner/public-repo", nil)
	setRequestHost(req, "embed.local")
	rec := httptest.NewRecorder()
	srv.Handler().ServeHTTP(rec, req)
	if rec.Code != http.StatusOK {
		t.Fatalf("anonymous optional route: expected 200, got %d: %s", rec.Code, rec.Body.String())
	}

	rateReq := httptest.NewRequest(http.MethodGet, "/api/v3/rate_limit", nil)
	setRequestHost(rateReq, "embed.local")
	rateRec := httptest.NewRecorder()
	srv.Handler().ServeHTTP(rateRec, rateReq)
	if rateRec.Code != http.StatusOK {
		t.Fatalf("anonymous rate_limit route: expected 200, got %d: %s", rateRec.Code, rateRec.Body.String())
	}
	if got := rateRec.Header().Get("X-RateLimit-Limit"); got != "100" {
		t.Fatalf("anonymous rate_limit header = %q, want 100", got)
	}

	if err := srv.deps.SvcDeps.StarRepo(authCtx, "public-owner/public-repo", owner.Login); err != nil {
		t.Fatalf("StarRepo public: %v", err)
	}
	if _, err := srv.deps.SvcDeps.CreateRepo(authCtx, service.CreateRepoInput{
		OwnerLogin: owner.Login,
		Name:       "private-repo",
		Private:    true,
		AutoInit:   true,
	}); err != nil {
		t.Fatalf("CreateRepo private: %v", err)
	}
	if err := srv.deps.SvcDeps.StarRepo(authCtx, "public-owner/private-repo", owner.Login); err != nil {
		t.Fatalf("StarRepo private: %v", err)
	}

	starredReq := httptest.NewRequest(http.MethodGet, "/api/v3/users/public-owner/starred", nil)
	setRequestHost(starredReq, "embed.local")
	starredRec := httptest.NewRecorder()
	srv.Handler().ServeHTTP(starredRec, starredReq)
	if starredRec.Code != http.StatusOK {
		t.Fatalf("anonymous starred route: expected 200, got %d: %s", starredRec.Code, starredRec.Body.String())
	}
	var starredBody []map[string]any
	if err := json.Unmarshal(starredRec.Body.Bytes(), &starredBody); err != nil {
		t.Fatalf("decode anonymous starred body: %v", err)
	}
	if len(starredBody) != 1 {
		t.Fatalf("anonymous starred repo count = %d, want 1", len(starredBody))
	}
	if got := starredBody[0]["full_name"]; got != "public-owner/public-repo" {
		t.Fatalf("anonymous starred repo full_name = %v, want public-owner/public-repo", got)
	}
}

func TestShutdown_CancelsServerContextBeforeWaitingForWorkers(t *testing.T) {
	srvCtx, srvCancel := context.WithCancel(context.Background())
	svc, registry, tenantCtx := newLifecycleTestService(t, srvCtx)
	svc.Wg.Add(1)
	workerExited := make(chan struct{})
	go func() {
		defer svc.Wg.Done()
		defer close(workerExited)
		<-tenantCtx.Done()
	}()

	srv := &Server{
		deps: &bootstrapDeps{
			SvcDeps:   svc,
			SrvCancel: srvCancel,
			Registry:  registry,
		},
	}

	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	if err := srv.Shutdown(ctx); err != nil {
		t.Fatalf("Shutdown: %v", err)
	}

	select {
	case <-workerExited:
	case <-time.After(time.Second):
		t.Fatal("expected worker to exit after shutdown canceled server context")
	}
}

func TestInitTenantServiceDeps_UsesConfiguredDataRootForWikiStorage(t *testing.T) {
	dataRoot := t.TempDir()
	fixture := newTenantServiceTestFixture(t, config.Config{
		TenantID:    "test",
		BaseURL:     "http://localhost:8080",
		DBdsn:       createBootstrapDSN(t, "server_wiki_storage", "test"),
		GitRepoDir:  dataRoot,
		ListenMode:  "production",
		Environment: "production",
	})
	_, resources := fixture.tenant(t, "test")
	canonicalRoot, err := gitstore.CanonicalizeRoot(dataRoot)
	if err != nil {
		t.Fatalf("canonicalize data root: %v", err)
	}
	if resources.WikiBlob.Root() != canonicalRoot {
		t.Fatalf("WikiBlob root = %q, want %q", resources.WikiBlob.Root(), canonicalRoot)
	}
}

func TestAuthenticatorOptionsRejectDuplicateRegistration(t *testing.T) {
	authenticator := headerAuthenticator{}

	var global options
	WithAuthenticator(authenticator)(&global)
	WithAuthenticator(authenticator)(&global)
	if len(global.errors) != 1 {
		t.Fatalf("global option errors = %v, want one duplicate error", global.errors)
	}

	var perTenant options
	WithTenantAuthenticator("tenant-a", authenticator)(&perTenant)
	WithTenantAuthenticator("tenant-a", authenticator)(&perTenant)
	if len(perTenant.errors) != 1 {
		t.Fatalf("tenant option errors = %v, want one duplicate error", perTenant.errors)
	}
}

func TestEmbeddedAuthConfigRequiresExplicitConfiguredTenant(t *testing.T) {
	authenticator := headerAuthenticator{}

	multitenant := &tenantRegistry{
		ids: []tenant.ID{"tenant-a", "tenant-b"},
		byID: map[tenant.ID]*service.TenantResources{
			"tenant-a": {},
			"tenant-b": {},
		},
	}
	if _, err := embeddedAuthConfig(options{
		singleTenantAuthenticator: authenticator,
	}, multitenant); err == nil {
		t.Fatal("process-global authenticator was accepted for a multitenant server")
	}

	singleTenant := &tenantRegistry{
		ids: []tenant.ID{"tenant-a"},
		byID: map[tenant.ID]*service.TenantResources{
			"tenant-a": {},
		},
	}
	if _, err := embeddedAuthConfig(options{
		tenantAuthenticators: map[tenant.ID]Authenticator{
			"tenant-b": authenticator,
		},
	}, singleTenant); !errors.Is(err, errUnknownTenant) {
		t.Fatalf("unknown tenant authenticator error = %v, want %v", err, errUnknownTenant)
	}
}
