package app

import (
	"bytes"
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"io"
	"net"
	"net/http"
	"net/http/httptest"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"sub2api-cn-relay-manager/internal/host/sub2api"
	"sub2api-cn-relay-manager/internal/pack"
	"sub2api-cn-relay-manager/internal/provision"
	"sub2api-cn-relay-manager/internal/reconcile"
	"sub2api-cn-relay-manager/internal/store/sqlite"
	"sub2api-cn-relay-manager/internal/testutil"
)

func TestServeExposesHealthz(t *testing.T) {
	server := NewServer("127.0.0.1:0", NewAPIHandler("admin-token", ActionSet{}), nil)
	listener, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatalf("net.Listen() error = %v", err)
	}

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	errCh := make(chan error, 1)
	go func() {
		errCh <- server.Serve(ctx, listener)
	}()

	response := waitForHealthz(t, "http://"+listener.Addr().String()+"/healthz")
	defer response.Body.Close()

	body, err := io.ReadAll(response.Body)
	if err != nil {
		t.Fatalf("ReadAll() error = %v", err)
	}

	if string(body) != "ok" {
		t.Fatalf("healthz body = %q, want %q", string(body), "ok")
	}

	cancel()

	if err := <-errCh; err != nil {
		t.Fatalf("Serve() error = %v, want nil", err)
	}
}

func TestRunReturnsAfterContextCancellation(t *testing.T) {
	listener, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatalf("net.Listen() error = %v", err)
	}

	server := NewServer("127.0.0.1:0", NewAPIHandler("admin-token", ActionSet{}), func(string, string) (net.Listener, error) {
		return listener, nil
	})

	ctx, cancel := context.WithCancel(context.Background())
	errCh := make(chan error, 1)
	go func() {
		errCh <- server.Run(ctx)
	}()

	response := waitForHealthz(t, "http://"+listener.Addr().String()+"/healthz")
	response.Body.Close()

	cancel()

	select {
	case err := <-errCh:
		if err != nil {
			t.Fatalf("Run() error = %v, want nil", err)
		}
	case <-time.After(2 * time.Second):
		t.Fatal("Run() did not return after context cancellation")
	}
}

func TestRunReturnsListenError(t *testing.T) {
	wantErr := errors.New("listen failed")
	server := NewServer("127.0.0.1:0", NewAPIHandler("admin-token", ActionSet{}), func(string, string) (net.Listener, error) {
		return nil, wantErr
	})

	err := server.Run(context.Background())
	if !errors.Is(err, wantErr) {
		t.Fatalf("Run() error = %v, want %v", err, wantErr)
	}
}

func TestServeReturnsListenerError(t *testing.T) {
	server := NewServer("127.0.0.1:0", NewAPIHandler("admin-token", ActionSet{}), nil)
	listener, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatalf("net.Listen() error = %v", err)
	}

	if err := listener.Close(); err != nil {
		t.Fatalf("listener.Close() error = %v", err)
	}

	err = server.Serve(context.Background(), listener)
	if err == nil {
		t.Fatal("Serve() error = nil, want listener startup error")
	}
}

func TestAPIRejectsMissingAdminToken(t *testing.T) {
	handler := NewAPIHandler("secret-token", ActionSet{})
	request := httptestRequest(t, http.MethodPost, "/api/packs/install", map[string]any{"host_base_url": "https://sub2api.example.com", "pack_path": "/tmp/pack.zip"}, "")
	response := httptestRecorder(handler, request)
	assertStatusCode(t, response, http.StatusUnauthorized)
	assertJSONContains(t, response.Body().Bytes(), "error.code", "unauthorized")
}

func TestAPIAdminSessionLoginSetsCookieAndAuthorizesSubsequentRequest(t *testing.T) {
	handler := NewAPIHandlerWithAuth(AdminAuthConfig{
		Token:      "secret-token",
		Username:   "admin",
		Password:   "pass-123",
		SessionTTL: 2 * time.Hour,
		Now: func() time.Time {
			return time.Unix(1_717_000_000, 0)
		},
	}, ActionSet{
		ListPacks: func(context.Context) ([]PackInfo, error) {
			return []PackInfo{{PackID: "openai-cn-pack", Version: "1.1.6"}}, nil
		},
	})

	loginRequest := httptestRequest(t, http.MethodPost, "/api/admin/session/login", map[string]any{
		"username": "admin",
		"password": "pass-123",
	}, "")
	loginResponse := httptestRecorder(handler, loginRequest)
	assertStatusCode(t, loginResponse, http.StatusOK)
	assertJSONContains(t, loginResponse.Body().Bytes(), "authenticated", true)
	assertJSONContains(t, loginResponse.Body().Bytes(), "username", "admin")

	cookies := loginResponse.Result().Cookies()
	if len(cookies) != 1 {
		t.Fatalf("login cookies = %d, want 1", len(cookies))
	}
	if cookies[0].Name != adminSessionCookieName {
		t.Fatalf("cookie name = %q, want %q", cookies[0].Name, adminSessionCookieName)
	}
	if !cookies[0].HttpOnly {
		t.Fatal("session cookie HttpOnly = false, want true")
	}

	authorizedRequest := httptestRequest(t, http.MethodGet, "/api/packs", nil, "")
	authorizedRequest.AddCookie(cookies[0])
	authorizedResponse := httptestRecorder(handler, authorizedRequest)
	assertStatusCode(t, authorizedResponse, http.StatusOK)
	if !strings.Contains(authorizedResponse.Body().String(), `"pack_id":"openai-cn-pack"`) {
		t.Fatalf("authorized response = %s, want pack_id openai-cn-pack", authorizedResponse.Body().String())
	}
}

func TestAPIAdminSessionRejectsInvalidPassword(t *testing.T) {
	handler := NewAPIHandlerWithAuth(AdminAuthConfig{
		Token:    "secret-token",
		Username: "admin",
		Password: "pass-123",
	}, ActionSet{})
	request := httptestRequest(t, http.MethodPost, "/api/admin/session/login", map[string]any{
		"username": "admin",
		"password": "wrong",
	}, "")
	response := httptestRecorder(handler, request)
	assertStatusCode(t, response, http.StatusUnauthorized)
	assertJSONContains(t, response.Body().Bytes(), "error.code", "unauthorized")
}

func TestAPIAdminSessionLogoutClearsCookie(t *testing.T) {
	handler := NewAPIHandlerWithAuth(AdminAuthConfig{
		Token:    "secret-token",
		Username: "admin",
		Password: "pass-123",
	}, ActionSet{})
	request := httptestRequest(t, http.MethodPost, "/api/admin/session/logout", nil, "")
	response := httptestRecorder(handler, request)
	assertStatusCode(t, response, http.StatusNoContent)

	cookies := response.Result().Cookies()
	if len(cookies) != 1 {
		t.Fatalf("logout cookies = %d, want 1", len(cookies))
	}
	if cookies[0].Name != adminSessionCookieName {
		t.Fatalf("cookie name = %q, want %q", cookies[0].Name, adminSessionCookieName)
	}
	if cookies[0].MaxAge != -1 {
		t.Fatalf("cookie MaxAge = %d, want -1", cookies[0].MaxAge)
	}
}

func TestAPIAdminSessionMeReportsAuthenticationState(t *testing.T) {
	now := time.Unix(1_717_000_000, 0)
	handler := NewAPIHandlerWithAuth(AdminAuthConfig{
		Token:      "secret-token",
		Username:   "admin",
		Password:   "pass-123",
		SessionTTL: time.Hour,
		Now: func() time.Time {
			return now
		},
	}, ActionSet{})

	request := httptestRequest(t, http.MethodGet, "/api/admin/session", nil, "")
	response := httptestRecorder(handler, request)
	assertStatusCode(t, response, http.StatusOK)
	assertJSONContains(t, response.Body().Bytes(), "authenticated", false)
	assertJSONContains(t, response.Body().Bytes(), "login_enabled", true)

	loginRequest := httptestRequest(t, http.MethodPost, "/api/admin/session/login", map[string]any{
		"username": "admin",
		"password": "pass-123",
	}, "")
	loginResponse := httptestRecorder(handler, loginRequest)
	assertStatusCode(t, loginResponse, http.StatusOK)

	meRequest := httptestRequest(t, http.MethodGet, "/api/admin/session", nil, "")
	for _, cookie := range loginResponse.Result().Cookies() {
		meRequest.AddCookie(cookie)
	}
	meResponse := httptestRecorder(handler, meRequest)
	assertStatusCode(t, meResponse, http.StatusOK)
	assertJSONContains(t, meResponse.Body().Bytes(), "authenticated", true)
	assertJSONContains(t, meResponse.Body().Bytes(), "username", "admin")
}

func TestAPIInstallPackReturnsSummary(t *testing.T) {
	handler := NewAPIHandler("secret-token", ActionSet{
		InstallPack: func(context.Context, InstallPackRequest) (provision.PackInstallResult, error) {
			return provision.PackInstallResult{
				Pack:        sqlite.Pack{PackID: "openai-cn-pack", Version: "1.0.0"},
				HostVersion: "0.1.126",
				Providers:   []sqlite.Provider{{ProviderID: "deepseek", DisplayName: "DeepSeek"}},
			}, nil
		},
	})
	request := httptestRequest(t, http.MethodPost, "/api/packs/install", map[string]any{"host_base_url": "https://sub2api.example.com", "pack_path": "/tmp/openai-pack.zip"}, "secret-token")
	response := httptestRecorder(handler, request)
	assertStatusCode(t, response, http.StatusOK)
	assertJSONContains(t, response.Body().Bytes(), "pack_id", "openai-cn-pack")
	assertJSONContains(t, response.Body().Bytes(), "host_version", "0.1.126")
}

func TestAPIPreviewProviderReturnsSummary(t *testing.T) {
	handler := NewAPIHandler("secret-token", ActionSet{
		PreviewProvider: func(_ context.Context, req PreviewProviderRequest) (provision.PreviewReport, error) {
			if req.ProviderID != "deepseek" {
				t.Fatalf("ProviderID = %q, want deepseek", req.ProviderID)
			}
			return provision.PreviewReport{
				AcceptedKeys: []string{"k1", "k2"},
				Names:        provision.ResourceNames{Group: "g", Channel: "c", Plan: "p"},
				Decisions: map[string]provision.PreviewDecision{
					"group": {Action: provision.PreviewActionCreate},
				},
			}, nil
		},
	})
	request := httptestRequest(t, http.MethodPost, "/api/providers/deepseek/preview-import", map[string]any{"host_base_url": "https://sub2api.example.com", "pack_path": "/tmp/openai-pack.zip", "keys": []string{"k1", "k2"}, "mode": "partial"}, "secret-token")
	response := httptestRecorder(handler, request)
	assertStatusCode(t, response, http.StatusOK)
	assertJSONContains(t, response.Body().Bytes(), "accepted_keys_count", float64(2))
}

