package core

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"io"
	"net/http"
	"net/http/httptest"
	"os"
	"sort"
	"strings"
	"sync"
	"testing"
	"time"
)

type deadlineAwareModelAgent struct {
	stubModelModeAgent
	mu          sync.Mutex
	hasDeadline bool
}

func (a *deadlineAwareModelAgent) AvailableModels(ctx context.Context) []ModelOption {
	a.mu.Lock()
	_, ok := ctx.Deadline()
	a.hasDeadline = ok
	a.mu.Unlock()
	return []ModelOption{{Name: "gpt-4.1"}}
}

func (a *deadlineAwareModelAgent) sawDeadline() bool {
	a.mu.Lock()
	defer a.mu.Unlock()
	return a.hasDeadline
}

// testManagementServer creates a ManagementServer with a test engine and returns an httptest.Server.
func testManagementServer(t *testing.T, token string) (*ManagementServer, *httptest.Server, *Engine) {
	t.Helper()

	agent := &stubAgent{}
	sm := NewSessionManager("")
	e := NewEngine("test-project", agent, nil, "", LangEnglish)
	e.sessions = sm

	mgmt := NewManagementServer(0, token, nil)
	mgmt.RegisterEngine("test-project", e)

	mux := http.NewServeMux()
	prefix := "/api/v1"
	mux.HandleFunc(prefix+"/status", mgmt.wrap(mgmt.handleStatus))
	mux.HandleFunc(prefix+"/restart", mgmt.wrap(mgmt.handleRestart))
	mux.HandleFunc(prefix+"/reload", mgmt.wrap(mgmt.handleReload))
	mux.HandleFunc(prefix+"/config", mgmt.wrap(mgmt.handleConfig))
	mux.HandleFunc(prefix+"/settings", mgmt.wrap(mgmt.handleGlobalSettings))
	mux.HandleFunc(prefix+"/agents", mgmt.wrap(mgmt.handleAgents))
	mux.HandleFunc(prefix+"/projects", mgmt.wrap(mgmt.handleProjects))
	mux.HandleFunc(prefix+"/projects/", mgmt.wrap(mgmt.handleProjectRoutes))
	mux.HandleFunc(prefix+"/cron", mgmt.wrap(mgmt.handleCron))
	mux.HandleFunc(prefix+"/cron/", mgmt.wrap(mgmt.handleCronByID))
	mux.HandleFunc(prefix+"/providers", mgmt.wrap(mgmt.handleGlobalProviders))
	mux.HandleFunc(prefix+"/providers/", mgmt.wrap(mgmt.handleGlobalProviderRoutes))
	mux.HandleFunc(prefix+"/skills", mgmt.wrap(mgmt.handleSkills))
	mux.HandleFunc(prefix+"/skills/presets", mgmt.wrap(mgmt.handleSkillPresets))
	mux.HandleFunc(prefix+"/bridge/adapters", mgmt.wrap(mgmt.handleBridgeAdapters))

	ts := httptest.NewServer(mux)
	t.Cleanup(ts.Close)

	return mgmt, ts, e
}

type mgmtResponse struct {
	OK    bool            `json:"ok"`
	Data  json.RawMessage `json:"data,omitempty"`
	Error string          `json:"error,omitempty"`
}

func mgmtGet(t *testing.T, url, token string) mgmtResponse {
	t.Helper()
	req, _ := http.NewRequest("GET", url, nil)
	if token != "" {
		req.Header.Set("Authorization", "Bearer "+token)
	}
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		t.Fatalf("GET %s: %v", url, err)
	}
	defer resp.Body.Close()
	var r mgmtResponse
	if err := json.NewDecoder(resp.Body).Decode(&r); err != nil {
		t.Fatalf("decode GET response: %v", err)
	}
	return r
}

func mgmtPost(t *testing.T, url, token string, body any) mgmtResponse {
	t.Helper()
	var buf bytes.Buffer
	if body != nil {
		if err := json.NewEncoder(&buf).Encode(body); err != nil {
			t.Fatalf("encode POST body: %v", err)
		}
	}
	req, _ := http.NewRequest("POST", url, &buf)
	req.Header.Set("Content-Type", "application/json")
	if token != "" {
		req.Header.Set("Authorization", "Bearer "+token)
	}
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		t.Fatalf("POST %s: %v", url, err)
	}
	defer resp.Body.Close()
	var r mgmtResponse
	if err := json.NewDecoder(resp.Body).Decode(&r); err != nil {
		t.Fatalf("decode POST response: %v", err)
	}
	return r
}

func mgmtPatch(t *testing.T, url, token string, body any) mgmtResponse {
	t.Helper()
	var buf bytes.Buffer
	if body != nil {
		if err := json.NewEncoder(&buf).Encode(body); err != nil {
			t.Fatalf("encode PATCH body: %v", err)
		}
	}
	req, _ := http.NewRequest("PATCH", url, &buf)
	req.Header.Set("Content-Type", "application/json")
	if token != "" {
		req.Header.Set("Authorization", "Bearer "+token)
	}
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		t.Fatalf("PATCH %s: %v", url, err)
	}
	defer resp.Body.Close()
	var r mgmtResponse
	if err := json.NewDecoder(resp.Body).Decode(&r); err != nil {
		t.Fatalf("decode PATCH response: %v", err)
	}
	return r
}

func mgmtDelete(t *testing.T, url, token string) mgmtResponse {
	t.Helper()
	req, _ := http.NewRequest("DELETE", url, nil)
	if token != "" {
		req.Header.Set("Authorization", "Bearer "+token)
	}
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		t.Fatalf("DELETE %s: %v", url, err)
	}
	defer resp.Body.Close()
	var r mgmtResponse
	if err := json.NewDecoder(resp.Body).Decode(&r); err != nil {
		t.Fatalf("decode DELETE response: %v", err)
	}
	return r
}

func TestMgmt_AuthRequired(t *testing.T) {
	_, ts, _ := testManagementServer(t, "secret-token")

	r := mgmtGet(t, ts.URL+"/api/v1/status", "")
	if r.OK {
		t.Fatal("expected auth failure without token")
	}
	if !strings.Contains(r.Error, "unauthorized") {
		t.Fatalf("expected unauthorized error, got: %s", r.Error)
	}

	r = mgmtGet(t, ts.URL+"/api/v1/status", "wrong-token")
	if r.OK {
		t.Fatal("expected auth failure with wrong token")
	}

	r = mgmtGet(t, ts.URL+"/api/v1/status", "secret-token")
	if !r.OK {
		t.Fatalf("expected success with correct token, got error: %s", r.Error)
	}
}

func TestMgmt_AuthQueryParam(t *testing.T) {
	_, ts, _ := testManagementServer(t, "qp-token")

	r := mgmtGet(t, ts.URL+"/api/v1/status?token=qp-token", "")
	if !r.OK {
		t.Fatalf("expected success with query param token, got: %s", r.Error)
	}
}

func TestMgmt_NoAuthRequired(t *testing.T) {
	_, ts, _ := testManagementServer(t, "")

	r := mgmtGet(t, ts.URL+"/api/v1/status", "")
	if !r.OK {
		t.Fatalf("expected success without token when no token configured, got: %s", r.Error)
	}
}

func TestMgmt_Status(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")

	r := mgmtGet(t, ts.URL+"/api/v1/status", "tok")
	if !r.OK {
		t.Fatalf("status failed: %s", r.Error)
	}

	var data map[string]any
	if err := json.Unmarshal(r.Data, &data); err != nil {
		t.Fatalf("unmarshal status data: %v", err)
	}
	if data["projects_count"] != float64(1) {
		t.Fatalf("expected 1 project, got %v", data["projects_count"])
	}
}

func TestMgmt_StatusIncludesBridgeToken(t *testing.T) {
	mgmt, ts, _ := testManagementServer(t, "tok")
	mgmt.SetBridgeServer(NewBridgeServer(9810, "bridge-secret", "/bridge/ws", nil))

	r := mgmtGet(t, ts.URL+"/api/v1/status", "tok")
	if !r.OK {
		t.Fatalf("status failed: %s", r.Error)
	}

	var data struct {
		Bridge struct {
			Enabled bool   `json:"enabled"`
			Port    int    `json:"port"`
			Path    string `json:"path"`
			Token   string `json:"token"`
		} `json:"bridge"`
	}
	if err := json.Unmarshal(r.Data, &data); err != nil {
		t.Fatalf("unmarshal status data: %v", err)
	}
	if !data.Bridge.Enabled {
		t.Fatal("expected bridge to be enabled")
	}
	if data.Bridge.Token != "bridge-secret" {
		t.Fatalf("expected bridge token, got %q", data.Bridge.Token)
	}
}

func TestMgmt_Projects(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")

	r := mgmtGet(t, ts.URL+"/api/v1/projects", "tok")
	if !r.OK {
		t.Fatalf("projects failed: %s", r.Error)
	}

	var data struct {
		Projects []map[string]any `json:"projects"`
	}
	if err := json.Unmarshal(r.Data, &data); err != nil {
		t.Fatalf("unmarshal projects data: %v", err)
	}
	if len(data.Projects) != 1 {
		t.Fatalf("expected 1 project, got %d", len(data.Projects))
	}
	if data.Projects[0]["name"] != "test-project" {
		t.Fatalf("expected test-project, got %v", data.Projects[0]["name"])
	}
}

func TestMgmt_ProjectDetail(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")

	r := mgmtGet(t, ts.URL+"/api/v1/projects/test-project", "tok")
	if !r.OK {
		t.Fatalf("project detail failed: %s", r.Error)
	}

	var data map[string]any
	if err := json.Unmarshal(r.Data, &data); err != nil {
		t.Fatalf("unmarshal project detail: %v", err)
	}
	if data["name"] != "test-project" {
		t.Fatalf("expected test-project, got %v", data["name"])
	}

	r = mgmtGet(t, ts.URL+"/api/v1/projects/nonexistent", "tok")
	if r.OK {
		t.Fatal("expected 404 for nonexistent project")
	}
}

func TestMgmt_ProjectPatch(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")

	r := mgmtPatch(t, ts.URL+"/api/v1/projects/test-project", "tok", map[string]any{
		"language": "zh",
	})
	if !r.OK {
		t.Fatalf("patch failed: %s", r.Error)
	}
}

func TestMgmt_Sessions(t *testing.T) {
	_, ts, e := testManagementServer(t, "tok")

	e.sessions.GetOrCreateActive("user1")

	r := mgmtGet(t, ts.URL+"/api/v1/projects/test-project/sessions", "tok")
	if !r.OK {
		t.Fatalf("sessions list failed: %s", r.Error)
	}

	// Create a session via API
	r = mgmtPost(t, ts.URL+"/api/v1/projects/test-project/sessions", "tok", map[string]string{
		"session_key": "user2",
		"name":        "work",
	})
	if !r.OK {
		t.Fatalf("create session failed: %s", r.Error)
	}
}

func TestMgmt_SessionDetail(t *testing.T) {
	_, ts, e := testManagementServer(t, "tok")

	s := e.sessions.GetOrCreateActive("user1")
	s.AddHistory("user", "hello")
	s.AddHistory("assistant", "hi there")

	r := mgmtGet(t, ts.URL+"/api/v1/projects/test-project/sessions/"+s.ID, "tok")
	if !r.OK {
		t.Fatalf("session detail failed: %s", r.Error)
	}

	var data struct {
		History []map[string]any `json:"history"`
	}
	if err := json.Unmarshal(r.Data, &data); err != nil {
		t.Fatalf("unmarshal session detail: %v", err)
	}
	if len(data.History) != 2 {
		t.Fatalf("expected 2 history entries, got %d", len(data.History))
	}
}

// TestMgmt_SessionsConcurrentNameWriteAndList pins the bug where the
// POST /sessions handler wrote s.Name = body.Name directly without
// holding s.mu, while the GET handler reads s.Name through s.mu.Lock().
// Run with -race to detect the data race; with the production fix
// the test stays clean.
func TestMgmt_SessionsConcurrentNameWriteAndList(t *testing.T) {
	_, ts, e := testManagementServer(t, "tok")

	// Pre-create the session so both handlers operate on the same instance.
	e.sessions.GetOrCreateActive("user1")

	listURL := ts.URL + "/api/v1/projects/test-project/sessions"
	postURL := listURL

	var wg sync.WaitGroup
	for i := 0; i < 30; i++ {
		wg.Add(1)
		go func(i int) {
			defer wg.Done()
			name := "a"
			if i%2 == 0 {
				name = "b"
			}
			_ = mgmtPost(t, postURL, "tok", map[string]string{
				"session_key": "user1",
				"name":        name,
			})
		}(i)
	}
	for i := 0; i < 30; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			_ = mgmtGet(t, listURL, "tok")
		}()
	}
	wg.Wait()
}