func TestAPICreateProviderDraftReturnsCreated(t *testing.T) {
	handler := NewAPIHandler("secret-token", ActionSet{
		CreateProviderDraft: func(_ context.Context, req CreateProviderDraftRequest) (ProviderDraftInfo, error) {
			if req.ProviderID != "openai-zhongzhuan" {
				t.Fatalf("ProviderID = %q, want openai-zhongzhuan", req.ProviderID)
			}
			return ProviderDraftInfo{
				DraftID:        "draft_001",
				PackID:         req.PackID,
				ProviderID:     req.ProviderID,
				DisplayName:    req.DisplayName,
				Platform:       req.Platform,
				SmokeTestModel: req.SmokeTestModel,
				SupportedModels: []string{
					"gpt-5.4",
				},
				Manifest: map[string]any{"provider_id": req.ProviderID},
			}, nil
		},
	})
	request := httptestRequest(t, http.MethodPost, "/api/provider-drafts", map[string]any{
		"pack_id":          "openai-cn-pack",
		"provider_id":      "openai-zhongzhuan",
		"display_name":     "OpenAI 中转",
		"platform":         "openai",
		"smoke_test_model": "gpt-5.4",
		"supported_models": []string{"gpt-5.4"},
	}, "secret-token")
	response := httptestRecorder(handler, request)
	assertStatusCode(t, response, http.StatusCreated)
	assertJSONContains(t, response.Body().Bytes(), "draft.draft_id", "draft_001")
	assertJSONContains(t, response.Body().Bytes(), "draft.provider_id", "openai-zhongzhuan")
}

func TestAPIListProviderDraftsReturnsCollection(t *testing.T) {
	handler := NewAPIHandler("secret-token", ActionSet{
		ListProviderDrafts: func(_ context.Context, req ListProviderDraftsRequest) ([]ProviderDraftInfo, error) {
			if req.PackID != "openai-cn-pack" {
				t.Fatalf("PackID = %q, want openai-cn-pack", req.PackID)
			}
			return []ProviderDraftInfo{{
				DraftID:      "draft_001",
				PackID:       req.PackID,
				ProviderID:   "minimax-53hk",
				DisplayName:  "MiniMax 53HK",
				Platform:     "openai",
				Manifest:     map[string]any{"provider_id": "minimax-53hk"},
				SourceHostID: "remote43-current-host",
			}}, nil
		},
	})
	request := httptestRequest(t, http.MethodGet, "/api/provider-drafts?pack_id=openai-cn-pack", nil, "secret-token")
	response := httptestRecorder(handler, request)
	assertStatusCode(t, response, http.StatusOK)
	var payload map[string]any
	if err := json.Unmarshal(response.Body().Bytes(), &payload); err != nil {
		t.Fatalf("json.Unmarshal() error = %v", err)
	}
	drafts, ok := payload["provider_drafts"].([]any)
	if !ok || len(drafts) != 1 {
		t.Fatalf("provider_drafts = %#v, want one item", payload["provider_drafts"])
	}
	item, ok := drafts[0].(map[string]any)
	if !ok {
		t.Fatalf("draft[0] = %#v, want object", drafts[0])
	}
	if got := item["provider_id"]; got != "minimax-53hk" {
		t.Fatalf("provider_id = %#v, want minimax-53hk", got)
	}
}

func TestAPIGetProviderDraftReturnsItem(t *testing.T) {
	handler := NewAPIHandler("secret-token", ActionSet{
		GetProviderDraft: func(_ context.Context, draftID string) (ProviderDraftInfo, error) {
			if draftID != "draft_001" {
				t.Fatalf("draftID = %q, want draft_001", draftID)
			}
			return ProviderDraftInfo{
				DraftID:      draftID,
				PackID:       "openai-cn-pack",
				ProviderID:   "deepseek-chat-official",
				DisplayName:  "DeepSeek Official",
				Platform:     "openai",
				Manifest:     map[string]any{"provider_id": "deepseek-chat-official"},
				SourceHostID: "remote43-current-host",
			}, nil
		},
	})
	request := httptestRequest(t, http.MethodGet, "/api/provider-drafts/draft_001", nil, "secret-token")
	response := httptestRecorder(handler, request)
	assertStatusCode(t, response, http.StatusOK)
	assertJSONContains(t, response.Body().Bytes(), "draft.provider_id", "deepseek-chat-official")
}

func TestAPIUpdateProviderDraftReturnsUpdatedItem(t *testing.T) {
	handler := NewAPIHandler("secret-token", ActionSet{
		UpdateProviderDraft: func(_ context.Context, req UpdateProviderDraftRequest) (ProviderDraftInfo, error) {
			if req.DraftID != "draft_001" {
				t.Fatalf("DraftID = %q, want draft_001", req.DraftID)
			}
			return ProviderDraftInfo{
				DraftID:      req.DraftID,
				PackID:       req.PackID,
				ProviderID:   req.ProviderID,
				DisplayName:  req.DisplayName,
				Platform:     req.Platform,
				BaseURL:      req.BaseURL,
				Manifest:     map[string]any{"provider_id": req.ProviderID},
				SourceHostID: req.SourceHostID,
			}, nil
		},
	})
	request := httptestRequest(t, http.MethodPut, "/api/provider-drafts/draft_001", map[string]any{
		"pack_id":      "openai-cn-pack",
		"provider_id":  "openai-zhongzhuan",
		"display_name": "OpenAI 中转 Updated",
		"platform":     "openai",
		"base_url":     "https://api.example.com/v1",
	}, "secret-token")
	response := httptestRecorder(handler, request)
	assertStatusCode(t, response, http.StatusOK)
	assertJSONContains(t, response.Body().Bytes(), "draft.display_name", "OpenAI 中转 Updated")
}

func TestAPIDeleteProviderDraftReturnsNoContent(t *testing.T) {
	handler := NewAPIHandler("secret-token", ActionSet{
		DeleteProviderDraft: func(_ context.Context, draftID string) error {
			if draftID != "draft_001" {
				t.Fatalf("draftID = %q, want draft_001", draftID)
			}
			return nil
		},
	})
	request := httptestRequest(t, http.MethodDelete, "/api/provider-drafts/draft_001", nil, "secret-token")
	response := httptestRecorder(handler, request)
	assertStatusCode(t, response, http.StatusNoContent)
}

func TestAPIPublishProviderDraftReturnsSummary(t *testing.T) {
	handler := NewAPIHandler("secret-token", ActionSet{
		PublishProviderDraft: func(_ context.Context, req PublishProviderDraftRequest) (PublishProviderDraftResult, error) {
			if req.DraftID != "draft_001" {
				t.Fatalf("DraftID = %q, want draft_001", req.DraftID)
			}
			return PublishProviderDraftResult{
				DraftID:           req.DraftID,
				PackID:            "openai-cn-pack",
				ProviderID:        "openai-zhongzhuan",
				ProviderPath:      "packs/openai-cn-pack/providers/openai-zhongzhuan.json",
				PackVersionBefore: "1.1.4",
				PackVersionAfter:  "1.1.5",
				PublishMode:       "created",
				CommitMessage:     "feat(pack): publish provider draft openai-zhongzhuan",
				CommitSHA:         "abc1234",
				RepoRoot:          "/srv/sub2api-cn-relay-manager",
			}, nil
		},
	})
	request := httptestRequest(t, http.MethodPost, "/api/provider-drafts/draft_001/publish", map[string]any{
		"commit_message": "feat(pack): publish provider draft openai-zhongzhuan",
	}, "secret-token")
	response := httptestRecorder(handler, request)
	assertStatusCode(t, response, http.StatusOK)
	assertJSONContains(t, response.Body().Bytes(), "publish.provider_id", "openai-zhongzhuan")
	assertJSONContains(t, response.Body().Bytes(), "publish.pack_version_after", "1.1.5")
	assertJSONContains(t, response.Body().Bytes(), "publish.commit_sha", "abc1234")
}

func TestAPIImportProviderReturnsConflictWithBatchStatus(t *testing.T) {
	handler := NewAPIHandler("secret-token", ActionSet{
		ImportProvider: func(context.Context, ImportProviderRequest) (provision.RuntimeImportResult, error) {
			return provision.RuntimeImportResult{
				BatchID: 12,
				Report: provision.ImportReport{
					BatchStatus:    provision.BatchStatusFailed,
					ProviderStatus: provision.ProviderStatusFailed,
					AccessStatus:   provision.AccessStatusBroken,
					Accounts:       []provision.AccountImportResult{{}},
				},
			}, errors.New("strict import failed")
		},
	})
	request := httptestRequest(t, http.MethodPost, "/api/providers/deepseek/import", map[string]any{"host_base_url": "https://sub2api.example.com", "pack_path": "/tmp/openai-pack.zip", "keys": []string{"k1"}, "mode": "strict", "access_mode": "self_service", "access_api_key": "user-key"}, "secret-token")
	response := httptestRecorder(handler, request)
	assertStatusCode(t, response, http.StatusConflict)
	assertJSONContains(t, response.Body().Bytes(), "batch_id", float64(12))
	assertJSONContains(t, response.Body().Bytes(), "batch_status", provision.BatchStatusFailed)
}

func TestAPIBatchDetailReturnsSummary(t *testing.T) {
	handler := NewAPIHandler("secret-token", ActionSet{
		BatchDetail: func(context.Context, BatchDetailRequest) (provision.BatchDetailResult, error) {
			return provision.BatchDetailResult{
				Batch: sqlite.ImportBatch{ID: 7, BatchStatus: "running", AccessStatus: "pending"},
				Items: []sqlite.ImportBatchItem{{ID: 1, KeyFingerprint: "sha256:abc", AccountStatus: "passed"}},
			}, nil
		},
	})
	request := httptestRequest(t, http.MethodGet, "/api/import-batches/7", nil, "secret-token")
	response := httptestRecorder(handler, request)
	assertStatusCode(t, response, http.StatusOK)
	assertJSONContains(t, response.Body().Bytes(), "batch.batch_status", "running")
	assertJSONContains(t, response.Body().Bytes(), "items_count", float64(1))
}

func TestAPIProviderStatusReturnsSummary(t *testing.T) {
	handler := NewAPIHandler("secret-token", ActionSet{
		GetProviderStatus: func(_ context.Context, req ProviderQueryRequest) (provision.ProviderSnapshot, error) {
			if req.ProviderID != "deepseek" {
				t.Fatalf("ProviderID = %q, want deepseek", req.ProviderID)
			}
			if req.PackID != "openai-cn-pack" {
				t.Fatalf("PackID = %q, want openai-cn-pack", req.PackID)
			}
			return provision.ProviderSnapshot{
				Host:                   sqlite.Host{HostID: "host-1", BaseURL: "https://sub2api.example.com", HostVersion: "0.1.126"},
				Pack:                   sqlite.Pack{PackID: "openai-cn-pack", Version: "1.0.0"},
				Provider:               sqlite.Provider{ProviderID: "deepseek", DisplayName: "DeepSeek", Platform: "openai"},
				Batch:                  sqlite.ImportBatch{ID: 7, BatchStatus: provision.BatchStatusSucceeded, AccessStatus: provision.AccessStatusSelfServiceReady, Mode: provision.ImportModeStrict},
				ProviderStatus:         "drifted",
				LatestAccessStatus:     provision.AccessStatusSelfServiceReady,
				LatestReconcileStatus:  "drifted",
				LatestReconcileSummary: map[string]any{"missing_count": 1, "stale_noise_count": 2},
				ManagedResources:       []sqlite.ManagedResource{{}, {}},
				AccessClosures:         []sqlite.AccessClosureRecord{{}},
				ReconcileRuns:          []sqlite.ReconcileRun{{}},
			}, nil
		},
	})
	request := httptestRequest(t, http.MethodGet, "/api/providers/deepseek/status?pack_id=openai-cn-pack", nil, "secret-token")
	response := httptestRecorder(handler, request)
	assertStatusCode(t, response, http.StatusOK)
	assertJSONContains(t, response.Body().Bytes(), "provider_status", "drifted")
	assertJSONContains(t, response.Body().Bytes(), "managed_resources_count", float64(2))
	assertJSONContains(t, response.Body().Bytes(), "latest_reconcile_summary.missing_count", float64(1))
	assertJSONContains(t, response.Body().Bytes(), "latest_reconcile_summary.stale_noise_count", float64(2))
}

func TestAPIProviderAccessStatusReturnsSummary(t *testing.T) {
	handler := NewAPIHandler("secret-token", ActionSet{
		GetProviderAccessStatus: func(_ context.Context, req ProviderQueryRequest) (provision.ProviderSnapshot, error) {
			if req.ProviderID != "deepseek" {
				t.Fatalf("ProviderID = %q, want deepseek", req.ProviderID)
			}
			return provision.ProviderSnapshot{
				Pack:               sqlite.Pack{PackID: "openai-cn-pack"},
				Provider:           sqlite.Provider{ProviderID: "deepseek"},
				Batch:              sqlite.ImportBatch{ID: 7, AccessStatus: provision.AccessStatusSelfServiceReady},
				LatestAccessStatus: provision.AccessStatusSelfServiceReady,
				AccessClosures:     []sqlite.AccessClosureRecord{{ID: 2, ClosureType: provision.AccessModeSelfService, Status: provision.AccessStatusSelfServiceReady, DetailsJSON: `{"ok":true}`}},
			}, nil
		},
	})
	request := httptestRequest(t, http.MethodGet, "/api/providers/deepseek/access/status", nil, "secret-token")
	response := httptestRecorder(handler, request)
	assertStatusCode(t, response, http.StatusOK)
	assertJSONContains(t, response.Body().Bytes(), "latest_access_status", provision.AccessStatusSelfServiceReady)
	assertJSONContains(t, response.Body().Bytes(), "closures_count", float64(1))
	if !strings.Contains(response.Body().String(), `"closure_type":"self_service"`) {
		t.Fatalf("access status payload missing closure type: %s", response.Body().String())
	}
}

func TestAPIListPackProvidersReturnsProviderMetadata(t *testing.T) {
	handler := NewAPIHandler("secret-token", ActionSet{
		ListPackProviders: func(_ context.Context, packID string) ([]PackProviderInfo, error) {
			if packID != "openai-cn-pack" {
				t.Fatalf("packID = %q, want openai-cn-pack", packID)
			}
			return []PackProviderInfo{{
				ProviderID:      "deepseek-chat-official",
				DisplayName:     "DeepSeek Official",
				Platform:        "openai",
				HostOverlays:    0,
				BaseURL:         "https://api.deepseek.com",
				SmokeTestModel:  "deepseek-chat",
				SupportedModels: []string{"deepseek-chat", "deepseek-reasoner"},
			}}, nil
		},
	})
	request := httptestRequest(t, http.MethodGet, "/api/packs/openai-cn-pack/providers", nil, "secret-token")
	response := httptestRecorder(handler, request)
	assertStatusCode(t, response, http.StatusOK)

	var payload struct {
		Providers []PackProviderInfo `json:"providers"`
	}
	if err := json.Unmarshal(response.Body().Bytes(), &payload); err != nil {
		t.Fatalf("json.Unmarshal() error = %v", err)
	}
	if len(payload.Providers) != 1 {
		t.Fatalf("providers len = %d, want 1", len(payload.Providers))
	}
	got := payload.Providers[0]
	if got.ProviderID != "deepseek-chat-official" {
		t.Fatalf("provider_id = %q, want deepseek-chat-official", got.ProviderID)
	}
	if got.BaseURL != "https://api.deepseek.com" {
		t.Fatalf("base_url = %q, want https://api.deepseek.com", got.BaseURL)
	}
	if got.SmokeTestModel != "deepseek-chat" {
		t.Fatalf("smoke_test_model = %q, want deepseek-chat", got.SmokeTestModel)
	}
	if len(got.SupportedModels) != 2 || got.SupportedModels[0] != "deepseek-chat" || got.SupportedModels[1] != "deepseek-reasoner" {
		t.Fatalf("supported_models = %#v, want [deepseek-chat deepseek-reasoner]", got.SupportedModels)
	}
}

func TestAPIProviderResourcesReturnsSummary(t *testing.T) {
	handler := NewAPIHandler("secret-token", ActionSet{
		GetProviderResources: func(_ context.Context, req ProviderQueryRequest) (provision.ProviderSnapshot, error) {
			if req.ProviderID != "deepseek" {
				t.Fatalf("ProviderID = %q, want deepseek", req.ProviderID)
			}
			return provision.ProviderSnapshot{
				Pack:             sqlite.Pack{PackID: "openai-cn-pack"},
				Provider:         sqlite.Provider{ProviderID: "deepseek"},
				Batch:            sqlite.ImportBatch{ID: 7},
				ManagedResources: []sqlite.ManagedResource{{ID: 1, ResourceType: "group", HostResourceID: "group-1", ResourceName: "deepseek-group"}},
				AccessClosures:   []sqlite.AccessClosureRecord{{ID: 2, ClosureType: provision.AccessModeSelfService, Status: provision.AccessStatusSelfServiceReady, DetailsJSON: `{"ok":true}`}},
				ReconcileRuns:    []sqlite.ReconcileRun{{ID: 3, Status: "active", SummaryJSON: `{"missing_count":0}`}},
			}, nil
		},
	})
	request := httptestRequest(t, http.MethodGet, "/api/providers/deepseek/resources", nil, "secret-token")
	response := httptestRecorder(handler, request)
	assertStatusCode(t, response, http.StatusOK)
	assertJSONContains(t, response.Body().Bytes(), "provider_id", "deepseek")
	assertJSONContains(t, response.Body().Bytes(), "pack_id", "openai-cn-pack")
	if !strings.Contains(response.Body().String(), `"resource_type":"group"`) {
		t.Fatalf("resources payload missing group resource: %s", response.Body().String())
	}
	if !strings.Contains(response.Body().String(), `"status":"self_service_ready"`) {
		t.Fatalf("resources payload missing access closure status: %s", response.Body().String())
	}
	if !strings.Contains(response.Body().String(), `"summary_json":"{\"missing_count\":0}"`) {
		t.Fatalf("resources payload missing reconcile summary: %s", response.Body().String())
	}
}

func TestAPIRollbackProviderReturnsSummary(t *testing.T) {
	handler := NewAPIHandler("secret-token", ActionSet{
		RollbackProvider: func(context.Context, RollbackProviderRequest) (provision.RollbackReport, error) {
			return provision.RollbackReport{AccountsDeleted: 2, PlansDeleted: 1, ChannelsDeleted: 1, GroupsDeleted: 1}, nil
		},
	})
	request := httptestRequest(t, http.MethodPost, "/api/providers/deepseek/rollback", map[string]any{"host_base_url": "https://sub2api.example.com", "pack_path": "/tmp/openai-pack.zip"}, "secret-token")
	response := httptestRecorder(handler, request)
	assertStatusCode(t, response, http.StatusOK)
	assertJSONContains(t, response.Body().Bytes(), "deleted_accounts", float64(2))
	assertJSONContains(t, response.Body().Bytes(), "provider_id", "deepseek")
}

func TestAPIReconcileProviderReturnsSummary(t *testing.T) {
	handler := NewAPIHandler("secret-token", ActionSet{
		ReconcileProvider: func(_ context.Context, req ReconcileProviderRequest) (reconcile.Result, error) {
			if req.AccessAPIKey != "user-key" {
				t.Fatalf("AccessAPIKey = %q, want user-key", req.AccessAPIKey)
			}
			return reconcile.Result{BatchID: 7, Status: "drifted", MissingCount: 1, ExtraCount: 2, StaleNoiseCount: 3, ProbeFailureCount: 1, AccessStatus: provision.AccessStatusBroken, Summary: map[string]any{"probe_failures": 1, "stale_noise_count": 3}}, nil
		},
	})
	request := httptestRequest(t, http.MethodPost, "/api/providers/deepseek/reconcile", map[string]any{"host_base_url": "https://sub2api.example.com", "pack_path": "/tmp/openai-pack.zip", "access_api_key": "user-key"}, "secret-token")
	response := httptestRecorder(handler, request)
	assertStatusCode(t, response, http.StatusOK)
	assertJSONContains(t, response.Body().Bytes(), "status", "drifted")
	assertJSONContains(t, response.Body().Bytes(), "missing_count", float64(1))
	assertJSONContains(t, response.Body().Bytes(), "stale_noise_count", float64(3))
	assertJSONContains(t, response.Body().Bytes(), "summary.probe_failures", float64(1))
	assertJSONContains(t, response.Body().Bytes(), "summary.stale_noise_count", float64(3))
}

func waitForHealthz(t *testing.T, url string) *http.Response {
	t.Helper()

	client := &http.Client{Timeout: 100 * time.Millisecond}
	deadline := time.Now().Add(2 * time.Second)

	for time.Now().Before(deadline) {
		response, err := client.Get(url)
		if err == nil && response.StatusCode == http.StatusOK {
			return response
		}

		if response != nil {
			response.Body.Close()
		}

		time.Sleep(20 * time.Millisecond)
	}

	t.Fatalf("health endpoint %q was not reachable before deadline", url)
	return nil
}

func httptestRequest(t *testing.T, method, path string, body any, token string) *http.Request {
	t.Helper()
	payload, err := json.Marshal(body)
	if err != nil {
		t.Fatalf("json.Marshal() error = %v", err)
	}
	request, err := http.NewRequest(method, path, bytes.NewReader(payload))
	if err != nil {
		t.Fatalf("http.NewRequest() error = %v", err)
	}
	request.Header.Set("Content-Type", "application/json")
	if token != "" {
		request.Header.Set("Authorization", "Bearer "+token)
	}
	return request
}

func httptestRecorder(handler http.Handler, request *http.Request) *responseRecorder {
	recorder := &responseRecorder{header: make(http.Header)}
	handler.ServeHTTP(recorder, request)
	return recorder
}

type responseRecorder struct {
	header http.Header
	body   bytes.Buffer
	code   int
}