func TestMgmt_SessionDelete(t *testing.T) {
	_, ts, e := testManagementServer(t, "tok")

	s := e.sessions.GetOrCreateActive("user1")
	sid := s.ID

	r := mgmtDelete(t, ts.URL+"/api/v1/projects/test-project/sessions/"+sid, "tok")
	if !r.OK {
		t.Fatalf("delete session failed: %s", r.Error)
	}

	r = mgmtGet(t, ts.URL+"/api/v1/projects/test-project/sessions/"+sid, "tok")
	if r.OK {
		t.Fatal("expected 404 after deletion")
	}
}

func TestMgmt_Config(t *testing.T) {
	srv, ts, _ := testManagementServer(t, "tok")

	// Write a temp TOML file and point the server at it
	tmp := t.TempDir()
	cfgPath := tmp + "/config.toml"
	if err := os.WriteFile(cfgPath, []byte("[display]\ntitle = \"test\"\n"), 0644); err != nil {
		t.Fatal(err)
	}
	srv.SetConfigFilePath(cfgPath)

	req, _ := http.NewRequest("GET", ts.URL+"/api/v1/config", nil)
	req.Header.Set("Authorization", "Bearer tok")
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		t.Fatal(err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != 200 {
		t.Fatalf("expected 200, got %d", resp.StatusCode)
	}
	body, _ := io.ReadAll(resp.Body)
	if !strings.Contains(string(body), "title") {
		t.Fatalf("expected TOML content, got: %s", body)
	}
}

func TestMgmt_Reload(t *testing.T) {
	_, ts, e := testManagementServer(t, "tok")

	reloaded := false
	e.configReloadFunc = func() (*ConfigReloadResult, error) {
		reloaded = true
		return &ConfigReloadResult{}, nil
	}

	r := mgmtPost(t, ts.URL+"/api/v1/reload", "tok", nil)
	if !r.OK {
		t.Fatalf("reload failed: %s", r.Error)
	}
	if !reloaded {
		t.Fatal("expected config reload to be triggered")
	}
}

func TestMgmt_BridgeAdapters(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")

	r := mgmtGet(t, ts.URL+"/api/v1/bridge/adapters", "tok")
	if !r.OK {
		t.Fatalf("bridge adapters failed: %s", r.Error)
	}
}

func TestMgmt_HeartbeatNotConfigured(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")

	r := mgmtGet(t, ts.URL+"/api/v1/projects/test-project/heartbeat", "tok")
	if r.OK {
		var data map[string]any
		if err := json.Unmarshal(r.Data, &data); err != nil {
			t.Fatalf("unmarshal heartbeat data: %v", err)
		}
		// heartbeat scheduler is nil, so we expect service unavailable
	}
}

func TestMgmt_HeartbeatWithScheduler(t *testing.T) {
	mgmt, ts, _ := testManagementServer(t, "tok")
	hs := NewHeartbeatScheduler("")
	mgmt.SetHeartbeatScheduler(hs)

	r := mgmtGet(t, ts.URL+"/api/v1/projects/test-project/heartbeat", "tok")
	if !r.OK {
		t.Fatalf("heartbeat status failed: %s", r.Error)
	}

	var data map[string]any
	if err := json.Unmarshal(r.Data, &data); err != nil {
		t.Fatalf("unmarshal heartbeat status: %v", err)
	}
	if data["enabled"] != false {
		t.Fatalf("expected heartbeat disabled, got %v", data["enabled"])
	}
}

func TestMgmt_CronNilScheduler(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")

	r := mgmtGet(t, ts.URL+"/api/v1/cron", "tok")
	if r.OK {
		t.Fatal("expected error when cron scheduler is nil")
	}
}

func TestMgmt_CronWithScheduler(t *testing.T) {
	mgmt, ts, e := testManagementServer(t, "tok")
	store, err := NewCronStore(t.TempDir())
	if err != nil {
		t.Fatal(err)
	}
	cs := NewCronScheduler(store)
	cs.RegisterEngine("test-project", e)
	mgmt.SetCronScheduler(cs)

	// List (empty)
	r := mgmtGet(t, ts.URL+"/api/v1/cron", "tok")
	if !r.OK {
		t.Fatalf("cron list failed: %s", r.Error)
	}

	// Add
	r = mgmtPost(t, ts.URL+"/api/v1/cron", "tok", map[string]any{
		"project":     "test-project",
		"session_key": "user1",
		"cron_expr":   "0 9 * * *",
		"prompt":      "hello",
		"description": "test cron",
	})
	if !r.OK {
		t.Fatalf("cron add failed: %s", r.Error)
	}

	var job CronJob
	if err := json.Unmarshal(r.Data, &job); err != nil {
		t.Fatalf("unmarshal cron job: %v", err)
	}
	if job.ID == "" {
		t.Fatal("expected cron job ID")
	}

	// List (should have 1)
	r = mgmtGet(t, ts.URL+"/api/v1/cron", "tok")
	if !r.OK {
		t.Fatalf("cron list failed: %s", r.Error)
	}

	// Delete
	r = mgmtDelete(t, ts.URL+"/api/v1/cron/"+job.ID, "tok")
	if !r.OK {
		t.Fatalf("cron delete failed: %s", r.Error)
	}

	// Delete nonexistent
	r = mgmtDelete(t, ts.URL+"/api/v1/cron/nonexistent", "tok")
	if r.OK {
		t.Fatal("expected 404 for nonexistent cron job")
	}
}

func TestMgmt_CronExecByID(t *testing.T) {
	mgmt, ts, e := testManagementServer(t, "tok")
	store, err := NewCronStore(t.TempDir())
	if err != nil {
		t.Fatal(err)
	}
	cs := NewCronScheduler(store)
	mgmt.SetCronScheduler(cs)

	platform := &stubCronReplyTargetPlatform{
		stubPlatformEngine: stubPlatformEngine{n: "discord"},
	}
	agentSession := newResultAgentSession("triggered from management")
	e.platforms = []Platform{platform}
	e.agent = &resultAgent{session: agentSession}
	e.cronScheduler = cs
	cs.RegisterEngine("test-project", e)

	job := &CronJob{
		ID:          "cron-run-1",
		Project:     "test-project",
		SessionKey:  "discord:channel-1:user-1",
		CronExpr:    "0 9 * * *",
		Prompt:      "hello",
		Description: "Run me now",
		Enabled:     false,
		CreatedAt:   time.Now(),
	}
	if err := store.Add(job); err != nil {
		t.Fatal(err)
	}

	r := mgmtPost(t, ts.URL+"/api/v1/cron/"+job.ID+"/exec", "tok", map[string]any{})
	if !r.OK {
		t.Fatalf("cron exec failed: %s", r.Error)
	}

	var data map[string]any
	if err := json.Unmarshal(r.Data, &data); err != nil {
		t.Fatalf("unmarshal run response: %v", err)
	}
	if data["status"] != "triggered" {
		t.Fatalf("status = %v, want triggered", data["status"])
	}
	if data["id"] != job.ID {
		t.Fatalf("id = %v, want %s", data["id"], job.ID)
	}

	deadline := time.Now().Add(2 * time.Second)
	for time.Now().Before(deadline) {
		if len(platform.getSent()) >= 2 {
			break
		}
		time.Sleep(10 * time.Millisecond)
	}
	if len(platform.getSent()) < 2 {
		t.Fatalf("timed out waiting for triggered cron exec, sent=%v", platform.getSent())
	}

	aliasJob := &CronJob{
		ID:          "cron-run-alias-1",
		Project:     "test-project",
		SessionKey:  "discord:channel-2:user-2",
		CronExpr:    "0 9 * * *",
		Prompt:      "hello alias",
		Description: "Run alias now",
		Enabled:     false,
		CreatedAt:   time.Now(),
	}
	if err := store.Add(aliasJob); err != nil {
		t.Fatal(err)
	}

	e.agent = &resultAgent{session: newResultAgentSession("triggered from management alias")}
	alias := mgmtPost(t, ts.URL+"/api/v1/cron/"+aliasJob.ID+"/run", "tok", map[string]any{})
	if !alias.OK {
		t.Fatalf("cron run compatibility alias failed: %s", alias.Error)
	}

	deadline = time.Now().Add(2 * time.Second)
	for time.Now().Before(deadline) {
		if len(platform.getSent()) >= 4 {
			return
		}
		time.Sleep(10 * time.Millisecond)
	}
	t.Fatalf("timed out waiting for triggered cron run alias, sent=%v", platform.getSent())
}

func TestMgmt_CronExecByID_RejectsExtraPathSegments(t *testing.T) {
	mgmt, ts, e := testManagementServer(t, "tok")
	store, err := NewCronStore(t.TempDir())
	if err != nil {
		t.Fatal(err)
	}
	cs := NewCronScheduler(store)
	mgmt.SetCronScheduler(cs)
	cs.RegisterEngine("test-project", e)

	job := &CronJob{
		ID:         "cron-run-extra",
		Project:    "test-project",
		SessionKey: "discord:channel-1:user-1",
		CronExpr:   "0 9 * * *",
		Prompt:     "hello",
		Enabled:    true,
		CreatedAt:  time.Now(),
	}
	if err := store.Add(job); err != nil {
		t.Fatal(err)
	}

	r := mgmtPost(t, ts.URL+"/api/v1/cron/"+job.ID+"/exec/extra", "tok", map[string]any{})
	if r.OK {
		t.Fatal("expected extra cron path segment to be rejected")
	}
	if !strings.Contains(r.Error, "unknown cron route") {
		t.Fatalf("error = %q, want unknown cron route", r.Error)
	}
}

func TestMgmt_CronExecByID_ProjectMissingIsBadRequest(t *testing.T) {
	mgmt, ts, e := testManagementServer(t, "tok")
	store, err := NewCronStore(t.TempDir())
	if err != nil {
		t.Fatal(err)
	}
	cs := NewCronScheduler(store)
	mgmt.SetCronScheduler(cs)
	cs.RegisterEngine("test-project", e)

	job := &CronJob{
		ID:         "cron-run-missing-project",
		Project:    "ghost",
		SessionKey: "discord:channel-1:user-1",
		CronExpr:   "0 9 * * *",
		Prompt:     "hello",
		Enabled:    true,
		CreatedAt:  time.Now(),
	}
	if err := store.Add(job); err != nil {
		t.Fatal(err)
	}

	r := mgmtPost(t, ts.URL+"/api/v1/cron/"+job.ID+"/exec", "tok", map[string]any{})
	if r.OK {
		t.Fatal("expected exec to fail when project is missing")
	}
	if !strings.Contains(r.Error, "project") {
		t.Fatalf("error = %q, want project error", r.Error)
	}
	if !strings.Contains(r.Error, "not found") {
		t.Fatalf("error = %q, want missing project details", r.Error)
	}
}

func TestMgmt_CORS(t *testing.T) {
	mgmt := NewManagementServer(0, "", []string{"http://localhost:3000"})
	mgmt.RegisterEngine("p", NewEngine("p", &stubAgent{}, nil, "", LangEnglish))

	mux := http.NewServeMux()
	mux.HandleFunc("/api/v1/status", mgmt.wrap(mgmt.handleStatus))
	ts := httptest.NewServer(mux)
	defer ts.Close()

	req, _ := http.NewRequest("OPTIONS", ts.URL+"/api/v1/status", nil)
	req.Header.Set("Origin", "http://localhost:3000")
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		t.Fatal(err)
	}
	resp.Body.Close()

	if resp.StatusCode != http.StatusNoContent {
		t.Fatalf("expected 204 for OPTIONS, got %d", resp.StatusCode)
	}
	if resp.Header.Get("Access-Control-Allow-Origin") != "http://localhost:3000" {
		t.Fatalf("expected CORS origin header, got %q", resp.Header.Get("Access-Control-Allow-Origin"))
	}
}