func (r *responseRecorder) Header() http.Header { return r.header }
func (r *responseRecorder) Write(body []byte) (int, error) {
	if r.code == 0 {
		r.code = http.StatusOK
	}
	return r.body.Write(body)
}
func (r *responseRecorder) WriteHeader(statusCode int) { r.code = statusCode }
func (r *responseRecorder) Body() *bytes.Buffer        { return &r.body }
func (r *responseRecorder) Result() *http.Response {
	statusCode := r.code
	if statusCode == 0 {
		statusCode = http.StatusOK
	}
	return &http.Response{
		StatusCode: statusCode,
		Header:     r.header.Clone(),
		Body:       io.NopCloser(bytes.NewReader(r.body.Bytes())),
	}
}

func assertStatusCode(t *testing.T, recorder *responseRecorder, want int) {
	t.Helper()
	if recorder.code != want {
		t.Fatalf("status code = %d, want %d; body=%s", recorder.code, want, recorder.body.String())
	}
}

func TestServerAddrReturnsConfiguredAddress(t *testing.T) {
	server := NewServer("127.0.0.1:9999", nil, nil)
	if got := server.Addr(); got != "127.0.0.1:9999" {
		t.Fatalf("Addr() = %q, want %q", got, "127.0.0.1:9999")
	}
}

func TestClassifyError(t *testing.T) {
	tests := []struct {
		name           string
		err            error
		wantStatusCode int
		wantCode       string
		wantUpstream   int
	}{
		{name: "nil", err: nil},
		{name: "http error passthrough", err: &httpError{StatusCode: http.StatusTeapot, Code: "teapot", Message: "brew"}, wantStatusCode: http.StatusTeapot, wantCode: "teapot"},
		{name: "upstream error", err: &sub2api.HTTPError{Method: http.MethodGet, Path: "/x", StatusCode: http.StatusForbidden, Body: "nope"}, wantStatusCode: http.StatusBadGateway, wantCode: "host_request_failed", wantUpstream: http.StatusForbidden},
		{name: "pack conflict already installed", err: errors.New("pack already installed"), wantStatusCode: http.StatusConflict, wantCode: "pack_conflict"},
		{name: "pack conflict checksum drift", err: errors.New("checksum drift detected"), wantStatusCode: http.StatusConflict, wantCode: "pack_conflict"},
		{name: "reconcile blocked rolled_back", err: errors.New("latest import batch is rolled_back; run import again before reconcile"), wantStatusCode: http.StatusConflict, wantCode: "batch_not_reconcilable"},
		{name: "not found generic", err: errors.New("host x not found"), wantStatusCode: http.StatusNotFound, wantCode: "not_found"},
		{name: "provider not found", err: errors.New("provider \"deepseek\" not found in pack \"openai\""), wantStatusCode: http.StatusBadRequest, wantCode: "provider_not_found"},
		{name: "bad request pack path", err: errors.New("pack path is required"), wantStatusCode: http.StatusBadRequest, wantCode: "bad_request"},
		{name: "bad request decode", err: errors.New("decode pack.json failed"), wantStatusCode: http.StatusBadRequest, wantCode: "bad_request"},
		{name: "internal error", err: errors.New("boom"), wantStatusCode: http.StatusInternalServerError, wantCode: "internal_error"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := classifyError(tt.err)
			if tt.err == nil {
				if got != nil {
					t.Fatalf("classifyError(nil) = %#v, want nil", got)
				}
				return
			}
			if got == nil {
				t.Fatal("classifyError() = nil, want error")
			}
			if got.StatusCode != tt.wantStatusCode {
				t.Fatalf("StatusCode = %d, want %d", got.StatusCode, tt.wantStatusCode)
			}
			if got.Code != tt.wantCode {
				t.Fatalf("Code = %q, want %q", got.Code, tt.wantCode)
			}
			if got.UpstreamStatus != tt.wantUpstream {
				t.Fatalf("UpstreamStatus = %d, want %d", got.UpstreamStatus, tt.wantUpstream)
			}
		})
	}
}

func TestWriteHTTPError(t *testing.T) {
	t.Run("default error when nil", func(t *testing.T) {
		recorder := &responseRecorder{header: make(http.Header)}
		writeHTTPError(recorder, nil)
		assertStatusCode(t, recorder, http.StatusInternalServerError)
		if got := recorder.Header().Get("Content-Type"); got != "application/json" {
			t.Fatalf("Content-Type = %q, want application/json", got)
		}
		assertJSONContains(t, recorder.Body().Bytes(), "error.code", "internal_error")
		assertJSONContains(t, recorder.Body().Bytes(), "error.message", "internal server error")
	})

	t.Run("writes provided error", func(t *testing.T) {
		recorder := &responseRecorder{header: make(http.Header)}
		writeHTTPError(recorder, &httpError{StatusCode: http.StatusBadRequest, Code: "bad_request", Message: "invalid input", UpstreamStatus: http.StatusConflict})
		assertStatusCode(t, recorder, http.StatusBadRequest)
		assertJSONContains(t, recorder.Body().Bytes(), "error.code", "bad_request")
		assertJSONContains(t, recorder.Body().Bytes(), "error.upstream_status", float64(http.StatusConflict))
	})
}

func TestDecodeJSON(t *testing.T) {
	t.Run("success", func(t *testing.T) {
		request := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(`{"host_base_url":"https://example.com","pack_path":"/tmp/pack.zip"}`))
		var got InstallPackRequest
		if err := decodeJSON(request, &got); err != nil {
			t.Fatalf("decodeJSON() error = %v, want nil", err)
		}
		if got.HostBaseURL != "https://example.com" || got.PackPath != "/tmp/pack.zip" {
			t.Fatalf("decoded request = %#v, want expected fields", got)
		}
	})

	t.Run("rejects unknown fields", func(t *testing.T) {
		request := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(`{"host_base_url":"https://example.com","unknown":true}`))
		var got InstallPackRequest
		err := decodeJSON(request, &got)
		if err == nil {
			t.Fatal("decodeJSON() error = nil, want error")
		}
		if err.StatusCode != http.StatusBadRequest || err.Code != "bad_request" {
			t.Fatalf("decodeJSON() = %#v, want bad_request", err)
		}
		if !strings.Contains(err.Message, "unknown field") {
			t.Fatalf("Message = %q, want unknown field", err.Message)
		}
	})

	t.Run("rejects trailing non-object payload", func(t *testing.T) {
		request := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(`{"host_base_url":"https://example.com"}[]`))
		var got InstallPackRequest
		err := decodeJSON(request, &got)
		if err == nil {
			t.Fatal("decodeJSON() error = nil, want error")
		}
		if err.Message != "request body must contain a single JSON object" {
			t.Fatalf("Message = %q, want single object error", err.Message)
		}
	})

	t.Run("rejects oversized request body", func(t *testing.T) {
		payload := `{"host_base_url":"https://example.com","pack_path":"` + strings.Repeat("x", int(maxJSONBodyBytes)) + `"}`
		request := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(payload))
		var got InstallPackRequest
		err := decodeJSON(request, &got)
		if err == nil {
			t.Fatal("decodeJSON() error = nil, want oversized error")
		}
		if err.StatusCode != http.StatusRequestEntityTooLarge || err.Code != "request_too_large" {
			t.Fatalf("decodeJSON() = %#v, want request_too_large", err)
		}
	})
}

func TestWriteJSON(t *testing.T) {
	recorder := &responseRecorder{header: make(http.Header)}
	writeJSON(recorder, http.StatusCreated, map[string]any{"ok": true, "count": 2})
	assertStatusCode(t, recorder, http.StatusCreated)
	if got := recorder.Header().Get("Content-Type"); got != "application/json" {
		t.Fatalf("Content-Type = %q, want application/json", got)
	}
	assertJSONContains(t, recorder.Body().Bytes(), "ok", true)
	assertJSONContains(t, recorder.Body().Bytes(), "count", float64(2))
}

func TestFindProvider(t *testing.T) {
	loaded := pack.LoadedPack{
		Manifest: pack.Manifest{PackID: "openai-cn-pack"},
		Providers: []pack.ProviderManifest{
			{ProviderID: "deepseek", DisplayName: "DeepSeek"},
			{ProviderID: "openai", DisplayName: "OpenAI"},
		},
	}

	provider, err := findProvider(loaded, " deepseek ")
	if err != nil {
		t.Fatalf("findProvider() error = %v, want nil", err)
	}
	if provider.ProviderID != "deepseek" {
		t.Fatalf("ProviderID = %q, want deepseek", provider.ProviderID)
	}

	_, err = findProvider(loaded, "missing")
	if err == nil {
		t.Fatal("findProvider() error = nil, want error")
	}
	if !strings.Contains(err.Error(), `provider "missing" not found in pack "openai-cn-pack"`) {
		t.Fatalf("findProvider() error = %v, want provider not found message", err)
	}
}

func TestAPIRequiresConfiguredAdminToken(t *testing.T) {
	handler := NewAPIHandler("", ActionSet{})
	request := httptestRequest(t, http.MethodPost, "/api/packs/install", map[string]any{"host_base_url": "https://sub2api.example.com"}, "any-token")
	response := httptestRecorder(handler, request)
	assertStatusCode(t, response, http.StatusInternalServerError)
	assertJSONContains(t, response.Body().Bytes(), "error.code", "server_misconfigured")
}

func TestAPIBatchDetailRejectsInvalidBatchID(t *testing.T) {
	handler := NewAPIHandler("secret-token", ActionSet{BatchDetail: func(context.Context, BatchDetailRequest) (provision.BatchDetailResult, error) {
		t.Fatal("BatchDetail should not be called for invalid batch id")
		return provision.BatchDetailResult{}, nil
	}})
	request := httptestRequest(t, http.MethodGet, "/api/import-batches/not-a-number", nil, "secret-token")
	response := httptestRecorder(handler, request)
	assertStatusCode(t, response, http.StatusBadRequest)
	assertJSONContains(t, response.Body().Bytes(), "error.code", "bad_request")
	assertJSONContains(t, response.Body().Bytes(), "error.message", "batch_id must be a positive integer")
}

func TestAPIInstallPackRejectsInvalidJSON(t *testing.T) {
	handler := NewAPIHandler("secret-token", ActionSet{InstallPack: func(context.Context, InstallPackRequest) (provision.PackInstallResult, error) {
		t.Fatal("InstallPack should not be called for invalid JSON")
		return provision.PackInstallResult{}, nil
	}})
	request, err := http.NewRequest(http.MethodPost, "/api/packs/install", strings.NewReader(`{"host_base_url":"https://sub2api.example.com","unknown":true}`))
	if err != nil {
		t.Fatalf("http.NewRequest() error = %v", err)
	}
	request.Header.Set("Authorization", "Bearer secret-token")
	request.Header.Set("Content-Type", "application/json")
	response := httptestRecorder(handler, request)
	assertStatusCode(t, response, http.StatusBadRequest)
	assertJSONContains(t, response.Body().Bytes(), "error.code", "bad_request")
}

func TestAPIImportProviderReturnsClassifiedErrorWithoutBatch(t *testing.T) {
	handler := NewAPIHandler("secret-token", ActionSet{
		ImportProvider: func(context.Context, ImportProviderRequest) (provision.RuntimeImportResult, error) {
			return provision.RuntimeImportResult{}, errors.New("pack path is required")
		},
	})
	request := httptestRequest(t, http.MethodPost, "/api/providers/deepseek/import", map[string]any{"host_base_url": "https://sub2api.example.com", "pack_path": "/tmp/openai-pack.zip"}, "secret-token")
	response := httptestRecorder(handler, request)
	assertStatusCode(t, response, http.StatusBadRequest)
	assertJSONContains(t, response.Body().Bytes(), "error.code", "bad_request")
	assertJSONContains(t, response.Body().Bytes(), "batch_id", float64(0))
}

func TestAPIPreviewProviderReturnsUpstreamError(t *testing.T) {
	handler := NewAPIHandler("secret-token", ActionSet{
		PreviewProvider: func(context.Context, PreviewProviderRequest) (provision.PreviewReport, error) {
			return provision.PreviewReport{}, &sub2api.HTTPError{Method: http.MethodPost, Path: "/preview", StatusCode: http.StatusTooManyRequests, Body: "rate limited"}
		},
	})
	request := httptestRequest(t, http.MethodPost, "/api/providers/deepseek/preview-import", map[string]any{"host_base_url": "https://sub2api.example.com", "pack_path": "/tmp/openai-pack.zip"}, "secret-token")
	response := httptestRecorder(handler, request)
	assertStatusCode(t, response, http.StatusBadGateway)
	assertJSONContains(t, response.Body().Bytes(), "error.code", "host_request_failed")
	assertJSONContains(t, response.Body().Bytes(), "error.upstream_status", float64(http.StatusTooManyRequests))
}

func TestAPIRollbackProviderReturnsConfiguredError(t *testing.T) {
	handler := NewAPIHandler("secret-token", ActionSet{
		RollbackProvider: func(context.Context, RollbackProviderRequest) (provision.RollbackReport, error) {
			return provision.RollbackReport{}, &httpError{StatusCode: http.StatusGone, Code: "rolled_back", Message: "already removed"}
		},
	})
	request := httptestRequest(t, http.MethodPost, "/api/providers/deepseek/rollback", map[string]any{"host_base_url": "https://sub2api.example.com", "pack_path": "/tmp/openai-pack.zip"}, "secret-token")
	response := httptestRecorder(handler, request)
	assertStatusCode(t, response, http.StatusGone)
	assertJSONContains(t, response.Body().Bytes(), "error.code", "rolled_back")
}

func TestAPIReconcileProviderRejectsTrailingNonObjectPayload(t *testing.T) {
	handler := NewAPIHandler("secret-token", ActionSet{ReconcileProvider: func(context.Context, ReconcileProviderRequest) (reconcile.Result, error) {
		t.Fatal("ReconcileProvider should not be called for invalid JSON")
		return reconcile.Result{}, nil
	}})
	request, err := http.NewRequest(http.MethodPost, "/api/providers/deepseek/reconcile", strings.NewReader(`{"host_base_url":"https://sub2api.example.com"}[]`))
	if err != nil {
		t.Fatalf("http.NewRequest() error = %v", err)
	}
	request.Header.Set("Authorization", "Bearer secret-token")
	request.Header.Set("Content-Type", "application/json")
	response := httptestRecorder(handler, request)
	assertStatusCode(t, response, http.StatusBadRequest)
	assertJSONContains(t, response.Body().Bytes(), "error.message", "request body must contain a single JSON object")
}

// --- Coverage edge cases ---

func TestHTTPErrorError(t *testing.T) {
	e := &httpError{StatusCode: http.StatusTeapot, Code: "teapot", Message: "i'm a teapot"}
	if got := e.Error(); got != "i'm a teapot" {
		t.Fatalf("httpError.Error() = %q, want %q", got, "i'm a teapot")
	}
}

func TestProviderStatusFnNil(t *testing.T) {
	handler := NewAPIHandler("t", ActionSet{})
	req := httptestRequest(t, http.MethodGet, "/api/providers/x/status", nil, "t")
	res := httptestRecorder(handler, req)
	assertStatusCode(t, res, http.StatusInternalServerError)
	assertJSONContains(t, res.Body().Bytes(), "error.code", "server_misconfigured")
}

func TestProviderAccessStatusFnNil(t *testing.T) {
	handler := NewAPIHandler("t", ActionSet{})
	req := httptestRequest(t, http.MethodGet, "/api/providers/x/access/status", nil, "t")
	res := httptestRecorder(handler, req)
	assertStatusCode(t, res, http.StatusInternalServerError)
	assertJSONContains(t, res.Body().Bytes(), "error.code", "server_misconfigured")
}

func TestProviderResourcesFnNil(t *testing.T) {
	handler := NewAPIHandler("t", ActionSet{})
	req := httptestRequest(t, http.MethodGet, "/api/providers/x/resources", nil, "t")
	res := httptestRecorder(handler, req)
	assertStatusCode(t, res, http.StatusInternalServerError)
	assertJSONContains(t, res.Body().Bytes(), "error.code", "server_misconfigured")
}

func TestProviderStatusReturnsError(t *testing.T) {
	handler := NewAPIHandler("t", ActionSet{
		GetProviderStatus: func(context.Context, ProviderQueryRequest) (provision.ProviderSnapshot, error) {
			return provision.ProviderSnapshot{}, errors.New(`provider "x" not found in pack "p"`)
		},
	})
	req := httptestRequest(t, http.MethodGet, "/api/providers/x/status", nil, "t")
	res := httptestRecorder(handler, req)
	assertStatusCode(t, res, http.StatusBadRequest)
	assertJSONContains(t, res.Body().Bytes(), "error.code", "provider_not_found")
}

func TestPostHandlersFnNil(t *testing.T) {
	tests := []struct {
		name   string
		method string
		path   string
		body   string
	}{
		{name: "install-pack", method: http.MethodPost, path: "/api/packs/install", body: `{}`},
		{name: "preview", method: http.MethodPost, path: "/api/providers/x/preview-import", body: `{}`},
		{name: "import", method: http.MethodPost, path: "/api/providers/x/import", body: `{}`},
		{name: "rollback", method: http.MethodPost, path: "/api/providers/x/rollback", body: `{}`},
		{name: "reconcile", method: http.MethodPost, path: "/api/providers/x/reconcile", body: `{}`},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			handler := NewAPIHandler("t", ActionSet{})
			req, _ := http.NewRequest(tt.method, tt.path, strings.NewReader(tt.body))
			req.Header.Set("Authorization", "Bearer t")
			req.Header.Set("Content-Type", "application/json")
			res := httptestRecorder(handler, req)
			assertStatusCode(t, res, http.StatusInternalServerError)
			assertJSONContains(t, res.Body().Bytes(), "error.code", "server_misconfigured")
		})
	}
}

func TestGetHandlersFnNil(t *testing.T) {
	tests := []struct {
		name string
		path string
	}{
		{name: "list-hosts", path: "/api/hosts"},
		{name: "get-host", path: "/api/hosts/my-host"},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			handler := NewAPIHandler("t", ActionSet{})
			req, _ := http.NewRequest(http.MethodGet, tt.path, nil)
			req.Header.Set("Authorization", "Bearer t")
			res := httptestRecorder(handler, req)
			assertStatusCode(t, res, http.StatusInternalServerError)
			assertJSONContains(t, res.Body().Bytes(), "error.code", "server_misconfigured")
		})
	}
}

func TestDeleteHandlersFnNil(t *testing.T) {
	tests := []struct {
		name string
		path string
	}{
		{name: "delete-host", path: "/api/hosts/my-host"},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			handler := NewAPIHandler("t", ActionSet{})
			req, _ := http.NewRequest(http.MethodDelete, tt.path, nil)
			req.Header.Set("Authorization", "Bearer t")
			res := httptestRecorder(handler, req)
			assertStatusCode(t, res, http.StatusInternalServerError)
			assertJSONContains(t, res.Body().Bytes(), "error.code", "server_misconfigured")
		})
	}
}

func TestHandlerErrorPaths(t *testing.T) {
	tests := []struct {
		name       string
		method     string
		path       string
		body       string
		actionSet  ActionSet
		wantStatus int
		wantCode   string
	}{
		{
			name:   "access-status-error",
			method: http.MethodGet,
			path:   "/api/providers/x/access/status",
			actionSet: ActionSet{
				GetProviderAccessStatus: func(context.Context, ProviderQueryRequest) (provision.ProviderSnapshot, error) {
					return provision.ProviderSnapshot{}, errors.New("boom")
				},
			},
			wantStatus: http.StatusInternalServerError,
			wantCode:   "internal_error",
		},
		{
			name:   "preview-error",
			method: http.MethodPost,
			path:   "/api/providers/x/preview-import",
			body:   `{}`,
			actionSet: ActionSet{
				PreviewProvider: func(context.Context, PreviewProviderRequest) (provision.PreviewReport, error) {
					return provision.PreviewReport{}, errors.New("boom")
				},
			},
			wantStatus: http.StatusInternalServerError,
			wantCode:   "internal_error",
		},
		{
			name:   "rollback-error",
			method: http.MethodPost,
			path:   "/api/providers/x/rollback",
			body:   `{}`,
			actionSet: ActionSet{
				RollbackProvider: func(context.Context, RollbackProviderRequest) (provision.RollbackReport, error) {
					return provision.RollbackReport{}, errors.New("boom")
				},
			},
			wantStatus: http.StatusInternalServerError,
			wantCode:   "internal_error",
		},
		{
			name:   "reconcile-error",
			method: http.MethodPost,
			path:   "/api/providers/x/reconcile",
			body:   `{}`,
			actionSet: ActionSet{
				ReconcileProvider: func(context.Context, ReconcileProviderRequest) (reconcile.Result, error) {
					return reconcile.Result{}, errors.New("boom")
				},
			},
			wantStatus: http.StatusInternalServerError,
			wantCode:   "internal_error",
		},
		{
			name:   "list-hosts-error",
			method: http.MethodGet,
			path:   "/api/hosts",
			actionSet: ActionSet{
				ListHosts: func(context.Context) ([]HostInfo, error) {
					return nil, errors.New("boom")
				},
			},
			wantStatus: http.StatusInternalServerError,
			wantCode:   "internal_error",
		},
		{
			name:   "get-host-error",
			method: http.MethodGet,
			path:   "/api/hosts/my-host",
			actionSet: ActionSet{
				GetHost: func(context.Context, string) (HostInfo, error) {
					return HostInfo{}, errors.New("boom")
				},
			},
			wantStatus: http.StatusInternalServerError,
			wantCode:   "internal_error",
		},
		{
			name:   "get-host-not-found",
			method: http.MethodGet,
			path:   "/api/hosts/unknown",
			actionSet: ActionSet{
				GetHost: func(context.Context, string) (HostInfo, error) {
					return HostInfo{}, errors.New("host unknown not found")
				},
			},
			wantStatus: http.StatusNotFound,
			wantCode:   "not_found",
		},
		{
			name:   "delete-host-error",
			method: http.MethodDelete,
			path:   "/api/hosts/my-host",
			actionSet: ActionSet{
				DeleteHost: func(context.Context, string) error {
					return errors.New("boom")
				},
			},
			wantStatus: http.StatusInternalServerError,
			wantCode:   "internal_error",
		},
		{
			name:   "delete-host-not-found",
			method: http.MethodDelete,
			path:   "/api/hosts/unknown",
			actionSet: ActionSet{
				DeleteHost: func(context.Context, string) error {
					return errors.New("host unknown not found")
				},
			},
			wantStatus: http.StatusNotFound,
			wantCode:   "not_found",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			handler := NewAPIHandler("t", tt.actionSet)
			var req *http.Request
			if tt.body != "" {
				req, _ = http.NewRequest(tt.method, tt.path, strings.NewReader(tt.body))
				req.Header.Set("Content-Type", "application/json")
			} else {
				var err error
				req, err = http.NewRequest(tt.method, tt.path, nil)
				if err != nil {
					t.Fatal(err)
				}
			}
			req.Header.Set("Authorization", "Bearer t")
			res := httptestRecorder(handler, req)
			assertStatusCode(t, res, tt.wantStatus)
			assertJSONContains(t, res.Body().Bytes(), "error.code", tt.wantCode)
		})
	}
}