func TestMgmt_BridgeWebSocketPathProxiesToBridgeServer(t *testing.T) {
	mgmt := NewManagementServer(0, "", []string{"*"})
	mgmt.RegisterEngine("p", NewEngine("p", &stubAgent{}, nil, "", LangEnglish))
	mgmt.SetBridgeServer(NewBridgeServer(9810, "bridge-secret", "/bridge/ws", []string{"*"}))

	mux := http.NewServeMux()
	ts := httptest.NewServer(mgmt.buildHandler(mux))
	defer ts.Close()

	req, _ := http.NewRequest("GET", ts.URL+"/bridge/ws?token=bridge-secret", nil)
	req.Header.Set("Connection", "Upgrade")
	req.Header.Set("Upgrade", "websocket")
	req.Header.Set("Sec-WebSocket-Version", "13")
	req.Header.Set("Sec-WebSocket-Key", "dGhlIHNhbXBsZSBub25jZQ==")
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		t.Fatal(err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusSwitchingProtocols {
		t.Fatalf("expected websocket upgrade, got %d", resp.StatusCode)
	}
}

func TestMgmt_BridgeWebSocketPathWorksWhenBridgeServerSetAfterHandlerBuild(t *testing.T) {
	mgmt := NewManagementServer(0, "", []string{"*"})
	mgmt.RegisterEngine("p", NewEngine("p", &stubAgent{}, nil, "", LangEnglish))

	mux := http.NewServeMux()
	ts := httptest.NewServer(mgmt.buildHandler(mux))
	defer ts.Close()

	mgmt.SetBridgeServer(NewBridgeServer(9810, "bridge-secret", "/bridge/ws", []string{"*"}))

	req, _ := http.NewRequest("GET", ts.URL+"/bridge/ws?token=bridge-secret", nil)
	req.Header.Set("Connection", "Upgrade")
	req.Header.Set("Upgrade", "websocket")
	req.Header.Set("Sec-WebSocket-Version", "13")
	req.Header.Set("Sec-WebSocket-Key", "dGhlIHNhbXBsZSBub25jZQ==")
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		t.Fatal(err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusSwitchingProtocols {
		t.Fatalf("expected websocket upgrade after late bridge setup, got %d", resp.StatusCode)
	}
}

func TestMgmt_MethodNotAllowed(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")

	req, _ := http.NewRequest("POST", ts.URL+"/api/v1/status", nil)
	req.Header.Set("Authorization", "Bearer tok")
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		t.Fatal(err)
	}

	var r mgmtResponse
	if err := json.NewDecoder(resp.Body).Decode(&r); err != nil {
		t.Fatalf("decode method not allowed response: %v", err)
	}
	resp.Body.Close()
}

func TestMgmt_ProjectModel_UsesSwitchModelWithActiveProvider(t *testing.T) {
	agent := &stubModelModeAgent{
		model: "gpt-4.1-mini",
		providers: []ProviderConfig{
			{
				Name:   "openai",
				Model:  "gpt-4.1-mini",
				Models: []ModelOption{{Name: "gpt-4.1", Alias: "gpt"}, {Name: "gpt-4.1-mini", Alias: "mini"}},
			},
		},
		active: "openai",
	}
	e := NewEngine("test-project", agent, nil, "", LangEnglish)
	var savedProvider, savedModel string
	e.SetProviderModelSaveFunc(func(providerName, model string) error {
		savedProvider = providerName
		savedModel = model
		return nil
	})

	mgmt := NewManagementServer(0, "tok", nil)
	mgmt.RegisterEngine("test-project", e)
	mux := http.NewServeMux()
	mux.HandleFunc("/api/v1/projects/", mgmt.wrap(mgmt.handleProjectRoutes))
	ts := httptest.NewServer(mux)
	defer ts.Close()

	r := mgmtPost(t, ts.URL+"/api/v1/projects/test-project/model", "tok", map[string]string{"model": "gpt-4.1"})
	if !r.OK {
		t.Fatalf("update model failed: %s", r.Error)
	}

	if got := agent.GetModel(); got != "gpt-4.1" {
		t.Fatalf("GetModel() = %q, want gpt-4.1", got)
	}
	if got := agent.GetActiveProvider(); got == nil || got.Model != "gpt-4.1" {
		t.Fatalf("active provider model = %#v, want gpt-4.1", got)
	}
	if savedProvider != "openai" || savedModel != "gpt-4.1" {
		t.Fatalf("saved provider/model = %q/%q, want openai/gpt-4.1", savedProvider, savedModel)
	}
}

func TestMgmt_ProjectModel_SavesModelWithoutActiveProvider(t *testing.T) {
	agent := &stubModelModeAgent{
		model: "gpt-4.1-mini",
		providers: []ProviderConfig{
			{
				Name:   "openai",
				Model:  "gpt-4.1-mini",
				Models: []ModelOption{{Name: "gpt-4.1", Alias: "gpt"}, {Name: "gpt-4.1-mini", Alias: "mini"}},
			},
		},
	}
	e := NewEngine("test-project", agent, nil, "", LangEnglish)
	var savedModel string
	var providerSaveCalled bool
	e.SetModelSaveFunc(func(model string) error {
		savedModel = model
		return nil
	})
	e.SetProviderModelSaveFunc(func(providerName, model string) error {
		providerSaveCalled = true
		return nil
	})

	mgmt := NewManagementServer(0, "tok", nil)
	mgmt.RegisterEngine("test-project", e)
	mux := http.NewServeMux()
	mux.HandleFunc("/api/v1/projects/", mgmt.wrap(mgmt.handleProjectRoutes))
	ts := httptest.NewServer(mux)
	defer ts.Close()

	r := mgmtPost(t, ts.URL+"/api/v1/projects/test-project/model", "tok", map[string]string{"model": "gpt-4.1"})
	if !r.OK {
		t.Fatalf("update model failed: %s", r.Error)
	}

	if got := agent.GetModel(); got != "gpt-4.1" {
		t.Fatalf("GetModel() = %q, want gpt-4.1", got)
	}
	if savedModel != "gpt-4.1" {
		t.Fatalf("saved model = %q, want gpt-4.1", savedModel)
	}
	if providerSaveCalled {
		t.Fatal("provider save callback should not be called without active provider")
	}
}

func TestMgmt_ProjectModel_ReturnsErrorWhenModelSaveFails(t *testing.T) {
	agent := &stubModelModeAgent{
		model: "gpt-4.1-mini",
		providers: []ProviderConfig{
			{
				Name:   "openai",
				Model:  "gpt-4.1-mini",
				Models: []ModelOption{{Name: "gpt-4.1", Alias: "gpt"}, {Name: "gpt-4.1-mini", Alias: "mini"}},
			},
		},
	}
	e := NewEngine("test-project", agent, nil, "", LangEnglish)
	e.SetModelSaveFunc(func(model string) error {
		return errors.New("disk full")
	})

	mgmt := NewManagementServer(0, "tok", nil)
	mgmt.RegisterEngine("test-project", e)
	mux := http.NewServeMux()
	mux.HandleFunc("/api/v1/projects/", mgmt.wrap(mgmt.handleProjectRoutes))
	ts := httptest.NewServer(mux)
	defer ts.Close()

	r := mgmtPost(t, ts.URL+"/api/v1/projects/test-project/model", "tok", map[string]string{"model": "gpt-4.1"})
	if r.OK {
		t.Fatal("update model unexpectedly succeeded")
	}
	if !strings.Contains(r.Error, "disk full") {
		t.Fatalf("error = %q, want save failure", r.Error)
	}
	if got := agent.GetModel(); got != "gpt-4.1-mini" {
		t.Fatalf("GetModel() = %q, want unchanged gpt-4.1-mini", got)
	}
}

func TestMgmt_ProjectModels_UsesTimeoutContext(t *testing.T) {
	agent := &deadlineAwareModelAgent{}
	e := NewEngine("test-project", agent, nil, "", LangEnglish)

	mgmt := NewManagementServer(0, "tok", nil)
	mgmt.RegisterEngine("test-project", e)
	mux := http.NewServeMux()
	mux.HandleFunc("/api/v1/projects/", mgmt.wrap(mgmt.handleProjectRoutes))
	ts := httptest.NewServer(mux)
	defer ts.Close()

	r := mgmtGet(t, ts.URL+"/api/v1/projects/test-project/models", "tok")
	if !r.OK {
		t.Fatalf("project models failed: %s", r.Error)
	}
	if !agent.sawDeadline() {
		t.Fatal("AvailableModels context has no deadline; want timeout-bounded context")
	}
}

func TestMgmt_RemoveGlobalProvider_PurgesFromEngines(t *testing.T) {
	agent := &stubProviderAgent{
		providers: []ProviderConfig{
			{Name: "prov-a", BaseURL: "https://a.example"},
			{Name: "prov-b", BaseURL: "https://b.example"},
		},
		active: "prov-b",
	}
	e := NewEngine("proj", agent, nil, "", LangEnglish)

	mgmt := NewManagementServer(0, "tok", nil)
	mgmt.RegisterEngine("proj", e)

	removed := ""
	mgmt.SetRemoveGlobalProvider(func(name string) error {
		removed = name
		return nil
	})

	mux := http.NewServeMux()
	mux.HandleFunc("/api/v1/providers/", mgmt.wrap(mgmt.handleGlobalProviderRoutes))
	ts := httptest.NewServer(mux)
	defer ts.Close()

	req, _ := http.NewRequest("DELETE", ts.URL+"/api/v1/providers/prov-a", nil)
	req.Header.Set("Authorization", "Bearer tok")
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		t.Fatalf("DELETE: %v", err)
	}
	resp.Body.Close()

	if removed != "prov-a" {
		t.Fatalf("removeGlobalProvider called with %q, want prov-a", removed)
	}

	remaining := agent.ListProviders()
	if len(remaining) != 1 {
		t.Fatalf("remaining providers = %d, want 1", len(remaining))
	}
	if remaining[0].Name != "prov-b" {
		t.Fatalf("remaining provider = %q, want prov-b", remaining[0].Name)
	}
}

func TestResolveGlobalProviderForAgent(t *testing.T) {
	g := GlobalProviderInfo{
		Name:    "relay",
		APIKey:  "sk-123",
		BaseURL: "https://api.example.com/anthropic",
		Model:   "claude-sonnet-4",
		Endpoints: map[string]string{
			"codex": "https://api.example.com/v1",
		},
		AgentModels: map[string]string{
			"codex": "gpt-5.3-codex",
		},
		AgentModelLists: map[string][]GlobalModelEntry{
			"codex": {{Model: "gpt-5.3-codex"}, {Model: "gpt-5.4"}},
		},
		Models: []struct {
			Model string `json:"model"`
			Alias string `json:"alias,omitempty"`
		}{{Model: "claude-sonnet-4"}, {Model: "claude-opus-4"}},
	}

	// claudecode: should use top-level values
	cc := resolveGlobalProviderForAgent(g, "claudecode")
	if cc.BaseURL != "https://api.example.com/anthropic" {
		t.Errorf("claudecode BaseURL = %q", cc.BaseURL)
	}
	if cc.Model != "claude-sonnet-4" {
		t.Errorf("claudecode Model = %q", cc.Model)
	}
	if len(cc.Models) != 2 || cc.Models[0].Name != "claude-sonnet-4" {
		t.Errorf("claudecode Models = %v", cc.Models)
	}

	// codex: should use per-agent overrides
	cx := resolveGlobalProviderForAgent(g, "codex")
	if cx.BaseURL != "https://api.example.com/v1" {
		t.Errorf("codex BaseURL = %q", cx.BaseURL)
	}
	if cx.Model != "gpt-5.3-codex" {
		t.Errorf("codex Model = %q", cx.Model)
	}
	if len(cx.Models) != 2 || cx.Models[0].Name != "gpt-5.3-codex" {
		t.Errorf("codex Models = %v", cx.Models)
	}
}

func TestMgmt_AddPlatformToNewProject_DoesNotRequireEngine(t *testing.T) {
	mgmt, ts, _ := testManagementServer(t, "tok")

	var savedProject, savedPlatType string
	mgmt.SetAddPlatformToProject(func(proj, platType string, opts map[string]any, workDir, agentType string) error {
		savedProject = proj
		savedPlatType = platType
		return nil
	})

	// "brand-new-project" has no engine registered — this must NOT return 404.
	r := mgmtPost(t, ts.URL+"/api/v1/projects/brand-new-project/add-platform", "tok", map[string]any{
		"type":    "dingtalk",
		"options": map[string]any{"client_id": "abc", "client_secret": "def"},
	})
	if !r.OK {
		t.Fatalf("add-platform to new project failed: %s — should not require a running engine", r.Error)
	}
	if savedProject != "brand-new-project" {
		t.Fatalf("saved project = %q, want brand-new-project", savedProject)
	}
	if savedPlatType != "dingtalk" {
		t.Fatalf("saved platform type = %q, want dingtalk", savedPlatType)
	}
}

func TestMgmt_OtherRoutesStillRequireEngine(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")

	r := mgmtGet(t, ts.URL+"/api/v1/projects/nonexistent/sessions", "tok")
	if r.OK {
		t.Fatal("expected 404 for sessions on nonexistent project")
	}
	if !strings.Contains(r.Error, "project not found") {
		t.Fatalf("error = %q, want project not found", r.Error)
	}
}

func mgmtPut(t *testing.T, url, token string, body any) mgmtResponse {
	t.Helper()
	var buf bytes.Buffer
	if body != nil {
		if err := json.NewEncoder(&buf).Encode(body); err != nil {
			t.Fatalf("encode PUT body: %v", err)
		}
	}
	req, _ := http.NewRequest("PUT", url, &buf)
	req.Header.Set("Content-Type", "application/json")
	if token != "" {
		req.Header.Set("Authorization", "Bearer "+token)
	}
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		t.Fatalf("PUT %s: %v", url, err)
	}
	defer resp.Body.Close()
	var r mgmtResponse
	if err := json.NewDecoder(resp.Body).Decode(&r); err != nil {
		t.Fatalf("decode PUT response: %v", err)
	}
	return r
}

// ── Restart ──

func TestMgmt_Restart(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")

	r := mgmtPost(t, ts.URL+"/api/v1/restart", "tok", nil)
	if !r.OK {
		t.Fatalf("restart failed: %s", r.Error)
	}
	select {
	case req := <-RestartCh:
		_ = req
	default:
	}
}

func TestMgmt_Restart_MethodNotAllowed(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")
	r := mgmtGet(t, ts.URL+"/api/v1/restart", "tok")
	if r.OK {
		t.Fatal("expected GET on restart to fail")
	}
}

// ── Agents ──

func TestMgmt_Agents(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")

	r := mgmtGet(t, ts.URL+"/api/v1/agents", "tok")
	if !r.OK {
		t.Fatalf("agents failed: %s", r.Error)
	}
	var data struct {
		Agents    []string `json:"agents"`
		Platforms []string `json:"platforms"`
	}
	if err := json.Unmarshal(r.Data, &data); err != nil {
		t.Fatalf("unmarshal agents: %v", err)
	}
}

func TestMgmt_Agents_MethodNotAllowed(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")
	r := mgmtPost(t, ts.URL+"/api/v1/agents", "tok", nil)
	if r.OK {
		t.Fatal("expected POST on agents to fail")
	}
}

// ── Global Settings ──

func TestMgmt_GlobalSettings_Get(t *testing.T) {
	mgmt, ts, _ := testManagementServer(t, "tok")
	mgmt.SetGetGlobalSettings(func() map[string]any {
		return map[string]any{"language": "zh", "auto_start": true}
	})

	r := mgmtGet(t, ts.URL+"/api/v1/settings", "tok")
	if !r.OK {
		t.Fatalf("get settings failed: %s", r.Error)
	}
	var data map[string]any
	if err := json.Unmarshal(r.Data, &data); err != nil {
		t.Fatalf("unmarshal: %v", err)
	}
	if data["language"] != "zh" {
		t.Fatalf("language = %v, want zh", data["language"])
	}
}

func TestMgmt_GlobalSettings_GetNotAvailable(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")
	r := mgmtGet(t, ts.URL+"/api/v1/settings", "tok")
	if r.OK {
		t.Fatal("expected error when getGlobalSettings is nil")
	}
}

func TestMgmt_GlobalSettings_Patch(t *testing.T) {
	mgmt, ts, _ := testManagementServer(t, "tok")

	saved := map[string]any{}
	mgmt.SetGetGlobalSettings(func() map[string]any { return saved })
	mgmt.SetSaveGlobalSettings(func(updates map[string]any) error {
		for k, v := range updates {
			saved[k] = v
		}
		return nil
	})

	r := mgmtPatch(t, ts.URL+"/api/v1/settings", "tok", map[string]any{"language": "ja"})
	if !r.OK {
		t.Fatalf("patch settings failed: %s", r.Error)
	}
	if saved["language"] != "ja" {
		t.Fatalf("saved language = %v, want ja", saved["language"])
	}
}

func TestMgmt_GlobalSettings_PatchSaveError(t *testing.T) {
	mgmt, ts, _ := testManagementServer(t, "tok")
	mgmt.SetSaveGlobalSettings(func(updates map[string]any) error {
		return errors.New("write failed")
	})
	r := mgmtPatch(t, ts.URL+"/api/v1/settings", "tok", map[string]any{"x": 1})
	if r.OK {
		t.Fatal("expected save error")
	}
	if !strings.Contains(r.Error, "write failed") {
		t.Fatalf("error = %q, want write failed", r.Error)
	}
}

// ── Project Send ──

func TestMgmt_ProjectSend_EmptyMessage(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")
	r := mgmtPost(t, ts.URL+"/api/v1/projects/test-project/send", "tok", map[string]string{
		"session_key": "user1",
		"message":     "",
	})
	if r.OK {
		t.Fatal("expected error for empty message")
	}
	if !strings.Contains(r.Error, "message is required") {
		t.Fatalf("error = %q, want message required", r.Error)
	}
}

func TestMgmt_ProjectSend_MethodNotAllowed(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")
	r := mgmtGet(t, ts.URL+"/api/v1/projects/test-project/send", "tok")
	if r.OK {
		t.Fatal("expected GET on send to fail")
	}
}

// ── Project Providers ──

func TestMgmt_ProjectProviders_NoProviderSwitcher(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")
	r := mgmtGet(t, ts.URL+"/api/v1/projects/test-project/providers", "tok")
	if r.OK {
		t.Fatal("expected error when agent doesn't support ProviderSwitcher")
	}
	if !strings.Contains(r.Error, "provider switching") {
		t.Fatalf("error = %q", r.Error)
	}
}

func TestMgmt_ProjectProviders_ListAndAdd(t *testing.T) {
	agent := &stubProviderAgent{
		providers: []ProviderConfig{{Name: "openai", BaseURL: "https://api.openai.com"}},
		active:    "openai",
	}
	e := NewEngine("proj", agent, nil, "", LangEnglish)

	mgmt := NewManagementServer(0, "tok", nil)
	mgmt.RegisterEngine("proj", e)
	mux := http.NewServeMux()
	mux.HandleFunc("/api/v1/projects/", mgmt.wrap(mgmt.handleProjectRoutes))
	ts := httptest.NewServer(mux)
	defer ts.Close()

	// GET list
	r := mgmtGet(t, ts.URL+"/api/v1/projects/proj/providers", "tok")
	if !r.OK {
		t.Fatalf("list providers failed: %s", r.Error)
	}
	var list struct {
		Providers      []map[string]any `json:"providers"`
		ActiveProvider string           `json:"active_provider"`
	}
	if err := json.Unmarshal(r.Data, &list); err != nil {
		t.Fatalf("unmarshal: %v", err)
	}
	if len(list.Providers) != 1 {
		t.Fatalf("providers count = %d, want 1", len(list.Providers))
	}
	if list.ActiveProvider != "openai" {
		t.Fatalf("active = %q, want openai", list.ActiveProvider)
	}

	// POST add
	r = mgmtPost(t, ts.URL+"/api/v1/projects/proj/providers", "tok", map[string]string{
		"name":     "anthropic",
		"api_key":  "sk-test",
		"base_url": "https://api.anthropic.com",
	})
	if !r.OK {
		t.Fatalf("add provider failed: %s", r.Error)
	}
	if len(agent.providers) != 2 {
		t.Fatalf("providers count = %d, want 2", len(agent.providers))
	}
}

func TestMgmt_ProjectProviders_AddMissingName(t *testing.T) {
	agent := &stubProviderAgent{
		providers: []ProviderConfig{{Name: "openai"}},
	}
	e := NewEngine("proj", agent, nil, "", LangEnglish)
	mgmt := NewManagementServer(0, "tok", nil)
	mgmt.RegisterEngine("proj", e)
	mux := http.NewServeMux()
	mux.HandleFunc("/api/v1/projects/", mgmt.wrap(mgmt.handleProjectRoutes))
	ts := httptest.NewServer(mux)
	defer ts.Close()

	r := mgmtPost(t, ts.URL+"/api/v1/projects/proj/providers", "tok", map[string]string{"api_key": "sk"})
	if r.OK {
		t.Fatal("expected error for missing name")
	}
}

func TestMgmt_ProjectProviders_Activate(t *testing.T) {
	agent := &stubProviderAgent{
		providers: []ProviderConfig{
			{Name: "openai", BaseURL: "https://openai.com"},
			{Name: "claude", BaseURL: "https://claude.ai"},
		},
		active: "openai",
	}
	e := NewEngine("proj", agent, nil, "", LangEnglish)
	mgmt := NewManagementServer(0, "tok", nil)
	mgmt.RegisterEngine("proj", e)
	mux := http.NewServeMux()
	mux.HandleFunc("/api/v1/projects/", mgmt.wrap(mgmt.handleProjectRoutes))
	ts := httptest.NewServer(mux)
	defer ts.Close()

	r := mgmtPost(t, ts.URL+"/api/v1/projects/proj/providers/claude/activate", "tok", nil)
	if !r.OK {
		t.Fatalf("activate failed: %s", r.Error)
	}
	if agent.active != "claude" {
		t.Fatalf("active = %q, want claude", agent.active)
	}
}

func TestMgmt_ProjectProviders_ActivateNotFound(t *testing.T) {
	agent := &stubProviderAgent{
		providers: []ProviderConfig{{Name: "openai"}},
	}
	e := NewEngine("proj", agent, nil, "", LangEnglish)
	mgmt := NewManagementServer(0, "tok", nil)
	mgmt.RegisterEngine("proj", e)
	mux := http.NewServeMux()
	mux.HandleFunc("/api/v1/projects/", mgmt.wrap(mgmt.handleProjectRoutes))
	ts := httptest.NewServer(mux)
	defer ts.Close()

	r := mgmtPost(t, ts.URL+"/api/v1/projects/proj/providers/nope/activate", "tok", nil)
	if r.OK {
		t.Fatal("expected 404 for nonexistent provider")
	}
}

func TestMgmt_ProjectProviders_DeleteActive(t *testing.T) {
	agent := &stubProviderAgent{
		providers: []ProviderConfig{
			{Name: "openai"},
			{Name: "claude"},
		},
		active: "openai",
	}
	e := NewEngine("proj", agent, nil, "", LangEnglish)
	mgmt := NewManagementServer(0, "tok", nil)
	mgmt.RegisterEngine("proj", e)
	mux := http.NewServeMux()
	mux.HandleFunc("/api/v1/projects/", mgmt.wrap(mgmt.handleProjectRoutes))
	ts := httptest.NewServer(mux)
	defer ts.Close()

	r := mgmtDelete(t, ts.URL+"/api/v1/projects/proj/providers/openai", "tok")
	if r.OK {
		t.Fatal("expected error when deleting active provider")
	}
	if !strings.Contains(r.Error, "active provider") {
		t.Fatalf("error = %q", r.Error)
	}
}

func TestMgmt_ProjectProviders_DeleteInactive(t *testing.T) {
	agent := &stubProviderAgent{
		providers: []ProviderConfig{
			{Name: "openai"},
			{Name: "claude"},
		},
		active: "openai",
	}
	e := NewEngine("proj", agent, nil, "", LangEnglish)
	mgmt := NewManagementServer(0, "tok", nil)
	mgmt.RegisterEngine("proj", e)
	mux := http.NewServeMux()
	mux.HandleFunc("/api/v1/projects/", mgmt.wrap(mgmt.handleProjectRoutes))
	ts := httptest.NewServer(mux)
	defer ts.Close()

	r := mgmtDelete(t, ts.URL+"/api/v1/projects/proj/providers/claude", "tok")
	if !r.OK {
		t.Fatalf("delete failed: %s", r.Error)
	}
	if len(agent.providers) != 1 {
		t.Fatalf("providers = %d, want 1", len(agent.providers))
	}
}

// ── Project Provider Refs ──

func TestMgmt_ProjectProviderRefs_GetEmpty(t *testing.T) {
	agent := &stubProviderAgent{providers: []ProviderConfig{{Name: "openai"}}}
	e := NewEngine("proj", agent, nil, "", LangEnglish)
	mgmt := NewManagementServer(0, "tok", nil)
	mgmt.RegisterEngine("proj", e)
	mux := http.NewServeMux()
	mux.HandleFunc("/api/v1/projects/", mgmt.wrap(mgmt.handleProjectRoutes))
	ts := httptest.NewServer(mux)
	defer ts.Close()

	r := mgmtGet(t, ts.URL+"/api/v1/projects/proj/provider-refs", "tok")
	if !r.OK {
		t.Fatalf("get provider-refs failed: %s", r.Error)
	}
	var data struct {
		ProviderRefs []string `json:"provider_refs"`
	}
	if err := json.Unmarshal(r.Data, &data); err != nil {
		t.Fatalf("unmarshal: %v", err)
	}
	if len(data.ProviderRefs) != 0 {
		t.Fatalf("expected empty refs, got %v", data.ProviderRefs)
	}
}