func TestResolveLatestAccessStatusAggregatesAcrossModeBatches(t *testing.T) {
	store := openAppTestStore(t)
	defer closeAppTestStore(t, store)

	ctx := context.Background()
	hostID, err := store.Hosts().Create(ctx, sqlite.Host{HostID: "host-1", BaseURL: "https://sub2api.example.com", HostVersion: "0.1.126", AuthType: "apikey", AuthToken: "token"})
	if err != nil {
		t.Fatalf("Hosts().Create() error = %v", err)
	}
	packID, err := store.Packs().Create(ctx, sqlite.Pack{PackID: "openai-cn-pack", Version: "1.0.0", TargetHost: "sub2api", Checksum: "checksum-1"})
	if err != nil {
		t.Fatalf("Packs().Create() error = %v", err)
	}
	providerID, err := store.Providers().Create(ctx, sqlite.Provider{PackID: packID, ProviderID: "deepseek", DisplayName: "DeepSeek", BaseURL: "https://api.deepseek.com", Platform: "openai"})
	if err != nil {
		t.Fatalf("Providers().Create() error = %v", err)
	}
	batchSubscription, err := store.ImportBatches().Create(ctx, sqlite.ImportBatch{HostID: hostID, PackID: packID, ProviderID: providerID, Mode: provision.ImportModePartial, BatchStatus: provision.BatchStatusSucceeded, AccessStatus: provision.AccessStatusSubscriptionReady})
	if err != nil {
		t.Fatalf("ImportBatches().Create(subscription) error = %v", err)
	}
	if _, err := store.AccessClosures().Create(ctx, sqlite.AccessClosureRecord{BatchID: batchSubscription, ClosureType: provision.AccessModeSubscription, Status: provision.AccessStatusSubscriptionReady, DetailsJSON: "{}"}); err != nil {
		t.Fatalf("AccessClosures().Create(subscription) error = %v", err)
	}
	batchSelf, err := store.ImportBatches().Create(ctx, sqlite.ImportBatch{HostID: hostID, PackID: packID, ProviderID: providerID, Mode: provision.ImportModePartial, BatchStatus: provision.BatchStatusSucceeded, AccessStatus: provision.AccessStatusSelfServiceReady})
	if err != nil {
		t.Fatalf("ImportBatches().Create(self_service) error = %v", err)
	}
	if _, err := store.AccessClosures().Create(ctx, sqlite.AccessClosureRecord{BatchID: batchSelf, ClosureType: provision.AccessModeSelfService, Status: provision.AccessStatusSelfServiceReady, DetailsJSON: "{}"}); err != nil {
		t.Fatalf("AccessClosures().Create(self_service) error = %v", err)
	}

	got, err := resolveLatestAccessStatus(ctx, store, sqlite.Provider{ID: providerID, ProviderID: "deepseek"}, "host-1")
	if err != nil {
		t.Fatalf("resolveLatestAccessStatus() error = %v", err)
	}
	if got != provision.AccessStatusFullyReady {
		t.Fatalf("resolveLatestAccessStatus() = %q, want %q", got, provision.AccessStatusFullyReady)
	}
}

func TestProviderAccessStatusMultipleClosures(t *testing.T) {
	handler := NewAPIHandler("t", ActionSet{
		GetProviderAccessStatus: func(context.Context, ProviderQueryRequest) (provision.ProviderSnapshot, error) {
			return provision.ProviderSnapshot{
				Pack:               sqlite.Pack{PackID: "p"},
				Provider:           sqlite.Provider{ProviderID: "dp"},
				Batch:              sqlite.ImportBatch{ID: 1},
				LatestAccessStatus: "ready",
				AccessClosures: []sqlite.AccessClosureRecord{
					{ID: 1, ClosureType: "preview", Status: "done", DetailsJSON: `{"v":1}`},
					{ID: 2, ClosureType: "self_service", Status: "active", DetailsJSON: `{"v":2}`},
				},
			}, nil
		},
	})
	req := httptestRequest(t, http.MethodGet, "/api/providers/dp/access/status", nil, "t")
	res := httptestRecorder(handler, req)
	assertStatusCode(t, res, http.StatusOK)
	// Should report the last closure (index n-1)
	if !strings.Contains(res.Body().String(), `"closure_type":"self_service"`) {
		t.Fatalf("expected latest closure to be self_service, got: %s", res.Body().String())
	}
}

func TestAccessStatusSupportsMode(t *testing.T) {
	tests := []struct {
		name   string
		status string
		mode   string
		want   bool
	}{
		{name: "subscription ready supports subscription", status: provision.AccessStatusSubscriptionReady, mode: provision.AccessModeSubscription, want: true},
		{name: "subscription ready does not support self service", status: provision.AccessStatusSubscriptionReady, mode: provision.AccessModeSelfService, want: false},
		{name: "fully ready supports self service", status: provision.AccessStatusFullyReady, mode: provision.AccessModeSelfService, want: true},
		{name: "broken does not support any", status: provision.AccessStatusBroken, mode: "", want: false},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := accessStatusSupportsMode(tt.status, tt.mode); got != tt.want {
				t.Fatalf("accessStatusSupportsMode(%q, %q) = %v, want %v", tt.status, tt.mode, got, tt.want)
			}
		})
	}
}

func TestHostSupportStatusRequiresPlansCapability(t *testing.T) {
	status := hostSupportStatus(sub2api.HostCapabilities{Groups: true, Channels: true, Plans: false, Accounts: true, AccountTest: true, AccountModels: true, Subscriptions: true})
	if status != "unsupported" {
		t.Fatalf("hostSupportStatus() = %q, want unsupported when plans capability is missing", status)
	}
}

func openAppTestStore(t *testing.T) *sqlite.DB {
	t.Helper()
	return testutil.OpenSQLiteStore(t, testutil.SQLiteTestDSN(t, "state.db", true))
}

func closeAppTestStore(t *testing.T, store *sqlite.DB) {
	t.Helper()
	testutil.CloseSQLiteStore(t, store)
}

func assertJSONContains(t *testing.T, payload []byte, key string, want any) {
	t.Helper()
	var decoded map[string]any
	if err := json.Unmarshal(payload, &decoded); err != nil {
		t.Fatalf("json.Unmarshal() error = %v; payload=%s", err, string(payload))
	}
	if strings.Contains(key, ".") {
		parts := strings.Split(key, ".")
		current := any(decoded)
		for _, part := range parts {
			object, ok := current.(map[string]any)
			if !ok {
				t.Fatalf("key %q not found in payload %s", key, string(payload))
			}
			current = object[part]
		}
		if current != want {
			t.Fatalf("json key %q = %#v, want %#v; payload=%s", key, current, want, string(payload))
		}
		return
	}
	if decoded[key] != want {
		t.Fatalf("json key %q = %#v, want %#v; payload=%s", key, decoded[key], want, string(payload))
	}
}

func TestNewActionSetReturnsNonNil(t *testing.T) {
	as := NewActionSet("file::memory:?cache=shared")
	t.Run("InstallPack", func(t *testing.T) {
		if as.InstallPack == nil {
			t.Fatal("is nil")
		}
	})
	t.Run("BatchDetail", func(t *testing.T) {
		if as.BatchDetail == nil {
			t.Fatal("is nil")
		}
	})
	t.Run("GetProviderStatus", func(t *testing.T) {
		if as.GetProviderStatus == nil {
			t.Fatal("is nil")
		}
	})
	t.Run("GetProviderResources", func(t *testing.T) {
		if as.GetProviderResources == nil {
			t.Fatal("is nil")
		}
	})
	t.Run("GetProviderAccessStatus", func(t *testing.T) {
		if as.GetProviderAccessStatus == nil {
			t.Fatal("is nil")
		}
	})
	t.Run("PreviewProvider", func(t *testing.T) {
		if as.PreviewProvider == nil {
			t.Fatal("is nil")
		}
	})
	t.Run("ImportProvider", func(t *testing.T) {
		if as.ImportProvider == nil {
			t.Fatal("is nil")
		}
	})
	t.Run("RollbackProvider", func(t *testing.T) {
		if as.RollbackProvider == nil {
			t.Fatal("is nil")
		}
	})
	t.Run("ReconcileProvider", func(t *testing.T) {
		if as.ReconcileProvider == nil {
			t.Fatal("is nil")
		}
	})
	t.Run("ListHosts", func(t *testing.T) {
		if as.ListHosts == nil {
			t.Fatal("is nil")
		}
	})
	t.Run("GetHost", func(t *testing.T) {
		if as.GetHost == nil {
			t.Fatal("is nil")
		}
	})
	t.Run("DeleteHost", func(t *testing.T) {
		if as.DeleteHost == nil {
			t.Fatal("is nil")
		}
	})
	t.Run("ProbeHost", func(t *testing.T) {
		if as.ProbeHost == nil {
			t.Fatal("is nil")
		}
	})
	t.Run("ListProviderImportBatches", func(t *testing.T) {
		if as.ListProviderImportBatches == nil {
			t.Fatal("is nil")
		}
	})
	t.Run("RollbackBatch", func(t *testing.T) {
		if as.RollbackBatch == nil {
			t.Fatal("is nil")
		}
	})
}

func TestBatchDetailReturnsNotFoundForMissingBatch(t *testing.T) {
	as := NewActionSet("file::memory:?cache=shared")
	_, err := as.BatchDetail(context.Background(), BatchDetailRequest{BatchID: 999})
	if err == nil {
		t.Fatal("BatchDetail() error = nil for missing batch, want error")
	}
}