func TestMgmt_ProjectProviderRefs_PutNotConfigured(t *testing.T) {
	agent := &stubProviderAgent{providers: []ProviderConfig{{Name: "openai"}}}
	e := NewEngine("proj", agent, nil, "", LangEnglish)
	mgmt := NewManagementServer(0, "tok", nil)
	mgmt.RegisterEngine("proj", e)
	mux := http.NewServeMux()
	mux.HandleFunc("/api/v1/projects/", mgmt.wrap(mgmt.handleProjectRoutes))
	ts := httptest.NewServer(mux)
	defer ts.Close()

	r := mgmtPut(t, ts.URL+"/api/v1/projects/proj/provider-refs", "tok", map[string]any{
		"provider_refs": []string{"shared-1"},
	})
	if r.OK {
		t.Fatal("expected error when saveProviderRefs is nil")
	}
}

// ── Project Users ──

func TestMgmt_ProjectUsers_Get(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")

	r := mgmtGet(t, ts.URL+"/api/v1/projects/test-project/users", "tok")
	if !r.OK {
		t.Fatalf("get users failed: %s", r.Error)
	}
}

func TestMgmt_ProjectUsers_PatchInvalidJSON(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")
	req, _ := http.NewRequest("PATCH", ts.URL+"/api/v1/projects/test-project/users", strings.NewReader("{bad"))
	req.Header.Set("Authorization", "Bearer tok")
	req.Header.Set("Content-Type", "application/json")
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		t.Fatal(err)
	}
	defer resp.Body.Close()
	var r mgmtResponse
	json.NewDecoder(resp.Body).Decode(&r)
	if r.OK {
		t.Fatal("expected error for invalid JSON")
	}
}

// ── Project Delete ──

func TestMgmt_ProjectDelete_NotConfigured(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")
	r := mgmtDelete(t, ts.URL+"/api/v1/projects/test-project", "tok")
	if r.OK {
		t.Fatal("expected error when removeProject is nil")
	}
	if !strings.Contains(r.Error, "not configured") {
		t.Fatalf("error = %q", r.Error)
	}
}

// ── Global Providers ──

func TestMgmt_GlobalProviders_GetEmpty(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")
	r := mgmtGet(t, ts.URL+"/api/v1/providers", "tok")
	if !r.OK {
		t.Fatalf("get global providers failed: %s", r.Error)
	}
	var data struct {
		Providers []any `json:"providers"`
	}
	if err := json.Unmarshal(r.Data, &data); err != nil {
		t.Fatalf("unmarshal: %v", err)
	}
	if len(data.Providers) != 0 {
		t.Fatalf("expected empty, got %d", len(data.Providers))
	}
}

func TestMgmt_GlobalProviders_GetWithFunc(t *testing.T) {
	mgmt, ts, _ := testManagementServer(t, "tok")
	mgmt.SetListGlobalProviders(func() ([]GlobalProviderInfo, error) {
		return []GlobalProviderInfo{{Name: "shared-relay"}}, nil
	})

	r := mgmtGet(t, ts.URL+"/api/v1/providers", "tok")
	if !r.OK {
		t.Fatalf("get providers failed: %s", r.Error)
	}
}

func TestMgmt_GlobalProviders_PostNotConfigured(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")
	r := mgmtPost(t, ts.URL+"/api/v1/providers", "tok", map[string]string{"name": "new"})
	if r.OK {
		t.Fatal("expected error when addGlobalProvider is nil")
	}
}

func TestMgmt_GlobalProviders_PostMissingName(t *testing.T) {
	mgmt, ts, _ := testManagementServer(t, "tok")
	mgmt.SetAddGlobalProvider(func(info GlobalProviderInfo) error { return nil })

	r := mgmtPost(t, ts.URL+"/api/v1/providers", "tok", map[string]string{"api_key": "sk"})
	if r.OK {
		t.Fatal("expected error for missing name")
	}
}

func TestMgmt_GlobalProviders_PostSuccess(t *testing.T) {
	mgmt, ts, _ := testManagementServer(t, "tok")
	var added string
	mgmt.SetAddGlobalProvider(func(info GlobalProviderInfo) error {
		added = info.Name
		return nil
	})

	r := mgmtPost(t, ts.URL+"/api/v1/providers", "tok", map[string]string{"name": "new-relay"})
	if !r.OK {
		t.Fatalf("add global provider failed: %s", r.Error)
	}
	if added != "new-relay" {
		t.Fatalf("added = %q, want new-relay", added)
	}
}

func TestMgmt_GlobalProviders_PostDuplicate(t *testing.T) {
	mgmt, ts, _ := testManagementServer(t, "tok")
	mgmt.SetAddGlobalProvider(func(info GlobalProviderInfo) error {
		return errors.New("already exists: " + info.Name)
	})

	r := mgmtPost(t, ts.URL+"/api/v1/providers", "tok", map[string]string{"name": "dup"})
	if r.OK {
		t.Fatal("expected conflict error")
	}
}

func TestMgmt_GlobalProviders_UpdateNotConfigured(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")
	r := mgmtPut(t, ts.URL+"/api/v1/providers/some-provider", "tok", map[string]string{"name": "some-provider"})
	if r.OK {
		t.Fatal("expected error when updateGlobalProvider is nil")
	}
}

func TestMgmt_GlobalProviders_UpdateSuccess(t *testing.T) {
	mgmt, ts, _ := testManagementServer(t, "tok")
	var updated string
	mgmt.SetUpdateGlobalProvider(func(name string, info GlobalProviderInfo) error {
		updated = name
		return nil
	})

	r := mgmtPut(t, ts.URL+"/api/v1/providers/relay-1", "tok", map[string]string{"model": "gpt-5"})
	if !r.OK {
		t.Fatalf("update global provider failed: %s", r.Error)
	}
	if updated != "relay-1" {
		t.Fatalf("updated = %q, want relay-1", updated)
	}
}

func TestMgmt_GlobalProviders_DeleteNotFound(t *testing.T) {
	mgmt, ts, _ := testManagementServer(t, "tok")
	mgmt.SetRemoveGlobalProvider(func(name string) error {
		return errors.New("not found: " + name)
	})

	r := mgmtDelete(t, ts.URL+"/api/v1/providers/nope", "tok")
	if r.OK {
		t.Fatal("expected 404")
	}
}

// ── Provider Presets ──

func TestMgmt_ProviderPresets_NilFunc(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")
	r := mgmtGet(t, ts.URL+"/api/v1/providers/presets", "tok")
	if !r.OK {
		t.Fatalf("presets failed: %s", r.Error)
	}
}

func TestMgmt_ProviderPresets_WithFunc(t *testing.T) {
	mgmt, ts, _ := testManagementServer(t, "tok")
	mgmt.SetFetchPresets(func() (*ProviderPresetsResponse, error) {
		return &ProviderPresetsResponse{Version: 2}, nil
	})

	r := mgmtGet(t, ts.URL+"/api/v1/providers/presets", "tok")
	if !r.OK {
		t.Fatalf("presets failed: %s", r.Error)
	}
}

func TestMgmt_ProviderPresets_Error(t *testing.T) {
	mgmt, ts, _ := testManagementServer(t, "tok")
	mgmt.SetFetchPresets(func() (*ProviderPresetsResponse, error) {
		return nil, errors.New("network error")
	})

	r := mgmtGet(t, ts.URL+"/api/v1/providers/presets", "tok")
	if r.OK {
		t.Fatal("expected error")
	}
}

// ── Skills ──

func TestMgmt_Skills(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")

	r := mgmtGet(t, ts.URL+"/api/v1/skills", "tok")
	if !r.OK {
		t.Fatalf("skills failed: %s", r.Error)
	}
	var data struct {
		Projects []projectSkills `json:"projects"`
	}
	if err := json.Unmarshal(r.Data, &data); err != nil {
		t.Fatalf("unmarshal: %v", err)
	}
	if len(data.Projects) != 1 {
		t.Fatalf("projects = %d, want 1", len(data.Projects))
	}
}

func TestMgmt_Skills_MethodNotAllowed(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")
	r := mgmtPost(t, ts.URL+"/api/v1/skills", "tok", nil)
	if r.OK {
		t.Fatal("expected POST on skills to fail")
	}
}

func TestMgmt_SkillPresets_NilFunc(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")
	r := mgmtGet(t, ts.URL+"/api/v1/skills/presets", "tok")
	if !r.OK {
		t.Fatalf("skill presets failed: %s", r.Error)
	}
}

func TestMgmt_SkillPresets_WithFunc(t *testing.T) {
	mgmt, ts, _ := testManagementServer(t, "tok")
	mgmt.SetFetchSkillPresets(func() (*SkillPresetsResponse, error) {
		return &SkillPresetsResponse{Version: 1}, nil
	})
	r := mgmtGet(t, ts.URL+"/api/v1/skills/presets", "tok")
	if !r.OK {
		t.Fatalf("skill presets failed: %s", r.Error)
	}
}

func TestMgmt_SkillPresets_Error(t *testing.T) {
	mgmt, ts, _ := testManagementServer(t, "tok")
	mgmt.SetFetchSkillPresets(func() (*SkillPresetsResponse, error) {
		return nil, errors.New("fetch failed")
	})
	r := mgmtGet(t, ts.URL+"/api/v1/skills/presets", "tok")
	if r.OK {
		t.Fatal("expected error")
	}
}

// ── Cron PATCH (update job) ──

func TestMgmt_CronPatch(t *testing.T) {
	mgmt, ts, e := testManagementServer(t, "tok")
	store, err := NewCronStore(t.TempDir())
	if err != nil {
		t.Fatal(err)
	}
	cs := NewCronScheduler(store)
	cs.RegisterEngine("test-project", e)
	mgmt.SetCronScheduler(cs)

	// Add a job
	r := mgmtPost(t, ts.URL+"/api/v1/cron", "tok", map[string]any{
		"project":     "test-project",
		"session_key": "test:chan:user",
		"cron_expr":   "0 9 * * *",
		"prompt":      "hello",
	})
	if !r.OK {
		t.Fatalf("cron add failed: %s", r.Error)
	}
	var job CronJob
	json.Unmarshal(r.Data, &job)

	// Patch it
	r = mgmtPatch(t, ts.URL+"/api/v1/cron/"+job.ID, "tok", map[string]any{
		"enabled": false,
	})
	if !r.OK {
		t.Fatalf("cron patch failed: %s", r.Error)
	}

	var updated CronJob
	json.Unmarshal(r.Data, &updated)
	if updated.Enabled {
		t.Fatal("expected enabled=false after patch")
	}
}

func TestMgmt_CronPatch_NonexistentJob(t *testing.T) {
	mgmt, ts, e := testManagementServer(t, "tok")
	store, err := NewCronStore(t.TempDir())
	if err != nil {
		t.Fatal(err)
	}
	cs := NewCronScheduler(store)
	cs.RegisterEngine("test-project", e)
	mgmt.SetCronScheduler(cs)

	r := mgmtPatch(t, ts.URL+"/api/v1/cron/nonexistent", "tok", map[string]any{"enabled": false})
	if r.OK {
		t.Fatal("expected error for nonexistent cron job")
	}
}

// ── Project routes: unknown sub-path ──

func TestMgmt_ProjectRoutes_UnknownSubpath(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")
	r := mgmtGet(t, ts.URL+"/api/v1/projects/test-project/unknown-route", "tok")
	if r.OK {
		t.Fatal("expected 404 for unknown sub-path")
	}
}

// ── Session create missing session_key ──

func TestMgmt_SessionCreate_MissingKey(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")
	r := mgmtPost(t, ts.URL+"/api/v1/projects/test-project/sessions", "tok", map[string]string{
		"name": "work",
	})
	if r.OK {
		t.Fatal("expected error for missing session_key")
	}
}

// ── Reload failure ──

func TestMgmt_Reload_Failure(t *testing.T) {
	_, ts, e := testManagementServer(t, "tok")
	e.configReloadFunc = func() (*ConfigReloadResult, error) {
		return nil, errors.New("parse error")
	}
	r := mgmtPost(t, ts.URL+"/api/v1/reload", "tok", nil)
	if r.OK {
		t.Fatal("expected reload failure")
	}
	if !strings.Contains(r.Error, "parse error") {
		t.Fatalf("error = %q, want parse error", r.Error)
	}
}

// ── Config PUT (save) ──

func TestMgmt_Config_Save(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")

	tmp := t.TempDir()
	cfgPath := tmp + "/config.toml"
	os.WriteFile(cfgPath, []byte("[display]\ntitle = \"old\"\n"), 0644)

	req, _ := http.NewRequest("PUT", ts.URL+"/api/v1/config", strings.NewReader("# new config\n"))
	req.Header.Set("Authorization", "Bearer tok")
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		t.Fatal(err)
	}
	resp.Body.Close()
	// Without SetConfigFilePath, save should fail
	if resp.StatusCode == 200 {
		t.Fatal("expected error without config file path set")
	}
}

// ── CC-Switch providers ──

func TestMgmt_CCSwitchProviders_NotConfigured(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")
	r := mgmtGet(t, ts.URL+"/api/v1/providers/cc-switch", "tok")
	if !r.OK {
		t.Fatalf("cc-switch get failed: %s", r.Error)
	}
	var data map[string]any
	json.Unmarshal(r.Data, &data)
	if data["available"] != false {
		t.Fatalf("expected available=false, got %v", data["available"])
	}
}

// ────────────────────────────────────────────────────────────────
// Edge cases & boundary tests below
// ────────────────────────────────────────────────────────────────

// ── Restart edge cases ──

func TestMgmt_Restart_AlreadyInProgress(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")

	// Fill the buffered channel (cap=1) so the next restart is rejected.
	RestartCh <- RestartRequest{}

	r := mgmtPost(t, ts.URL+"/api/v1/restart", "tok", nil)
	if r.OK {
		t.Fatal("expected conflict when restart channel is full")
	}
	if !strings.Contains(r.Error, "already in progress") {
		t.Fatalf("error = %q, want 'already in progress'", r.Error)
	}

	// Drain so other tests aren't affected.
	<-RestartCh
}

func TestMgmt_Restart_WithSessionKey(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")

	r := mgmtPost(t, ts.URL+"/api/v1/restart", "tok", map[string]string{
		"session_key": "user1",
		"platform":    "feishu",
	})
	if !r.OK {
		t.Fatalf("restart with session_key failed: %s", r.Error)
	}
	req := <-RestartCh
	if req.SessionKey != "user1" || req.Platform != "feishu" {
		t.Fatalf("restart request = %+v, want session_key=user1 platform=feishu", req)
	}
}

// ── Config edge cases ──

func TestMgmt_Config_NoPathSet(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")
	r := mgmtGet(t, ts.URL+"/api/v1/config", "tok")
	if r.OK {
		t.Fatal("expected error when configFilePath is empty")
	}
	if !strings.Contains(r.Error, "not set") {
		t.Fatalf("error = %q", r.Error)
	}
}

func TestMgmt_Config_FileNotFound(t *testing.T) {
	srv, ts, _ := testManagementServer(t, "tok")
	srv.SetConfigFilePath("/nonexistent/path/config.toml")

	r := mgmtGet(t, ts.URL+"/api/v1/config", "tok")
	if r.OK {
		t.Fatal("expected error for missing config file")
	}
}

func TestMgmt_Config_MethodNotAllowed(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")
	r := mgmtPost(t, ts.URL+"/api/v1/config", "tok", nil)
	if r.OK {
		t.Fatal("expected POST on config to fail")
	}
}

// ── Settings edge cases ──

func TestMgmt_GlobalSettings_PatchInvalidJSON(t *testing.T) {
	mgmt, ts, _ := testManagementServer(t, "tok")
	mgmt.SetSaveGlobalSettings(func(updates map[string]any) error { return nil })

	req, _ := http.NewRequest("PATCH", ts.URL+"/api/v1/settings", strings.NewReader("{bad json"))
	req.Header.Set("Authorization", "Bearer tok")
	req.Header.Set("Content-Type", "application/json")
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		t.Fatal(err)
	}
	defer resp.Body.Close()
	var r mgmtResponse
	json.NewDecoder(resp.Body).Decode(&r)
	if r.OK {
		t.Fatal("expected error for invalid JSON body")
	}
	if !strings.Contains(r.Error, "invalid JSON") {
		t.Fatalf("error = %q, want 'invalid JSON'", r.Error)
	}
}

func TestMgmt_GlobalSettings_PatchNotConfigured(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")
	r := mgmtPatch(t, ts.URL+"/api/v1/settings", "tok", map[string]any{"x": 1})
	if r.OK {
		t.Fatal("expected error when saveGlobalSettings is nil")
	}
}

func TestMgmt_GlobalSettings_MethodNotAllowed(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")
	r := mgmtDelete(t, ts.URL+"/api/v1/settings", "tok")
	if r.OK {
		t.Fatal("expected DELETE on settings to fail")
	}
}

// ── Send edge cases ──

func TestMgmt_ProjectSend_InvalidJSON(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")
	req, _ := http.NewRequest("POST", ts.URL+"/api/v1/projects/test-project/send", strings.NewReader("{bad"))
	req.Header.Set("Authorization", "Bearer tok")
	req.Header.Set("Content-Type", "application/json")
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		t.Fatal(err)
	}
	defer resp.Body.Close()
	var r mgmtResponse
	json.NewDecoder(resp.Body).Decode(&r)
	if r.OK {
		t.Fatal("expected error for invalid JSON")
	}
}

func TestMgmt_ProjectSend_NonexistentProject(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")
	r := mgmtPost(t, ts.URL+"/api/v1/projects/nonexistent/send", "tok", map[string]string{
		"message": "hello",
	})
	if r.OK {
		t.Fatal("expected 404 for send to nonexistent project")
	}
}

// ── Project Detail PATCH edge cases ──

func TestMgmt_ProjectPatch_InvalidJSON(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")
	req, _ := http.NewRequest("PATCH", ts.URL+"/api/v1/projects/test-project", strings.NewReader("{bad"))
	req.Header.Set("Authorization", "Bearer tok")
	req.Header.Set("Content-Type", "application/json")
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		t.Fatal(err)
	}
	defer resp.Body.Close()
	var r mgmtResponse
	json.NewDecoder(resp.Body).Decode(&r)
	if r.OK {
		t.Fatal("expected error for invalid JSON body")
	}
}

func TestMgmt_ProjectPatch_UnknownAgentType(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")
	agentType := "totally-unknown-agent"
	r := mgmtPatch(t, ts.URL+"/api/v1/projects/test-project", "tok", map[string]any{
		"agent_type": agentType,
	})
	if r.OK {
		t.Fatal("expected error for unknown agent type")
	}
	if !strings.Contains(r.Error, "unknown agent type") {
		t.Fatalf("error = %q", r.Error)
	}
}

func TestMgmt_ProjectPatch_DisabledCommands(t *testing.T) {
	_, ts, e := testManagementServer(t, "tok")
	r := mgmtPatch(t, ts.URL+"/api/v1/projects/test-project", "tok", map[string]any{
		"disabled_commands": []string{"new", "delete"},
	})
	if !r.OK {
		t.Fatalf("patch disabled_commands failed: %s", r.Error)
	}
	got := e.GetDisabledCommands()
	sort.Strings(got)
	if len(got) != 2 || got[0] != "delete" || got[1] != "new" {
		t.Fatalf("disabled_commands = %v, want [delete new]", got)
	}
}

func TestMgmt_ProjectPatch_AdminFrom(t *testing.T) {
	_, ts, e := testManagementServer(t, "tok")
	r := mgmtPatch(t, ts.URL+"/api/v1/projects/test-project", "tok", map[string]any{
		"admin_from": "user123",
	})
	if !r.OK {
		t.Fatalf("patch admin_from failed: %s", r.Error)
	}
	e.userRolesMu.RLock()
	got := e.adminFrom
	e.userRolesMu.RUnlock()
	if got != "user123" {
		t.Fatalf("adminFrom = %q, want user123", got)
	}
}

func TestMgmt_ProjectPatch_Language(t *testing.T) {
	_, ts, e := testManagementServer(t, "tok")
	for _, lang := range []string{"zh", "ja", "es", "zh-TW", "en"} {
		r := mgmtPatch(t, ts.URL+"/api/v1/projects/test-project", "tok", map[string]any{
			"language": lang,
		})
		if !r.OK {
			t.Fatalf("patch language=%s failed: %s", lang, r.Error)
		}
	}
	if e.i18n.CurrentLang() != LangEnglish {
		t.Fatalf("final lang = %s, want en", e.i18n.CurrentLang())
	}
}

func TestMgmt_ProjectDetail_MethodNotAllowed(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")
	r := mgmtPost(t, ts.URL+"/api/v1/projects/test-project", "tok", nil)
	if r.OK {
		t.Fatal("expected POST on project detail to fail")
	}
}

// ── Project Delete edge cases ──

func TestMgmt_ProjectDelete_Success(t *testing.T) {
	mgmt, ts, _ := testManagementServer(t, "tok")
	var removed string
	mgmt.SetRemoveProject(func(name string) error {
		removed = name
		return nil
	})
	r := mgmtDelete(t, ts.URL+"/api/v1/projects/test-project", "tok")
	if !r.OK {
		t.Fatalf("delete project failed: %s", r.Error)
	}
	if removed != "test-project" {
		t.Fatalf("removed = %q, want test-project", removed)
	}
	var data map[string]any
	json.Unmarshal(r.Data, &data)
	if data["restart_required"] != true {
		t.Fatal("expected restart_required=true")
	}
}

func TestMgmt_ProjectDelete_Error(t *testing.T) {
	mgmt, ts, _ := testManagementServer(t, "tok")
	mgmt.SetRemoveProject(func(name string) error {
		return errors.New("cannot remove last project")
	})
	r := mgmtDelete(t, ts.URL+"/api/v1/projects/test-project", "tok")
	if r.OK {
		t.Fatal("expected error from removeProject")
	}
	if !strings.Contains(r.Error, "cannot remove last project") {
		t.Fatalf("error = %q", r.Error)
	}
}

// ── Session switch edge cases ──

func TestMgmt_SessionSwitch_Success(t *testing.T) {
	_, ts, e := testManagementServer(t, "tok")

	s1 := e.sessions.GetOrCreateActive("user1")
	s2 := e.sessions.NewSession("user1", "second session")

	r := mgmtPost(t, ts.URL+"/api/v1/projects/test-project/sessions/switch", "tok", map[string]string{
		"session_key": "user1",
		"session_id":  s2.ID,
	})
	if !r.OK {
		t.Fatalf("switch failed: %s", r.Error)
	}
	current := e.sessions.GetOrCreateActive("user1")
	if current.ID == s1.ID {
		t.Fatal("session did not switch")
	}
}

func TestMgmt_SessionSwitch_MissingFields(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")

	r := mgmtPost(t, ts.URL+"/api/v1/projects/test-project/sessions/switch", "tok", map[string]string{
		"session_key": "user1",
	})
	if r.OK {
		t.Fatal("expected error for missing session_id")
	}
	if !strings.Contains(r.Error, "required") {
		t.Fatalf("error = %q", r.Error)
	}
}

func TestMgmt_SessionSwitch_InvalidSessionID(t *testing.T) {
	_, ts, e := testManagementServer(t, "tok")
	e.sessions.GetOrCreateActive("user1")

	r := mgmtPost(t, ts.URL+"/api/v1/projects/test-project/sessions/switch", "tok", map[string]string{
		"session_key": "user1",
		"session_id":  "nonexistent-id",
	})
	if r.OK {
		t.Fatal("expected error for nonexistent session_id")
	}
}

func TestMgmt_SessionSwitch_MethodNotAllowed(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")
	r := mgmtGet(t, ts.URL+"/api/v1/projects/test-project/sessions/switch", "tok")
	if r.OK {
		t.Fatal("expected GET on session switch to fail")
	}
}

func TestMgmt_SessionSwitch_InvalidJSON(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")
	req, _ := http.NewRequest("POST", ts.URL+"/api/v1/projects/test-project/sessions/switch", strings.NewReader("{bad"))
	req.Header.Set("Authorization", "Bearer tok")
	req.Header.Set("Content-Type", "application/json")
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		t.Fatal(err)
	}
	defer resp.Body.Close()
	var r mgmtResponse
	json.NewDecoder(resp.Body).Decode(&r)
	if r.OK {
		t.Fatal("expected error for invalid JSON")
	}
}

// ── Session detail edge cases ──

func TestMgmt_SessionDetail_NotFound(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")
	r := mgmtGet(t, ts.URL+"/api/v1/projects/test-project/sessions/nonexistent-id", "tok")
	if r.OK {
		t.Fatal("expected 404 for nonexistent session")
	}
}

func TestMgmt_SessionDetail_DeleteNotFound(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")
	r := mgmtDelete(t, ts.URL+"/api/v1/projects/test-project/sessions/nonexistent-id", "tok")
	if r.OK {
		t.Fatal("expected 404 for deleting nonexistent session")
	}
}

func TestMgmt_SessionDetail_MethodNotAllowed(t *testing.T) {
	_, ts, e := testManagementServer(t, "tok")
	s := e.sessions.GetOrCreateActive("user1")
	r := mgmtPost(t, ts.URL+"/api/v1/projects/test-project/sessions/"+s.ID, "tok", nil)
	if r.OK {
		t.Fatal("expected POST on session detail to fail")
	}
}