func TestNewActionSetSQLiteClosures(t *testing.T) {
	dsn := "file::memory:?cache=shared"
	as := NewActionSet(dsn)
	ctx := context.Background()

	t.Run("GetProviderStatus on empty DB", func(t *testing.T) {
		_, err := as.GetProviderStatus(ctx, ProviderQueryRequest{ProviderID: "x", PackID: "p"})
		if err == nil {
			t.Fatal("expected error from empty DB, got nil")
		}
	})

	t.Run("GetProviderResources on empty DB", func(t *testing.T) {
		_, err := as.GetProviderResources(ctx, ProviderQueryRequest{ProviderID: "x", PackID: "p"})
		if err == nil {
			t.Fatal("expected error from empty DB, got nil")
		}
	})

	t.Run("GetProviderAccessStatus on empty DB", func(t *testing.T) {
		_, err := as.GetProviderAccessStatus(ctx, ProviderQueryRequest{ProviderID: "x", PackID: "p"})
		if err == nil {
			t.Fatal("expected error from empty DB, got nil")
		}
	})

	t.Run("ListHosts on empty DB", func(t *testing.T) {
		hosts, err := as.ListHosts(ctx)
		if err != nil {
			t.Fatalf("ListHosts() on empty DB error = %v, want nil", err)
		}
		if len(hosts) != 0 {
			t.Fatalf("ListHosts() len = %d, want 0", len(hosts))
		}
	})

	t.Run("GetHost on empty DB", func(t *testing.T) {
		_, err := as.GetHost(ctx, "nonexistent")
		if err == nil {
			t.Fatal("expected error from empty DB, got nil")
		}
	})

	t.Run("ListProviderImportBatches on empty DB", func(t *testing.T) {
		batches, err := as.ListProviderImportBatches(ctx, ProviderQueryRequest{ProviderID: "x"})
		if err != nil {
			t.Fatalf("ListProviderImportBatches() on empty DB error = %v, want nil", err)
		}
		if len(batches) != 0 {
			t.Fatalf("ListProviderImportBatches() len = %d, want 0", len(batches))
		}
	})
}

func TestAPIProbeHostReturnsHostSnapshot(t *testing.T) {
	handler := NewAPIHandler("secret-token", ActionSet{
		ProbeHost: func(_ context.Context, req ProbeHostRequest) (HostInfo, error) {
			if req.HostID != "prod-sub2api" {
				t.Fatalf("ProbeHost hostID = %q, want prod-sub2api", req.HostID)
			}
			if req.Auth.Type != "bearer" || req.Auth.Token != "probe-token" {
				t.Fatalf("ProbeHost auth = %#v, want bearer/probe-token", req.Auth)
			}
			return HostInfo{HostID: req.HostID, BaseURL: "https://sub2api.example.com", HostVersion: "0.1.126", Status: "supported"}, nil
		},
	})
	req := httptestRequest(t, http.MethodPost, "/api/hosts/prod-sub2api/probe", map[string]any{
		"auth": map[string]any{"type": "bearer", "token": "probe-token"},
	}, "secret-token")
	res := httptestRecorder(handler, req)
	assertStatusCode(t, res, http.StatusOK)
	assertJSONContains(t, res.Body().Bytes(), "host_id", "prod-sub2api")
	assertJSONContains(t, res.Body().Bytes(), "host_version", "0.1.126")
	assertJSONContains(t, res.Body().Bytes(), "status", "supported")
}

func TestAPIListProviderImportBatchesReturnsItems(t *testing.T) {
	handler := NewAPIHandler("secret-token", ActionSet{
		ListProviderImportBatches: func(_ context.Context, req ProviderQueryRequest) ([]ImportBatchInfo, error) {
			if req.ProviderID != "deepseek" {
				t.Fatalf("ListProviderImportBatches providerID = %q, want deepseek", req.ProviderID)
			}
			return []ImportBatchInfo{{BatchID: 7, BatchStatus: provision.BatchStatusSucceeded, AccessStatus: provision.AccessStatusSelfServiceReady}}, nil
		},
	})
	req := httptestRequest(t, http.MethodGet, "/api/providers/deepseek/import-batches", nil, "secret-token")
	res := httptestRecorder(handler, req)
	assertStatusCode(t, res, http.StatusOK)
	body := res.Body().String()
	if !strings.Contains(body, `"batch_id":7`) || !strings.Contains(body, `"batch_status":"succeeded"`) || !strings.Contains(body, `"access_status":"self_service_ready"`) {
		t.Fatalf("unexpected import batch payload: %s", body)
	}
}

func TestAPIRollbackBatchReturnsSummary(t *testing.T) {
	handler := NewAPIHandler("secret-token", ActionSet{
		RollbackBatch: func(_ context.Context, req RollbackBatchRequest) (provision.RollbackReport, error) {
			if req.BatchID != 11 {
				t.Fatalf("RollbackBatch batchID = %d, want 11", req.BatchID)
			}
			if req.Auth.Type != "apikey" || req.Auth.Token != "admin-key" {
				t.Fatalf("RollbackBatch auth = %#v, want apikey/admin-key", req.Auth)
			}
			return provision.RollbackReport{AccountsDeleted: 2, PlansDeleted: 1, ChannelsDeleted: 1, GroupsDeleted: 1}, nil
		},
	})
	req := httptestRequest(t, http.MethodPost, "/api/import-batches/11/rollback", map[string]any{
		"auth": map[string]any{"type": "apikey", "token": "admin-key"},
	}, "secret-token")
	res := httptestRecorder(handler, req)
	assertStatusCode(t, res, http.StatusOK)
	assertJSONContains(t, res.Body().Bytes(), "deleted_accounts", float64(2))
	assertJSONContains(t, res.Body().Bytes(), "deleted_plans", float64(1))
	assertJSONContains(t, res.Body().Bytes(), "deleted_channels", float64(1))
	assertJSONContains(t, res.Body().Bytes(), "deleted_groups", float64(1))
}

func TestNewActionSetPackErrorPaths(t *testing.T) {
	dsn := "file::memory:?cache=shared"
	as := NewActionSet(dsn)
	ctx := context.Background()

	t.Run("InstallPack bad path", func(t *testing.T) {
		_, err := as.InstallPack(ctx, InstallPackRequest{PackPath: "/nonexistent/pack"})
		if err == nil {
			t.Fatal("expected error from bad pack path")
		}
	})

	t.Run("PreviewProvider bad path", func(t *testing.T) {
		_, err := as.PreviewProvider(ctx, PreviewProviderRequest{PackPath: "/nonexistent/pack", ProviderID: "x"})
		if err == nil {
			t.Fatal("expected error from bad pack path")
		}
	})

	t.Run("ImportProvider bad path", func(t *testing.T) {
		_, err := as.ImportProvider(ctx, ImportProviderRequest{PackPath: "/nonexistent/pack", ProviderID: "x", HostBaseURL: "http://h:8080"})
		if err == nil {
			t.Fatal("expected error from bad pack path")
		}
	})

	t.Run("RollbackProvider bad path", func(t *testing.T) {
		_, err := as.RollbackProvider(ctx, RollbackProviderRequest{PackPath: "/nonexistent/pack", ProviderID: "x", HostBaseURL: "http://h:8080"})
		if err == nil {
			t.Fatal("expected error from bad pack path")
		}
	})

	t.Run("ReconcileProvider bad path", func(t *testing.T) {
		_, err := as.ReconcileProvider(ctx, ReconcileProviderRequest{PackPath: "/nonexistent/pack", ProviderID: "x", HostBaseURL: "http://h:8080"})
		if err == nil {
			t.Fatal("expected error from bad pack path")
		}
	})
}

func TestNewActionSetCreateProviderDraftRejectsModelConflictWithStoredProvider(t *testing.T) {
	ctx := context.Background()
	store, err := sqlite.Open(ctx, "file:"+filepath.Join(t.TempDir(), "create-draft-conflict.db")+"?_foreign_keys=on&_busy_timeout=5000")
	if err != nil {
		t.Fatalf("sqlite.Open() error = %v", err)
	}
	defer store.Close()

	packID, err := store.Packs().Create(ctx, sqlite.Pack{
		PackID:       "openai-cn-pack",
		Version:      "1.1.4",
		Checksum:     "chk-openai-cn-pack",
		Vendor:       "YourTeam",
		TargetHost:   "sub2api",
		ManifestJSON: `{"pack_id":"openai-cn-pack","version":"1.1.4","target_host":"sub2api"}`,
	})
	if err != nil {
		t.Fatalf("Packs().Create() error = %v", err)
	}
	if _, err := store.Providers().Create(ctx, sqlite.Provider{
		PackID:            packID,
		ProviderID:        "deepseek-chat-official",
		DisplayName:       "DeepSeek Official",
		BaseURL:           "https://api.deepseek.com",
		Platform:          "openai",
		AccountType:       "apikey",
		DefaultModelsJSON: `["deepseek-chat"]`,
		SmokeTestModel:    "deepseek-chat",
		ManifestJSON:      `{"provider_id":"deepseek-chat-official","display_name":"DeepSeek Official","base_url":"https://api.deepseek.com","platform":"openai","default_models":["deepseek-chat"],"smoke_test_model":"deepseek-chat"}`,
	}); err != nil {
		t.Fatalf("Providers().Create() error = %v", err)
	}

	actions := NewActionSet(appTestDSN(t, store))
	_, err = actions.CreateProviderDraft(ctx, CreateProviderDraftRequest{
		PackID:          "openai-cn-pack",
		ProviderID:      "deepseek-chat-relay",
		DisplayName:     "DeepSeek Relay",
		Platform:        "openai",
		BaseURL:         "https://relay.example.com/v1",
		SmokeTestModel:  "deepseek-chat",
		SupportedModels: []string{"deepseek-chat"},
	})
	if err == nil {
		t.Fatal("CreateProviderDraft() error = nil, want provider model conflict")
	}
	var httpErr *httpError
	if !errors.As(err, &httpErr) {
		t.Fatalf("CreateProviderDraft() error = %T, want *httpError", err)
	}
	if httpErr.StatusCode != http.StatusConflict || httpErr.Code != "provider_model_conflict" {
		t.Fatalf("CreateProviderDraft() = %#v, want 409/provider_model_conflict", httpErr)
	}
}