func TestMgmt_SessionCreate_InvalidJSON(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")
	req, _ := http.NewRequest("POST", ts.URL+"/api/v1/projects/test-project/sessions", strings.NewReader("{bad"))
	req.Header.Set("Authorization", "Bearer tok")
	req.Header.Set("Content-Type", "application/json")
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		t.Fatal(err)
	}
	defer resp.Body.Close()
	var r mgmtResponse
	json.NewDecoder(resp.Body).Decode(&r)
	if r.OK {
		t.Fatal("expected error for invalid JSON")
	}
}

func TestMgmt_Sessions_MethodNotAllowed(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")
	r := mgmtDelete(t, ts.URL+"/api/v1/projects/test-project/sessions", "tok")
	if r.OK {
		t.Fatal("expected DELETE on sessions list to fail")
	}
}

// ── Provider edge cases ──

func TestMgmt_ProjectProviders_PostInvalidJSON(t *testing.T) {
	agent := &stubProviderAgent{providers: []ProviderConfig{{Name: "a"}}}
	e := NewEngine("proj", agent, nil, "", LangEnglish)
	mgmt := NewManagementServer(0, "tok", nil)
	mgmt.RegisterEngine("proj", e)
	mux := http.NewServeMux()
	mux.HandleFunc("/api/v1/projects/", mgmt.wrap(mgmt.handleProjectRoutes))
	ts := httptest.NewServer(mux)
	defer ts.Close()

	req, _ := http.NewRequest("POST", ts.URL+"/api/v1/projects/proj/providers", strings.NewReader("{bad"))
	req.Header.Set("Authorization", "Bearer tok")
	req.Header.Set("Content-Type", "application/json")
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		t.Fatal(err)
	}
	defer resp.Body.Close()
	var r mgmtResponse
	json.NewDecoder(resp.Body).Decode(&r)
	if r.OK {
		t.Fatal("expected error for invalid JSON")
	}
}

func TestMgmt_ProjectProviders_DeleteNotFound(t *testing.T) {
	agent := &stubProviderAgent{
		providers: []ProviderConfig{{Name: "openai"}},
		active:    "openai",
	}
	e := NewEngine("proj", agent, nil, "", LangEnglish)
	mgmt := NewManagementServer(0, "tok", nil)
	mgmt.RegisterEngine("proj", e)
	mux := http.NewServeMux()
	mux.HandleFunc("/api/v1/projects/", mgmt.wrap(mgmt.handleProjectRoutes))
	ts := httptest.NewServer(mux)
	defer ts.Close()

	r := mgmtDelete(t, ts.URL+"/api/v1/projects/proj/providers/nonexistent", "tok")
	if r.OK {
		t.Fatal("expected 404 for nonexistent provider")
	}
}

func TestMgmt_ProjectProviders_MethodNotAllowed(t *testing.T) {
	agent := &stubProviderAgent{providers: []ProviderConfig{{Name: "a"}}}
	e := NewEngine("proj", agent, nil, "", LangEnglish)
	mgmt := NewManagementServer(0, "tok", nil)
	mgmt.RegisterEngine("proj", e)
	mux := http.NewServeMux()
	mux.HandleFunc("/api/v1/projects/", mgmt.wrap(mgmt.handleProjectRoutes))
	ts := httptest.NewServer(mux)
	defer ts.Close()

	r := mgmtPut(t, ts.URL+"/api/v1/projects/proj/providers", "tok", nil)
	if r.OK {
		t.Fatal("expected PUT on providers list to fail")
	}
}

// ── Provider Refs edge cases ──

func TestMgmt_ProjectProviderRefs_PutInvalidJSON(t *testing.T) {
	agent := &stubProviderAgent{providers: []ProviderConfig{{Name: "a"}}}
	e := NewEngine("proj", agent, nil, "", LangEnglish)
	mgmt := NewManagementServer(0, "tok", nil)
	mgmt.RegisterEngine("proj", e)
	mgmt.SetSaveProviderRefs(func(proj string, refs []string) error { return nil })
	mux := http.NewServeMux()
	mux.HandleFunc("/api/v1/projects/", mgmt.wrap(mgmt.handleProjectRoutes))
	ts := httptest.NewServer(mux)
	defer ts.Close()

	req, _ := http.NewRequest("PUT", ts.URL+"/api/v1/projects/proj/provider-refs", strings.NewReader("{bad"))
	req.Header.Set("Authorization", "Bearer tok")
	req.Header.Set("Content-Type", "application/json")
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		t.Fatal(err)
	}
	defer resp.Body.Close()
	var r mgmtResponse
	json.NewDecoder(resp.Body).Decode(&r)
	if r.OK {
		t.Fatal("expected error for invalid JSON")
	}
}

func TestMgmt_ProjectProviderRefs_PutSaveError(t *testing.T) {
	agent := &stubProviderAgent{providers: []ProviderConfig{{Name: "a"}}}
	e := NewEngine("proj", agent, nil, "", LangEnglish)
	mgmt := NewManagementServer(0, "tok", nil)
	mgmt.RegisterEngine("proj", e)
	mgmt.SetSaveProviderRefs(func(proj string, refs []string) error {
		return errors.New("disk full")
	})
	mux := http.NewServeMux()
	mux.HandleFunc("/api/v1/projects/", mgmt.wrap(mgmt.handleProjectRoutes))
	ts := httptest.NewServer(mux)
	defer ts.Close()

	r := mgmtPut(t, ts.URL+"/api/v1/projects/proj/provider-refs", "tok", map[string]any{
		"provider_refs": []string{"shared"},
	})
	if r.OK {
		t.Fatal("expected error from save")
	}
	if !strings.Contains(r.Error, "disk full") {
		t.Fatalf("error = %q", r.Error)
	}
}

func TestMgmt_ProjectProviderRefs_PutSuccess(t *testing.T) {
	agent := &stubProviderAgent{providers: []ProviderConfig{{Name: "a"}}}
	e := NewEngine("proj", agent, nil, "", LangEnglish)
	mgmt := NewManagementServer(0, "tok", nil)
	mgmt.RegisterEngine("proj", e)
	var savedRefs []string
	mgmt.SetSaveProviderRefs(func(proj string, refs []string) error {
		savedRefs = refs
		return nil
	})
	mux := http.NewServeMux()
	mux.HandleFunc("/api/v1/projects/", mgmt.wrap(mgmt.handleProjectRoutes))
	ts := httptest.NewServer(mux)
	defer ts.Close()

	r := mgmtPut(t, ts.URL+"/api/v1/projects/proj/provider-refs", "tok", map[string]any{
		"provider_refs": []string{"shared-1", "shared-2"},
	})
	if !r.OK {
		t.Fatalf("put provider-refs failed: %s", r.Error)
	}
	if len(savedRefs) != 2 {
		t.Fatalf("savedRefs = %v", savedRefs)
	}
}

func TestMgmt_ProjectProviderRefs_MethodNotAllowed(t *testing.T) {
	agent := &stubProviderAgent{providers: []ProviderConfig{{Name: "a"}}}
	e := NewEngine("proj", agent, nil, "", LangEnglish)
	mgmt := NewManagementServer(0, "tok", nil)
	mgmt.RegisterEngine("proj", e)
	mux := http.NewServeMux()
	mux.HandleFunc("/api/v1/projects/", mgmt.wrap(mgmt.handleProjectRoutes))
	ts := httptest.NewServer(mux)
	defer ts.Close()

	r := mgmtDelete(t, ts.URL+"/api/v1/projects/proj/provider-refs", "tok")
	if r.OK {
		t.Fatal("expected DELETE on provider-refs to fail")
	}
}

// ── Users edge cases ──

func TestMgmt_ProjectUsers_PatchValid(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")
	r := mgmtPatch(t, ts.URL+"/api/v1/projects/test-project/users", "tok", map[string]any{
		"default_role": "member",
		"roles": map[string]any{
			"member": map[string]any{
				"user_ids": []string{"uid-member-1"},
			},
			"admin": map[string]any{
				"user_ids": []string{"uid-admin"},
			},
		},
	})
	if !r.OK {
		t.Fatalf("patch users failed: %s", r.Error)
	}
}

func TestMgmt_ProjectUsers_PatchInvalidRoleConfig(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")
	r := mgmtPatch(t, ts.URL+"/api/v1/projects/test-project/users", "tok", map[string]any{
		"default_role": "nonexistent",
		"roles": map[string]any{
			"admin": map[string]any{
				"user_ids": []string{"uid-admin"},
			},
		},
	})
	if r.OK {
		t.Fatal("expected error when default_role doesn't match any defined role")
	}
	if !strings.Contains(r.Error, "invalid users config") {
		t.Fatalf("error = %q", r.Error)
	}
}

func TestMgmt_ProjectUsers_MethodNotAllowed(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")
	r := mgmtDelete(t, ts.URL+"/api/v1/projects/test-project/users", "tok")
	if r.OK {
		t.Fatal("expected DELETE on users to fail")
	}
}

// ── Global Providers edge cases ──

func TestMgmt_GlobalProviders_GetError(t *testing.T) {
	mgmt, ts, _ := testManagementServer(t, "tok")
	mgmt.SetListGlobalProviders(func() ([]GlobalProviderInfo, error) {
		return nil, errors.New("db connection lost")
	})
	r := mgmtGet(t, ts.URL+"/api/v1/providers", "tok")
	if r.OK {
		t.Fatal("expected error from list")
	}
	if !strings.Contains(r.Error, "db connection lost") {
		t.Fatalf("error = %q", r.Error)
	}
}

func TestMgmt_GlobalProviders_PostInvalidJSON(t *testing.T) {
	mgmt, ts, _ := testManagementServer(t, "tok")
	mgmt.SetAddGlobalProvider(func(info GlobalProviderInfo) error { return nil })

	req, _ := http.NewRequest("POST", ts.URL+"/api/v1/providers", strings.NewReader("{bad"))
	req.Header.Set("Authorization", "Bearer tok")
	req.Header.Set("Content-Type", "application/json")
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		t.Fatal(err)
	}
	defer resp.Body.Close()
	var r mgmtResponse
	json.NewDecoder(resp.Body).Decode(&r)
	if r.OK {
		t.Fatal("expected error for invalid JSON")
	}
}

func TestMgmt_GlobalProviders_MethodNotAllowed(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")
	r := mgmtDelete(t, ts.URL+"/api/v1/providers", "tok")
	if r.OK {
		t.Fatal("expected DELETE on /providers to fail")
	}
}

func TestMgmt_GlobalProviders_UpdateNotFound(t *testing.T) {
	mgmt, ts, _ := testManagementServer(t, "tok")
	mgmt.SetUpdateGlobalProvider(func(name string, info GlobalProviderInfo) error {
		return errors.New("not found: " + name)
	})
	r := mgmtPut(t, ts.URL+"/api/v1/providers/nope", "tok", map[string]string{"model": "x"})
	if r.OK {
		t.Fatal("expected 404")
	}
}

func TestMgmt_GlobalProviders_UpdateInvalidJSON(t *testing.T) {
	mgmt, ts, _ := testManagementServer(t, "tok")
	mgmt.SetUpdateGlobalProvider(func(name string, info GlobalProviderInfo) error { return nil })

	req, _ := http.NewRequest("PUT", ts.URL+"/api/v1/providers/test", strings.NewReader("{bad"))
	req.Header.Set("Authorization", "Bearer tok")
	req.Header.Set("Content-Type", "application/json")
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		t.Fatal(err)
	}
	defer resp.Body.Close()
	var r mgmtResponse
	json.NewDecoder(resp.Body).Decode(&r)
	if r.OK {
		t.Fatal("expected error for invalid JSON")
	}
}

func TestMgmt_GlobalProviders_DeleteNotConfigured(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")
	r := mgmtDelete(t, ts.URL+"/api/v1/providers/anything", "tok")
	if r.OK {
		t.Fatal("expected error when removeGlobalProvider is nil")
	}
}

func TestMgmt_GlobalProviders_DeleteSuccess(t *testing.T) {
	mgmt, ts, _ := testManagementServer(t, "tok")
	var deleted string
	mgmt.SetRemoveGlobalProvider(func(name string) error {
		deleted = name
		return nil
	})
	r := mgmtDelete(t, ts.URL+"/api/v1/providers/relay-old", "tok")
	if !r.OK {
		t.Fatalf("delete global provider failed: %s", r.Error)
	}
	if deleted != "relay-old" {
		t.Fatalf("deleted = %q, want relay-old", deleted)
	}
}