func TestNewActionSetUpdateProviderDraftRejectsModelConflictWithOtherDraft(t *testing.T) {
	ctx := context.Background()
	store, err := sqlite.Open(ctx, "file:"+filepath.Join(t.TempDir(), "update-draft-conflict.db")+"?_foreign_keys=on&_busy_timeout=5000")
	if err != nil {
		t.Fatalf("sqlite.Open() error = %v", err)
	}
	defer store.Close()

	if _, err := store.ProviderDrafts().Create(ctx, sqlite.ProviderDraft{
		DraftID:             "draft_existing",
		PackID:              "openai-cn-pack",
		ProviderID:          "deepseek-chat-official",
		DisplayName:         "DeepSeek Official",
		Platform:            "openai",
		BaseURL:             "https://api.deepseek.com",
		SmokeTestModel:      "deepseek-chat",
		SupportedModelsJSON: `["deepseek-chat"]`,
		ManifestJSON:        `{"provider_id":"deepseek-chat-official","display_name":"DeepSeek Official","platform":"openai","base_url":"https://api.deepseek.com","smoke_test_model":"deepseek-chat"}`,
	}); err != nil {
		t.Fatalf("ProviderDrafts().Create(existing) error = %v", err)
	}
	if _, err := store.ProviderDrafts().Create(ctx, sqlite.ProviderDraft{
		DraftID:             "draft_target",
		PackID:              "openai-cn-pack",
		ProviderID:          "deepseek-chat-relay",
		DisplayName:         "DeepSeek Relay",
		Platform:            "openai",
		BaseURL:             "https://relay.example.com/v1",
		SmokeTestModel:      "deepseek-reasoner",
		SupportedModelsJSON: `["deepseek-reasoner"]`,
		ManifestJSON:        `{"provider_id":"deepseek-chat-relay","display_name":"DeepSeek Relay","platform":"openai","base_url":"https://relay.example.com/v1","smoke_test_model":"deepseek-reasoner"}`,
	}); err != nil {
		t.Fatalf("ProviderDrafts().Create(target) error = %v", err)
	}

	actions := NewActionSet(appTestDSN(t, store))
	_, err = actions.UpdateProviderDraft(ctx, UpdateProviderDraftRequest{
		DraftID: "draft_target",
		CreateProviderDraftRequest: CreateProviderDraftRequest{
			PackID:          "openai-cn-pack",
			ProviderID:      "deepseek-chat-relay",
			DisplayName:     "DeepSeek Relay",
			Platform:        "openai",
			BaseURL:         "https://relay.example.com/v1",
			SmokeTestModel:  "deepseek-chat",
			SupportedModels: []string{"deepseek-chat"},
		},
	})
	if err == nil {
		t.Fatal("UpdateProviderDraft() error = nil, want provider model conflict")
	}
	var httpErr *httpError
	if !errors.As(err, &httpErr) {
		t.Fatalf("UpdateProviderDraft() error = %T, want *httpError", err)
	}
	if httpErr.StatusCode != http.StatusConflict || httpErr.Code != "provider_model_conflict" {
		t.Fatalf("UpdateProviderDraft() = %#v, want 409/provider_model_conflict", httpErr)
	}
}

func TestNewActionSetPublishProviderDraftRejectsModelConflictWithRepoProvider(t *testing.T) {
	if _, err := exec.LookPath("git"); err != nil {
		t.Skip("git is required for publish action test")
	}

	ctx := context.Background()
	store, err := sqlite.Open(ctx, "file:"+filepath.Join(t.TempDir(), "publish-draft-conflict.db")+"?_foreign_keys=on&_busy_timeout=5000")
	if err != nil {
		t.Fatalf("sqlite.Open() error = %v", err)
	}
	defer store.Close()

	repoRoot := t.TempDir()
	packDir := filepath.Join(repoRoot, "packs", "openai-cn-pack")
	createPackFixtureAt(t, packDir, map[string]string{
		"pack.json": `{
  "pack_id": "openai-cn-pack",
  "version": "1.1.4",
  "vendor": "YourTeam",
  "target_host": "sub2api",
  "min_host_version": "0.1.126",
  "max_host_version": "0.2.x",
  "providers_dir": "providers",
  "checksum_file": "checksums.txt"
}`,
		"providers/deepseek-chat-official.json": `{
  "provider_id": "deepseek-chat-official",
  "display_name": "DeepSeek Official",
  "base_url": "https://api.deepseek.com",
  "platform": "openai",
  "account_type": "apikey",
  "default_models": ["deepseek-chat"],
  "smoke_test_model": "deepseek-chat",
  "group_template": {"name": "g", "rate_multiplier": 1.0},
  "channel_template": {"name": "c", "model_mapping": {"deepseek-chat": "deepseek-chat"}},
  "plan_template": {"name": "p", "price": 1, "validity_days": 30, "validity_unit": "day"},
  "import": {"supports_multi_key": true, "supports_strict": true, "supports_partial": true}
}`,
	})
	initGitRepo(t, repoRoot)

	if _, err := store.ProviderDrafts().Create(ctx, sqlite.ProviderDraft{
		DraftID:             "draft_publish_conflict",
		PackID:              "openai-cn-pack",
		ProviderID:          "deepseek-chat-relay",
		DisplayName:         "DeepSeek Relay",
		Platform:            "openai",
		BaseURL:             "https://relay.example.com/v1",
		SmokeTestModel:      "deepseek-chat",
		SupportedModelsJSON: `["deepseek-chat"]`,
		ManifestJSON:        `{"provider_id":"deepseek-chat-relay","display_name":"DeepSeek Relay","platform":"openai","base_url":"https://relay.example.com/v1","smoke_test_model":"deepseek-chat"}`,
	}); err != nil {
		t.Fatalf("ProviderDrafts().Create() error = %v", err)
	}

	t.Setenv("SUB2API_CRM_REPO_ROOT", repoRoot)
	actions := NewActionSet(appTestDSN(t, store))
	_, err = actions.PublishProviderDraft(ctx, PublishProviderDraftRequest{
		DraftID:       "draft_publish_conflict",
		CommitMessage: "feat(pack): publish provider draft deepseek-chat-relay",
	})
	if err == nil {
		t.Fatal("PublishProviderDraft() error = nil, want provider model conflict")
	}
	var httpErr *httpError
	if !errors.As(err, &httpErr) {
		t.Fatalf("PublishProviderDraft() error = %T, want *httpError", err)
	}
	if httpErr.StatusCode != http.StatusConflict || httpErr.Code != "provider_model_conflict" {
		t.Fatalf("PublishProviderDraft() = %#v, want 409/provider_model_conflict", httpErr)
	}
}

func TestNewActionSetPublishProviderDraftCreatesPackCommit(t *testing.T) {
	if _, err := exec.LookPath("git"); err != nil {
		t.Skip("git is required for publish action test")
	}

	ctx := context.Background()
	store, err := sqlite.Open(ctx, "file:"+filepath.Join(t.TempDir(), "publish-action.db")+"?_foreign_keys=on&_busy_timeout=5000")
	if err != nil {
		t.Fatalf("sqlite.Open() error = %v", err)
	}
	defer store.Close()

	repoRoot := t.TempDir()
	packDir := filepath.Join(repoRoot, "packs", "openai-cn-pack")
	createPackFixtureAt(t, packDir, map[string]string{
		"pack.json": `{
  "pack_id": "openai-cn-pack",
  "version": "1.1.4",
  "vendor": "YourTeam",
  "target_host": "sub2api",
  "min_host_version": "0.1.126",
  "max_host_version": "0.2.x",
  "providers_dir": "providers",
  "checksum_file": "checksums.txt"
}`,
		"providers/deepseek.json": `{
  "provider_id": "deepseek",
  "display_name": "DeepSeek",
  "base_url": "https://api.deepseek.com",
  "platform": "openai",
  "account_type": "apikey",
  "default_models": ["deepseek-chat"],
  "smoke_test_model": "deepseek-chat",
  "group_template": {"name": "g", "rate_multiplier": 1.0},
  "channel_template": {"name": "c", "model_mapping": {"deepseek-chat": "deepseek-chat"}},
  "plan_template": {"name": "p", "price": 1, "validity_days": 30, "validity_unit": "day"},
  "import": {"supports_multi_key": true, "supports_strict": true, "supports_partial": true}
}`,
	})
	initGitRepo(t, repoRoot)

	if _, err := store.ProviderDrafts().Create(ctx, sqlite.ProviderDraft{
		DraftID:             "draft_publish_001",
		PackID:              "openai-cn-pack",
		ProviderID:          "openai-zhongzhuan",
		DisplayName:         "OpenAI 中转",
		Platform:            "openai",
		BaseURL:             "https://api.example.com/v1",
		SmokeTestModel:      "gpt-5.4",
		SupportedModelsJSON: `["gpt-5.4","gpt-5.4-mini"]`,
		ManifestJSON:        `{"provider_id":"openai-zhongzhuan","display_name":"OpenAI 中转","platform":"openai","base_url":"https://api.example.com/v1","smoke_test_model":"gpt-5.4"}`,
		SourceHostID:        "remote43-current-host",
	}); err != nil {
		t.Fatalf("ProviderDrafts().Create() error = %v", err)
	}

	t.Setenv("SUB2API_CRM_REPO_ROOT", repoRoot)
	actions := NewActionSet(appTestDSN(t, store))
	result, err := actions.PublishProviderDraft(ctx, PublishProviderDraftRequest{
		DraftID:       "draft_publish_001",
		CommitMessage: "feat(pack): publish provider draft openai-zhongzhuan",
	})
	if err != nil {
		t.Fatalf("PublishProviderDraft() error = %v", err)
	}
	if result.PublishMode != "created" {
		t.Fatalf("PublishMode = %q, want created", result.PublishMode)
	}
	if result.PackVersionAfter != "1.1.5" {
		t.Fatalf("PackVersionAfter = %q, want 1.1.5", result.PackVersionAfter)
	}
	if strings.TrimSpace(result.CommitSHA) == "" {
		t.Fatal("CommitSHA is empty")
	}

	body, err := os.ReadFile(filepath.Join(packDir, "providers", "openai-zhongzhuan.json"))
	if err != nil {
		t.Fatalf("os.ReadFile() provider error = %v", err)
	}
	if !strings.Contains(string(body), `"provider_id": "openai-zhongzhuan"`) {
		t.Fatalf("provider body = %s, want openai-zhongzhuan", string(body))
	}
}

func createPackFixtureAt(t *testing.T, packDir string, files map[string]string) {
	t.Helper()

	lines := make([]string, 0, len(files))
	for relativePath, content := range files {
		absolutePath := filepath.Join(packDir, relativePath)
		if err := os.MkdirAll(filepath.Dir(absolutePath), 0o755); err != nil {
			t.Fatalf("os.MkdirAll(%q) error = %v", absolutePath, err)
		}
		if err := os.WriteFile(absolutePath, []byte(content), 0o644); err != nil {
			t.Fatalf("os.WriteFile(%q) error = %v", absolutePath, err)
		}
		sum := sha256.Sum256([]byte(content))
		lines = append(lines, hex.EncodeToString(sum[:])+"  "+relativePath)
	}
	checksumPath := filepath.Join(packDir, "checksums.txt")
	if err := os.WriteFile(checksumPath, []byte(strings.Join(lines, "\n")+"\n"), 0o644); err != nil {
		t.Fatalf("os.WriteFile(%q) error = %v", checksumPath, err)
	}
}

func initGitRepo(t *testing.T, repoRoot string) {
	t.Helper()
	runGitTest(t, repoRoot, "init")
	runGitTest(t, repoRoot, "config", "user.name", "Test User")
	runGitTest(t, repoRoot, "config", "user.email", "test@example.com")
	runGitTest(t, repoRoot, "add", ".")
	runGitTest(t, repoRoot, "commit", "-m", "chore: seed pack fixture")
}

func runGitTest(t *testing.T, repoRoot string, args ...string) {
	t.Helper()
	cmd := exec.Command("git", append([]string{"-C", repoRoot}, args...)...)
	output, err := cmd.CombinedOutput()
	if err != nil {
		t.Fatalf("git %v error = %v: %s", args, err, strings.TrimSpace(string(output)))
	}
}