func TestMgmt_GlobalProviders_RouteMethodNotAllowed(t *testing.T) {
	mgmt, ts, _ := testManagementServer(t, "tok")
	mgmt.SetUpdateGlobalProvider(func(name string, info GlobalProviderInfo) error { return nil })

	r := mgmtPost(t, ts.URL+"/api/v1/providers/test-prov", "tok", nil)
	if r.OK {
		t.Fatal("expected POST on /providers/{name} to fail")
	}
}

// ── Heartbeat edge cases ──

func TestMgmt_Heartbeat_PauseResumeRun(t *testing.T) {
	mgmt, ts, _ := testManagementServer(t, "tok")
	hs := NewHeartbeatScheduler("")
	mgmt.SetHeartbeatScheduler(hs)

	// pause/resume/run on unconfigured project → 404
	for _, action := range []string{"pause", "resume", "run"} {
		r := mgmtPost(t, ts.URL+"/api/v1/projects/test-project/heartbeat/"+action, "tok", nil)
		if r.OK {
			t.Fatalf("expected 404 for heartbeat %s on unconfigured project", action)
		}
	}
}

func TestMgmt_Heartbeat_IntervalTooSmall(t *testing.T) {
	mgmt, ts, _ := testManagementServer(t, "tok")
	hs := NewHeartbeatScheduler("")
	mgmt.SetHeartbeatScheduler(hs)

	r := mgmtPost(t, ts.URL+"/api/v1/projects/test-project/heartbeat/interval", "tok",
		map[string]any{"minutes": 0})
	if r.OK {
		t.Fatal("expected error for minutes < 1")
	}
	if !strings.Contains(r.Error, "minutes must be >= 1") {
		t.Fatalf("error = %q", r.Error)
	}
}

func TestMgmt_Heartbeat_IntervalInvalidJSON(t *testing.T) {
	mgmt, ts, _ := testManagementServer(t, "tok")
	hs := NewHeartbeatScheduler("")
	mgmt.SetHeartbeatScheduler(hs)

	req, _ := http.NewRequest("POST", ts.URL+"/api/v1/projects/test-project/heartbeat/interval", strings.NewReader("{bad"))
	req.Header.Set("Authorization", "Bearer tok")
	req.Header.Set("Content-Type", "application/json")
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		t.Fatal(err)
	}
	defer resp.Body.Close()
	var r mgmtResponse
	json.NewDecoder(resp.Body).Decode(&r)
	if r.OK {
		t.Fatal("expected error for invalid JSON")
	}
}

func TestMgmt_Heartbeat_UnknownAction(t *testing.T) {
	mgmt, ts, _ := testManagementServer(t, "tok")
	hs := NewHeartbeatScheduler("")
	mgmt.SetHeartbeatScheduler(hs)

	r := mgmtGet(t, ts.URL+"/api/v1/projects/test-project/heartbeat/unknown", "tok")
	if r.OK {
		t.Fatal("expected 404 for unknown heartbeat action")
	}
}

func TestMgmt_Heartbeat_MethodNotAllowed(t *testing.T) {
	mgmt, ts, _ := testManagementServer(t, "tok")
	hs := NewHeartbeatScheduler("")
	mgmt.SetHeartbeatScheduler(hs)

	r := mgmtGet(t, ts.URL+"/api/v1/projects/test-project/heartbeat/pause", "tok")
	if r.OK {
		t.Fatal("expected GET on pause to fail")
	}
}

// ── Cron edge cases ──

func TestMgmt_Cron_PostMissingCronExpr(t *testing.T) {
	mgmt, ts, e := testManagementServer(t, "tok")
	store, _ := NewCronStore(t.TempDir())
	cs := NewCronScheduler(store)
	cs.RegisterEngine("test-project", e)
	mgmt.SetCronScheduler(cs)

	r := mgmtPost(t, ts.URL+"/api/v1/cron", "tok", map[string]any{
		"project": "test-project",
		"prompt":  "hello",
	})
	if r.OK {
		t.Fatal("expected error for missing cron_expr")
	}
	if !strings.Contains(r.Error, "cron_expr is required") {
		t.Fatalf("error = %q", r.Error)
	}
}

func TestMgmt_Cron_PostMissingPromptAndExec(t *testing.T) {
	mgmt, ts, e := testManagementServer(t, "tok")
	store, _ := NewCronStore(t.TempDir())
	cs := NewCronScheduler(store)
	cs.RegisterEngine("test-project", e)
	mgmt.SetCronScheduler(cs)

	r := mgmtPost(t, ts.URL+"/api/v1/cron", "tok", map[string]any{
		"project":   "test-project",
		"cron_expr": "0 9 * * *",
	})
	if r.OK {
		t.Fatal("expected error for missing prompt and exec")
	}
}

func TestMgmt_Cron_PostPromptAndExecMutuallyExclusive(t *testing.T) {
	mgmt, ts, e := testManagementServer(t, "tok")
	store, _ := NewCronStore(t.TempDir())
	cs := NewCronScheduler(store)
	cs.RegisterEngine("test-project", e)
	mgmt.SetCronScheduler(cs)

	r := mgmtPost(t, ts.URL+"/api/v1/cron", "tok", map[string]any{
		"project":   "test-project",
		"cron_expr": "0 9 * * *",
		"prompt":    "hello",
		"exec":      "ls -la",
	})
	if r.OK {
		t.Fatal("expected error when both prompt and exec are set")
	}
	if !strings.Contains(r.Error, "mutually exclusive") {
		t.Fatalf("error = %q", r.Error)
	}
}

func TestMgmt_Cron_PostInvalidJSON(t *testing.T) {
	mgmt, ts, e := testManagementServer(t, "tok")
	store, _ := NewCronStore(t.TempDir())
	cs := NewCronScheduler(store)
	cs.RegisterEngine("test-project", e)
	mgmt.SetCronScheduler(cs)

	req, _ := http.NewRequest("POST", ts.URL+"/api/v1/cron", strings.NewReader("{bad"))
	req.Header.Set("Authorization", "Bearer tok")
	req.Header.Set("Content-Type", "application/json")
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		t.Fatal(err)
	}
	defer resp.Body.Close()
	var r mgmtResponse
	json.NewDecoder(resp.Body).Decode(&r)
	if r.OK {
		t.Fatal("expected error for invalid JSON")
	}
}

func TestMgmt_Cron_MethodNotAllowed(t *testing.T) {
	mgmt, ts, e := testManagementServer(t, "tok")
	store, _ := NewCronStore(t.TempDir())
	cs := NewCronScheduler(store)
	cs.RegisterEngine("test-project", e)
	mgmt.SetCronScheduler(cs)

	r := mgmtDelete(t, ts.URL+"/api/v1/cron", "tok")
	if r.OK {
		t.Fatal("expected DELETE on /cron to fail")
	}
}

func TestMgmt_CronByID_MethodNotAllowed(t *testing.T) {
	mgmt, ts, e := testManagementServer(t, "tok")
	store, _ := NewCronStore(t.TempDir())
	cs := NewCronScheduler(store)
	cs.RegisterEngine("test-project", e)
	mgmt.SetCronScheduler(cs)

	r := mgmtGet(t, ts.URL+"/api/v1/cron/some-id", "tok")
	if r.OK {
		t.Fatal("expected GET on /cron/{id} to fail")
	}
}

func TestMgmt_CronPatch_InvalidJSON(t *testing.T) {
	mgmt, ts, e := testManagementServer(t, "tok")
	store, _ := NewCronStore(t.TempDir())
	cs := NewCronScheduler(store)
	cs.RegisterEngine("test-project", e)
	mgmt.SetCronScheduler(cs)

	req, _ := http.NewRequest("PATCH", ts.URL+"/api/v1/cron/some-id", strings.NewReader("{bad"))
	req.Header.Set("Authorization", "Bearer tok")
	req.Header.Set("Content-Type", "application/json")
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		t.Fatal(err)
	}
	defer resp.Body.Close()
	var r mgmtResponse
	json.NewDecoder(resp.Body).Decode(&r)
	if r.OK {
		t.Fatal("expected error for invalid JSON")
	}
}

func TestMgmt_CronByID_EmptyID(t *testing.T) {
	mgmt, ts, e := testManagementServer(t, "tok")
	store, _ := NewCronStore(t.TempDir())
	cs := NewCronScheduler(store)
	cs.RegisterEngine("test-project", e)
	mgmt.SetCronScheduler(cs)

	r := mgmtDelete(t, ts.URL+"/api/v1/cron/", "tok")
	if r.OK {
		t.Fatal("expected error for empty cron id")
	}
}

// ── Project routes: empty project name ──

func TestMgmt_ProjectRoutes_EmptyProjectName(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")
	r := mgmtGet(t, ts.URL+"/api/v1/projects/", "tok")
	// /projects/ with empty trailing slash is dispatched to handleProjectRoutes
	// which returns "project name required" error.
	if r.OK {
		t.Fatal("expected error for empty project name in project routes")
	}
}

// ── Reload edge cases ──

func TestMgmt_Reload_MethodNotAllowed(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")
	r := mgmtGet(t, ts.URL+"/api/v1/reload", "tok")
	if r.OK {
		t.Fatal("expected GET on reload to fail")
	}
}

func TestMgmt_Reload_NoReloadFunc(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")
	r := mgmtPost(t, ts.URL+"/api/v1/reload", "tok", nil)
	if !r.OK {
		t.Fatalf("reload with nil reloadFunc should succeed: %s", r.Error)
	}
}

// ── CC-Switch edge cases ──

func TestMgmt_CCSwitchProviders_PostNotConfigured(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")
	r := mgmtPost(t, ts.URL+"/api/v1/providers/cc-switch", "tok", map[string]any{
		"names": []string{"relay-1"},
	})
	if r.OK {
		t.Fatal("expected error when cc-switch not configured")
	}
}

func TestMgmt_CCSwitchProviders_PostMissingNames(t *testing.T) {
	mgmt, ts, _ := testManagementServer(t, "tok")
	mgmt.SetListCCSwitchProviders(func() ([]CCSwitchProviderInfo, error) {
		return nil, nil
	})
	mgmt.SetAddGlobalProvider(func(info GlobalProviderInfo) error { return nil })

	r := mgmtPost(t, ts.URL+"/api/v1/providers/cc-switch", "tok", map[string]any{
		"names": []string{},
	})
	if r.OK {
		t.Fatal("expected error for empty names")
	}
	if !strings.Contains(r.Error, "names is required") {
		t.Fatalf("error = %q", r.Error)
	}
}

func TestMgmt_CCSwitchProviders_MethodNotAllowed(t *testing.T) {
	_, ts, _ := testManagementServer(t, "tok")
	r := mgmtDelete(t, ts.URL+"/api/v1/providers/cc-switch", "tok")
	if r.OK {
		t.Fatal("expected DELETE on cc-switch to fail")
	}
}

// TestMgmt_SetupWeixinPoll_RejectsMalformedAPIURL is a regression test for a
// nil-pointer panic in handleSetupWeixinPoll. The handler did
// `u, _ := url.Parse(apiBase + "/")` and then immediately called
// `u.JoinPath(...)`. For inputs like "://" or "%zz", url.Parse returns a nil
// URL plus an error; the discarded error meant the next line crashed the
// management server with `runtime error: invalid memory address or nil
// pointer dereference`. handleSetupWeixinBegin already validated this same
// field; this test pins the symmetric handling here.
func TestMgmt_SetupWeixinPoll_RejectsMalformedAPIURL(t *testing.T) {
	mgmt := NewManagementServer(0, "", nil)

	for _, bad := range []string{"://", "://malformed", "%zz"} {
		body := map[string]any{
			"qr_key":  "abc",
			"api_url": bad,
		}
		buf := new(bytes.Buffer)
		if err := json.NewEncoder(buf).Encode(body); err != nil {
			t.Fatalf("encode body: %v", err)
		}
		req := httptest.NewRequest(http.MethodPost, "/api/v1/setup/weixin/poll", buf)
		req.Header.Set("Content-Type", "application/json")
		w := httptest.NewRecorder()

		// Recover any panic so the test reports a meaningful failure rather
		// than crashing the test binary, then assert the handler returned a
		// 4xx (not 5xx and not a panic) for the bad input.
		func() {
			defer func() {
				if r := recover(); r != nil {
					t.Fatalf("handleSetupWeixinPoll panicked on api_url=%q: %v", bad, r)
				}
			}()
			mgmt.handleSetupWeixinPoll(w, req)
		}()

		if w.Code != http.StatusBadRequest {
			t.Errorf("api_url=%q: status=%d, want %d (body=%s)", bad, w.Code, http.StatusBadRequest, w.Body.String())
		}
	}
}
