package web

import (
	"archive/zip"
	"bytes"
	"context"
	"crypto/aes"
	"crypto/cipher"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"errors"
	"image/color"
	"image/png"
	"io"
	"mime/multipart"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"testing"
	"time"

	"ymhut-box/server/unified-management/internal/auth"
	"ymhut-box/server/unified-management/internal/config"
	"ymhut-box/server/unified-management/internal/db"
	"ymhut-box/server/unified-management/internal/feedback"
	"ymhut-box/server/unified-management/internal/legacy"
	"ymhut-box/server/unified-management/internal/notices"
	"ymhut-box/server/unified-management/internal/releases"
	"ymhut-box/server/unified-management/internal/sources"
)

func TestCompatibilityRoutes(t *testing.T) {
	handler, cleanup := testRouter(t)
	defer cleanup()

	for _, path := range []string{"/api/client/bootstrap", "/update-info.json", "/update-info", "/tool-status.json", "/tool-status", "/media-types.json", "/media-types", "/modules.json", "/modules", "/api/modules"} {
		req := httptest.NewRequest(http.MethodGet, path, nil)
		res := httptest.NewRecorder()
		handler.ServeHTTP(res, req)
		if res.Code != http.StatusOK {
			t.Fatalf("%s returned %d: %s", path, res.Code, res.Body.String())
		}
		var payload map[string]any
		if err := json.Unmarshal(res.Body.Bytes(), &payload); err != nil {
			t.Fatalf("%s did not return JSON: %v", path, err)
		}
	}
}

func TestLegacyPublicContractFields(t *testing.T) {
	handler, cleanup := testRouter(t)
	defer cleanup()

	cases := []struct {
		Path         string
		RequiredKeys []string
	}{
		{Path: "/update-info.json", RequiredKeys: []string{"app_version", "manifest_version", "packages", "modules"}},
		{Path: "/update-info", RequiredKeys: []string{"app_version", "manifest_version", "packages", "modules"}},
		{Path: "/tool-status.json", RequiredKeys: []string{"ok"}},
		{Path: "/tool-status", RequiredKeys: []string{"ok"}},
		{Path: "/modules.json", RequiredKeys: []string{"modules"}},
		{Path: "/modules", RequiredKeys: []string{"modules"}},
		{Path: "/api/modules", RequiredKeys: []string{"modules"}},
		{Path: "/media-types.json", RequiredKeys: []string{"layout_version", "last_updated", "categories"}},
		{Path: "/media-types", RequiredKeys: []string{"layout_version", "last_updated", "categories"}},
	}
	for _, tc := range cases {
		req := httptest.NewRequest(http.MethodGet, tc.Path, nil)
		res := httptest.NewRecorder()
		handler.ServeHTTP(res, req)
		if res.Code != http.StatusOK {
			t.Fatalf("%s returned %d: %s", tc.Path, res.Code, res.Body.String())
		}
		var payload map[string]any
		if err := json.Unmarshal(res.Body.Bytes(), &payload); err != nil {
			t.Fatalf("%s did not return JSON: %v", tc.Path, err)
		}
		assertJSONKeys(t, tc.Path, payload, tc.RequiredKeys)
	}

	req := httptest.NewRequest(http.MethodGet, "/downloads/fixture.txt", nil)
	res := httptest.NewRecorder()
	handler.ServeHTTP(res, req)
	if res.Code != http.StatusOK {
		t.Fatalf("download returned %d: %s", res.Code, res.Body.String())
	}
	if strings.TrimSpace(res.Body.String()) != "download fixture" {
		t.Fatalf("unexpected download body: %q", res.Body.String())
	}
}

func TestDownloadRejectsPathEscape(t *testing.T) {
	handler, cleanup := testRouter(t)
	defer cleanup()

	for _, path := range []string{"/downloads/../update-info.json", "/downloads/%2e%2e/update-info.json", "/downloads/foo\\bar.exe"} {
		req := httptest.NewRequest(http.MethodGet, path, nil)
		res := httptest.NewRecorder()
		handler.ServeHTTP(res, req)
		if res.Code != http.StatusForbidden && res.Code != http.StatusNotFound {
			t.Fatalf("%s returned %d, want forbidden or not found: %s", path, res.Code, res.Body.String())
		}
	}
}

func TestClientBootstrapAndEndpointsShape(t *testing.T) {
	handler, cleanup := testRouter(t)
	defer cleanup()

	for _, path := range []string{"/api/client/bootstrap", "/api/client/endpoints", "/api/client/sources", "/api/client/notices"} {
		req := httptest.NewRequest(http.MethodGet, path, nil)
		res := httptest.NewRecorder()
		handler.ServeHTTP(res, req)
		if res.Code != http.StatusOK {
			t.Fatalf("%s returned %d: %s", path, res.Code, res.Body.String())
		}
		var payload map[string]any
		if err := json.Unmarshal(res.Body.Bytes(), &payload); err != nil {
			t.Fatal(err)
		}
		if path == "/api/client/sources" {
			if payload["categories"] == nil {
				t.Fatalf("%s missing categories: %#v", path, payload)
			}
			continue
		}
		if path == "/api/client/bootstrap" {
			branding, ok := payload["branding"].(map[string]any)
			if !ok {
				t.Fatalf("bootstrap missing branding: %#v", payload)
			}
			if branding["developerName"] != "YMhut" || branding["feedbackEmail"] != "support@ymhut.cn" {
				t.Fatalf("unexpected branding defaults: %#v", branding)
			}
		}
		if payload["ok"] != true {
			t.Fatalf("%s missing ok=true: %#v", path, payload)
		}
	}
}

func TestAdminDeleteSourcePublishesCompatibilityJSON(t *testing.T) {
	handler, cleanup := testRouter(t)
	defer cleanup()
	session, csrf, err := loginForTest(handler)
	if err != nil {
		t.Fatal(err)
	}

	req := httptest.NewRequest(http.MethodDelete, "/api/admin/sources/demo", nil)
	req.AddCookie(&http.Cookie{Name: auth.SessionCookie, Value: session})
	req.Header.Set("X-CSRF-Token", csrf)
	res := httptest.NewRecorder()
	handler.ServeHTTP(res, req)
	if res.Code != http.StatusOK {
		t.Fatalf("delete source returned %d: %s", res.Code, res.Body.String())
	}

	mediaReq := httptest.NewRequest(http.MethodGet, "/media-types.json", nil)
	mediaRes := httptest.NewRecorder()
	handler.ServeHTTP(mediaRes, mediaReq)
	if mediaRes.Code != http.StatusOK {
		t.Fatalf("media-types returned %d: %s", mediaRes.Code, mediaRes.Body.String())
	}
	if strings.Contains(mediaRes.Body.String(), `"demo"`) {
		t.Fatalf("deleted source leaked into media-types.json: %s", mediaRes.Body.String())
	}

	updateReq := httptest.NewRequest(http.MethodGet, "/update-info.json", nil)
	updateRes := httptest.NewRecorder()
	handler.ServeHTTP(updateRes, updateReq)
	if updateRes.Code != http.StatusOK {
		t.Fatalf("update-info returned %d: %s", updateRes.Code, updateRes.Body.String())
	}
	var updatePayload map[string]any
	if err := json.Unmarshal(updateRes.Body.Bytes(), &updatePayload); err != nil {
		t.Fatal(err)
	}
	assertJSONKeys(t, "update-info after source delete", updatePayload, []string{"app_version", "manifest_version", "packages", "modules"})
}

func TestAdminAuditPaginationAndBranding(t *testing.T) {
	handler, cleanup := testRouter(t)
	defer cleanup()
	session, csrf, err := loginForTest(handler)
	if err != nil {
		t.Fatal(err)
	}

	for i := 0; i < 40; i++ {
		body := strings.NewReader(`{"developerName":"YMhut","feedbackEmail":"support@ymhut.cn"}`)
		req := httptest.NewRequest(http.MethodPost, "/api/admin/system/branding", body)
		req.AddCookie(&http.Cookie{Name: auth.SessionCookie, Value: session})
		req.Header.Set("X-CSRF-Token", csrf)
		req.Header.Set("Content-Type", "application/json")
		res := httptest.NewRecorder()
		handler.ServeHTTP(res, req)
		if res.Code != http.StatusOK {
			t.Fatalf("branding save %d returned %d: %s", i, res.Code, res.Body.String())
		}
	}

	req := httptest.NewRequest(http.MethodGet, "/api/admin/system/audit?page=1&perPage=35&type=system.branding.saved", nil)
	req.AddCookie(&http.Cookie{Name: auth.SessionCookie, Value: session})
	res := httptest.NewRecorder()
	handler.ServeHTTP(res, req)
	if res.Code != http.StatusOK {
		t.Fatalf("audit returned %d: %s", res.Code, res.Body.String())
	}
	var payload struct {
		Items []any `json:"items"`
		Page  struct {
			Total   int `json:"total"`
			Page    int `json:"page"`
			PerPage int `json:"perPage"`
		} `json:"page"`
	}
	if err := json.Unmarshal(res.Body.Bytes(), &payload); err != nil {
		t.Fatal(err)
	}
	if payload.Page.Page != 1 || payload.Page.PerPage != 35 {
		t.Fatalf("unexpected audit page metadata: %#v", payload.Page)
	}
	if payload.Page.Total < 40 {
		t.Fatalf("expected at least 40 branding audit records, got %d", payload.Page.Total)
	}
	if len(payload.Items) > 35 {
		t.Fatalf("expected at most 35 audit items, got %d", len(payload.Items))
	}
}

func TestLegacyFeedbackStatusDTOContract(t *testing.T) {
	payload := legacyFeedbackStatus(db.Feedback{
		Code:                 "FB-20260626-ABCDEF",
		Status:               "processing",
		StatusDetail:         "公开进度",
		Category:             "issue",
		Priority:             "normal",
		PublicReply:          "公开回复",
		Note:                 "内部备注",
		Assignee:             "owner",
		HandledBy:            "admin",
		Attachment:           "private.zip",
		PackagePath:          "storage/feedback/private.zip",
		EncryptedPackagePath: "storage/feedback/private.ymfb",
		MailSent:             true,
		CreatedAt:            "2026-06-26T00:00:00Z",
		UpdatedAt:            "2026-06-26T00:10:00Z",
		LastActivityAt:       "2026-06-26T00:20:00Z",
	}, true)
	data, err := json.Marshal(payload)
	if err != nil {
		t.Fatal(err)
	}
	var out map[string]any
	if err := json.Unmarshal(data, &out); err != nil {
		t.Fatal(err)
	}
	assertJSONKeys(t, "legacy feedback status", out, []string{"ok", "code", "status", "statusLabel", "statusDetail", "category", "priority", "hasReply", "reply", "receivedAt", "updatedAt", "mailSent", "duplicate"})
	for _, privateKey := range []string{"note", "assignee", "handledBy", "attachments", "events", "legacyEvents", "mailRecords", "path", "attachment", "packagePath", "encryptedPackagePath", "packageSha256", "plainPackageSha256"} {
		if _, ok := out[privateKey]; ok {
			t.Fatalf("legacy DTO leaked private key %q: %#v", privateKey, out)
		}
	}
}

func TestLegacyFeedbackPublicStatusShape(t *testing.T) {
	handler, cleanup := testRouter(t)
	defer cleanup()

	req := httptest.NewRequest(http.MethodPost, "/", bytes.NewBufferString(`{"title":"旧版反馈","type":"issue","severity":"normal","body":"客户端反馈内容"}`))
	req.Header.Set("Content-Type", "application/json")
	res := httptest.NewRecorder()
	handler.ServeHTTP(res, req)
	if res.Code != http.StatusOK {
		t.Fatalf("submit returned %d: %s", res.Code, res.Body.String())
	}
	var submitted map[string]any
	if err := json.Unmarshal(res.Body.Bytes(), &submitted); err != nil {
		t.Fatal(err)
	}
	code, _ := submitted["code"].(string)
	if code == "" || submitted["statusLabel"] == nil || submitted["feedback"] != nil {
		t.Fatalf("unexpected submit payload: %#v", submitted)
	}

	req = httptest.NewRequest(http.MethodGet, "/?api=status&code="+code, nil)
	res = httptest.NewRecorder()
	handler.ServeHTTP(res, req)
	if res.Code != http.StatusOK {
		t.Fatalf("status returned %d: %s", res.Code, res.Body.String())
	}
	var status map[string]any
	if err := json.Unmarshal(res.Body.Bytes(), &status); err != nil {
		t.Fatal(err)
	}
	if status["code"] != code || status["statusLabel"] == nil || status["feedback"] != nil {
		t.Fatalf("unexpected status payload: %#v", status)
	}
	for _, privateKey := range []string{"note", "assignee", "handledBy", "attachment", "attachments", "path", "packagePath", "encryptedPackagePath", "events", "legacyEvents", "mailRecords", "packageSha256", "plainPackageSha256"} {
		if _, ok := status[privateKey]; ok {
			t.Fatalf("status leaked private key %q: %#v", privateKey, status)
		}
	}
}

func TestLegacyFeedbackMultipartFallback(t *testing.T) {
	handler, cleanup := testRouter(t)
	defer cleanup()

	body := &bytes.Buffer{}
	writer := multipart.NewWriter(body)
	_ = writer.WriteField("subject", "Multipart legacy feedback")
	_ = writer.WriteField("category", "issue")
	_ = writer.WriteField("priority", "normal")
	_ = writer.WriteField("email", "user@example.com")
	_ = writer.WriteField("message", "Submitted by an old multipart client.")
	if part, err := writer.CreateFormFile("ignored", "note.txt"); err == nil {
		_, _ = io.WriteString(part, "not signed, should fall back")
	}
	_ = writer.Close()

	req := httptest.NewRequest(http.MethodPost, "/", body)
	req.Header.Set("Content-Type", writer.FormDataContentType())
	res := httptest.NewRecorder()
	handler.ServeHTTP(res, req)
	if res.Code != http.StatusOK {
		t.Fatalf("multipart submit returned %d: %s", res.Code, res.Body.String())
	}
	var payload map[string]any
	if err := json.Unmarshal(res.Body.Bytes(), &payload); err != nil {
		t.Fatal(err)
	}
	if payload["code"] == "" || payload["feedback"] != nil {
		t.Fatalf("unexpected multipart payload: %#v", payload)
	}
}

func TestLegacyFeedbackSignedEncryptedMultipartRoute(t *testing.T) {
	handler, cleanup := testRouter(t)
	defer cleanup()

	plain := routeZipBytes(t, map[string]string{
		"feedback.json": `{"request":{"title":"Signed route feedback","type":"issue","severity":"major","contact":"user@example.com","body":"Signed package body."}}`,
		"summary.txt":   "signed route summary",
	})
	encrypted := routeEncryptPackage(t, plain, "ymhut-box-feedback-package-v1")
	encryptedHash := routeSHA256Hex(encrypted)
	plainHash := routeSHA256Hex(plain)
	payloadData, err := json.Marshal(map[string]any{
		"feedbackCode":       "FB-20260626-ABC123",
		"title":              "Signed route feedback",
		"type":               "issue",
		"severity":           "major",
		"contact":            "user@example.com",
		"bodyLength":         20,
		"packageEncrypted":   true,
		"encryption":         feedback.PackageMagic,
		"packageBytes":       len(encrypted),
		"packageSha256":      encryptedHash,
		"plainPackageBytes":  len(plain),
		"plainPackageSha256": plainHash,
		"createdAt":          "2026-06-26T00:00:00Z",
	})
	if err != nil {
		t.Fatal(err)
	}
	payload := string(payloadData)
	timestamp := strconv.FormatInt(time.Now().Unix(), 10)
	body := &bytes.Buffer{}
	writer := multipart.NewWriter(body)
	_ = writer.WriteField("payload", payload)
	_ = writer.WriteField("timestamp", timestamp)
	_ = writer.WriteField("nonce", "route-test")
	_ = writer.WriteField("packageSha256", encryptedHash)
	_ = writer.WriteField("signature", feedback.SignWithKey("ymhut-box-feedback-client-v1", timestamp, "route-test", encryptedHash, payload))
	part, err := writer.CreateFormFile("package", "feedback.ymfb")
	if err != nil {
		t.Fatal(err)
	}
	_, _ = io.Copy(part, bytes.NewReader(encrypted))
	_ = writer.Close()

	req := httptest.NewRequest(http.MethodPost, "/", body)
	req.Header.Set("Content-Type", writer.FormDataContentType())
	res := httptest.NewRecorder()
	handler.ServeHTTP(res, req)
	if res.Code != http.StatusOK {
		t.Fatalf("signed multipart submit returned %d: %s", res.Code, res.Body.String())
	}
	var submitted map[string]any
	if err := json.Unmarshal(res.Body.Bytes(), &submitted); err != nil {
		t.Fatal(err)
	}
	if submitted["code"] != "FB-20260626-ABC123" || submitted["duplicate"] != nil {
		t.Fatalf("unexpected signed submit payload: %#v", submitted)
	}

	req = httptest.NewRequest(http.MethodGet, "/?api=status&code=FB-20260626-ABC123", nil)
	res = httptest.NewRecorder()
	handler.ServeHTTP(res, req)
	if res.Code != http.StatusOK {
		t.Fatalf("signed status returned %d: %s", res.Code, res.Body.String())
	}
	var status map[string]any
	if err := json.Unmarshal(res.Body.Bytes(), &status); err != nil {
		t.Fatal(err)
	}
	if status["code"] != "FB-20260626-ABC123" || status["statusLabel"] == nil || status["reply"] == nil {
		t.Fatalf("unexpected signed status payload: %#v", status)
	}
	for _, privateKey := range []string{"note", "assignee", "handledBy", "attachments", "events", "mailRecords", "packagePath", "encryptedPackagePath", "path"} {
		if _, ok := status[privateKey]; ok {
			t.Fatalf("signed status leaked private key %q: %#v", privateKey, status)
		}
	}
}

func TestBuiltFrontendAssetsAreServed(t *testing.T) {
	handler, cleanup := testRouter(t)
	defer cleanup()

	for _, item := range []struct {
		Path         string
		ContentTypes []string
	}{
		{Path: "/assets/portal.css", ContentTypes: []string{"text/css"}},
		{Path: "/assets/portal.js", ContentTypes: []string{"text/javascript", "application/javascript"}},
		{Path: "/admin/assets/admin.css", ContentTypes: []string{"text/css"}},
		{Path: "/admin/assets/admin.js", ContentTypes: []string{"text/javascript", "application/javascript"}},
	} {
		req := httptest.NewRequest(http.MethodGet, item.Path, nil)
		res := httptest.NewRecorder()
		handler.ServeHTTP(res, req)
		if res.Code != http.StatusOK {
			t.Fatalf("%s returned %d: %s", item.Path, res.Code, res.Body.String())
		}
		if got := res.Header().Get("Content-Type"); !containsAny(got, item.ContentTypes) {
			t.Fatalf("%s content type = %q, want one of %v", item.Path, got, item.ContentTypes)
		}
	}
}

func TestAdminSystemAndLegacyAdminPagesServeSPA(t *testing.T) {
	handler, cleanup := testRouter(t)
	defer cleanup()

	for _, path := range []string{"/admin/system", "/admin/database", "/admin/health", "/admin/settings", "/admin/audit"} {
		req := httptest.NewRequest(http.MethodGet, path, nil)
		res := httptest.NewRecorder()
		handler.ServeHTTP(res, req)
		if res.Code != http.StatusOK {
			t.Fatalf("%s returned %d: %s", path, res.Code, res.Body.String())
		}
		if !strings.Contains(res.Body.String(), "/admin/assets/admin.js") {
			t.Fatalf("%s did not serve admin SPA shell: %s", path, res.Body.String())
		}
	}
}

func containsAny(value string, needles []string) bool {
	for _, needle := range needles {
		if strings.Contains(value, needle) {
			return true
		}
	}
	return false
}

func assertJSONKeys(t *testing.T, label string, payload map[string]any, keys []string) {
	t.Helper()
	for _, key := range keys {
		if _, ok := payload[key]; !ok {
			t.Fatalf("%s missing key %q: %#v", label, key, payload)
		}
	}
}

func TestReleaseNoticesRoutes(t *testing.T) {
	handler, cleanup := testRouter(t)
	defer cleanup()

	for _, path := range []string{"/api/client/notices", "/api/client/notices/2.0.0"} {
		req := httptest.NewRequest(http.MethodGet, path, nil)
		res := httptest.NewRecorder()
		handler.ServeHTTP(res, req)
		if res.Code != http.StatusOK {
			t.Fatalf("%s returned %d: %s", path, res.Code, res.Body.String())
		}
	}
	req := httptest.NewRequest(http.MethodGet, "/api/client/releases", nil)
	res := httptest.NewRecorder()
	handler.ServeHTTP(res, req)
	var payload map[string]any
	if err := json.Unmarshal(res.Body.Bytes(), &payload); err != nil {
		t.Fatal(err)
	}
	if payload["notices"] == nil {
		t.Fatalf("release manifest missing notices: %#v", payload)
	}
}

func TestAdminLegacyRequiresAuth(t *testing.T) {
	handler, cleanup := testRouter(t)
	defer cleanup()

	req := httptest.NewRequest(http.MethodPut, "/api/admin/legacy/media-types", bytes.NewBufferString(`{"raw":"{}"}`))
	res := httptest.NewRecorder()
	handler.ServeHTTP(res, req)
	if res.Code != http.StatusUnauthorized {
		t.Fatalf("expected unauthorized, got %d", res.Code)
	}
}

func TestAdminLegacyUpdateInfoSyncsReleaseNotice(t *testing.T) {
	handler, cleanup := testRouter(t)
	defer cleanup()

	session, csrf, err := loginForTest(handler)
	if err != nil {
		t.Fatal(err)
	}
	body, _ := json.Marshal(map[string]string{
		"raw": `{"app_version":"2.0.7.5","title":"YMhut Box 2.0.7.5","message":"随机放映室优化","release_notes":"修复图片源和全屏预览"}`,
	})
	req := httptest.NewRequest(http.MethodPut, "/api/admin/legacy/update-info", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("X-CSRF-Token", csrf)
	req.AddCookie(&http.Cookie{Name: auth.SessionCookie, Value: session})
	res := httptest.NewRecorder()
	handler.ServeHTTP(res, req)
	if res.Code != http.StatusOK {
		t.Fatalf("save update-info returned %d: %s", res.Code, res.Body.String())
	}

	listReq := httptest.NewRequest(http.MethodGet, "/api/admin/releases/notices", nil)
	listReq.AddCookie(&http.Cookie{Name: auth.SessionCookie, Value: session})
	listRes := httptest.NewRecorder()
	handler.ServeHTTP(listRes, listReq)
	if listRes.Code != http.StatusOK {
		t.Fatalf("notice list returned %d: %s", listRes.Code, listRes.Body.String())
	}
	var payload struct {
		Items []struct {
			Version string `json:"version"`
			Title   string `json:"title"`
		} `json:"items"`
	}
	if err := json.Unmarshal(listRes.Body.Bytes(), &payload); err != nil {
		t.Fatal(err)
	}
	found := false
	for _, item := range payload.Items {
		if item.Version == "2.0.7.5" && item.Title == "YMhut Box 2.0.7.5" {
			found = true
		}
	}
	if !found {
		t.Fatalf("synced release notice not found: %#v", payload.Items)
	}
}

func TestAdminLegacyValidationErrorIsChinese(t *testing.T) {
	handler, cleanup := testRouter(t)
	defer cleanup()

	session, csrf, err := loginForTest(handler)
	if err != nil {
		t.Fatal(err)
	}
	body, _ := json.Marshal(map[string]string{"raw": `{"message":"missing version and title"}`})
	req := httptest.NewRequest(http.MethodPost, "/api/admin/legacy/update-info/validate", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("X-CSRF-Token", csrf)
	req.AddCookie(&http.Cookie{Name: auth.SessionCookie, Value: session})
	res := httptest.NewRecorder()
	handler.ServeHTTP(res, req)
	if res.Code != http.StatusBadRequest {
		t.Fatalf("expected validation failure, got %d: %s", res.Code, res.Body.String())
	}
	if strings.Contains(res.Body.String(), "update-info requires app_version or title") {
		t.Fatalf("english validation leaked: %s", res.Body.String())
	}
	if !strings.Contains(res.Body.String(), "更新 JSON 需要填写 app_version 或 title") {
		t.Fatalf("missing chinese validation message: %s", res.Body.String())
	}
}

func TestAdminWriteRequiresCSRF(t *testing.T) {
	handler, cleanup := testRouter(t)
	defer cleanup()

	session, _, err := loginForTest(handler)
	if err != nil {
		t.Fatal(err)
	}
	req := httptest.NewRequest(http.MethodPost, "/api/admin/sources/check", bytes.NewBufferString(`{}`))
	req.AddCookie(&http.Cookie{Name: auth.SessionCookie, Value: session})
	res := httptest.NewRecorder()
	handler.ServeHTTP(res, req)
	if res.Code != http.StatusForbidden {
		t.Fatalf("expected forbidden without csrf, got %d: %s", res.Code, res.Body.String())
	}
}

func loginForTest(handler http.Handler) (string, string, error) {
	captchaReq := httptest.NewRequest(http.MethodGet, "/api/admin/auth/captcha", nil)
	captchaRes := httptest.NewRecorder()
	handler.ServeHTTP(captchaRes, captchaReq)
	if captchaRes.Code != http.StatusOK {
		return "", "", errors.New(captchaRes.Body.String())
	}
	var captchaPayload struct {
		CaptchaID string `json:"captchaId"`
		Image     string `json:"image"`
	}
	if err := json.Unmarshal(captchaRes.Body.Bytes(), &captchaPayload); err != nil {
		return "", "", err
	}
	answer, err := readTestCaptcha(captchaPayload.Image)
	if err != nil {
		return "", "", err
	}
	loginBody, _ := json.Marshal(map[string]string{
		"username":  "admin",
		"password":  "admin",
		"captchaId": captchaPayload.CaptchaID,
		"captcha":   answer,
	})
	loginReq := httptest.NewRequest(http.MethodPost, "/api/admin/auth/login", bytes.NewReader(loginBody))
	loginReq.Header.Set("Content-Type", "application/json")
	loginRes := httptest.NewRecorder()
	handler.ServeHTTP(loginRes, loginReq)
	if loginRes.Code != http.StatusOK {
		return "", "", errors.New(loginRes.Body.String())
	}
	var loginPayload struct {
		OK        bool   `json:"ok"`
		CSRFToken string `json:"csrfToken"`
		Message   string `json:"message"`
	}
	if err := json.Unmarshal(loginRes.Body.Bytes(), &loginPayload); err != nil {
		return "", "", err
	}
	if !loginPayload.OK {
		return "", "", errors.New(loginPayload.Message)
	}
	for _, cookie := range loginRes.Result().Cookies() {
		if cookie.Name == auth.SessionCookie {
			return cookie.Value, loginPayload.CSRFToken, nil
		}
	}
	return "", "", errors.New("session cookie not set")
}

func readTestCaptcha(dataURL string) (string, error) {
	const prefix = "data:image/png;base64,"
	raw, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(dataURL, prefix))
	if err != nil {
		return "", err
	}
	img, err := png.Decode(bytes.NewReader(raw))
	if err != nil {
		return "", err
	}
	var builder strings.Builder
	for index := 0; index < 5; index++ {
		x := 18 + index*32
		y := 13
		mask := [7]bool{
			isCaptchaInk(img.At(x+11, y+2)),
			isCaptchaInk(img.At(x+20, y+12)),
			isCaptchaInk(img.At(x+20, y+28)),
			isCaptchaInk(img.At(x+11, y+34)),
			isCaptchaInk(img.At(x+2, y+28)),
			isCaptchaInk(img.At(x+2, y+12)),
			isCaptchaInk(img.At(x+11, y+18)),
		}
		digit := -1
		for candidate, segments := range testCaptchaSegments {
			if segments == mask {
				digit = candidate
				break
			}
		}
		if digit < 0 {
			return "", errors.New("captcha digit could not be read")
		}
		builder.WriteByte(byte('0' + digit))
	}
	return builder.String(), nil
}

func isCaptchaInk(colorValue color.Color) bool {
	r, g, b, _ := colorValue.RGBA()
	return r>>8 < 80 && g>>8 < 100 && b>>8 < 130
}

var testCaptchaSegments = [10][7]bool{
	{true, true, true, true, true, true, false},
	{false, true, true, false, false, false, false},
	{true, true, false, true, true, false, true},
	{true, true, true, true, false, false, true},
	{false, true, true, false, false, true, true},
	{true, false, true, true, false, true, true},
	{true, false, true, true, true, true, true},
	{true, true, true, false, false, false, false},
	{true, true, true, true, true, true, true},
	{true, true, true, true, false, true, true},
}

func testRouter(t *testing.T) (http.Handler, func()) {
	t.Helper()
	root := t.TempDir()
	public := filepath.Join(root, "public")
	noticeDir := filepath.Join(root, "update-notice")
	if err := os.MkdirAll(filepath.Join(public, "downloads"), 0o755); err != nil {
		t.Fatal(err)
	}
	adminDist := filepath.Join(root, "admin")
	portalDist := filepath.Join(root, "portal")
	for _, dir := range []string{filepath.Join(adminDist, "assets"), filepath.Join(portalDist, "assets")} {
		if err := os.MkdirAll(dir, 0o755); err != nil {
			t.Fatal(err)
		}
	}
	if err := os.WriteFile(filepath.Join(portalDist, "index.html"), []byte(`<!doctype html><link rel="stylesheet" href="/assets/portal.css"><script type="module" src="/assets/portal.js"></script>`), 0o644); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(portalDist, "assets", "portal.css"), []byte(`body{}`), 0o644); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(portalDist, "assets", "portal.js"), []byte(`console.log("portal")`), 0o644); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(adminDist, "index.html"), []byte(`<!doctype html><link rel="stylesheet" href="/admin/assets/admin.css"><script type="module" src="/admin/assets/admin.js"></script>`), 0o644); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(adminDist, "assets", "admin.css"), []byte(`body{}`), 0o644); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(adminDist, "assets", "admin.js"), []byte(`console.log("admin")`), 0o644); err != nil {
		t.Fatal(err)
	}
	if err := os.MkdirAll(noticeDir, 0o755); err != nil {
		t.Fatal(err)
	}
	mustWriteJSON(t, filepath.Join(public, "update-info.json"), map[string]any{"app_version": "0.0.1"})
	mustWriteJSON(t, filepath.Join(public, "tool-status.json"), map[string]any{"ok": true})
	mustWriteJSON(t, filepath.Join(public, "modules.json"), map[string]any{"modules": []any{}})
	mustWriteJSON(t, filepath.Join(public, "media-types.json"), map[string]any{
		"categories": []map[string]any{{
			"id": "image", "name": "image",
			"subcategories": []map[string]any{{"id": "demo", "name": "demo", "api_url": "https://example.com/demo"}},
		}},
	})
	if err := os.WriteFile(filepath.Join(public, "downloads", "fixture.txt"), []byte("download fixture\n"), 0o644); err != nil {
		t.Fatal(err)
	}
	mustWriteJSON(t, filepath.Join(noticeDir, "total.json"), map[string]any{
		"schema_version":     1,
		"latest_version":     "2.0.0",
		"latest_notice_file": "2.0.0.json",
		"latest":             map[string]any{"version": "2.0.0", "title": "YMhut Box 2.0.0", "release_notes": "Initial release"},
		"versions":           []map[string]any{{"version": "2.0.0", "notice_file": "2.0.0.json", "summary": "Initial release"}},
	})
	mustWriteJSON(t, filepath.Join(noticeDir, "2.0.0.json"), map[string]any{"app_version": "2.0.0", "title": "YMhut Box 2.0.0", "release_notes": "Initial release", "release_notes_md": "## Initial"})
	cfg := &config.Config{
		BaseDir:                root,
		ConfigPath:             filepath.Join(root, "config.json"),
		Listen:                 ":0",
		BaseURL:                "https://update.ymhut.cn",
		StorageDir:             filepath.Join(root, "storage"),
		DataDir:                filepath.Join(root, "data"),
		UpdatePublicDir:        public,
		UpdateNoticeDir:        noticeDir,
		DownloadsDir:           filepath.Join(public, "downloads"),
		AdminWebDir:            adminDist,
		PortalWebDir:           portalDist,
		SourceCheckSeconds:     3600,
		ClientSignatureKey:     "ymhut-box-feedback-client-v1",
		PackageEncryptionKey:   "ymhut-box-feedback-package-v1",
		TimestampWindowSeconds: 600,
		MaxRequestBytes:        12 << 20,
		MaxPackageBytes:        10 << 20,
		Database: config.DatabaseConfig{
			Provider:          "sqlite",
			SQLitePath:        filepath.Join(root, "storage", "unified.sqlite"),
			FailoverEnabled:   true,
			HotSyncEnabled:    true,
			HealthIntervalSec: 3600,
		},
		UploadGuard: config.UploadGuardConfig{MaxZipFiles: 80, MaxDecompressedBytes: 30 << 20, MaxSingleFileBytes: 8 << 20, MaxCompressionRatio: 120, MaxReadableTextBytes: 256 << 10, AllowUnexpectedZipFiles: true},
	}
	store, err := db.Open(cfg)
	if err != nil {
		t.Fatal(err)
	}
	if err := store.EnsureDefaultAdmin(context.Background()); err != nil {
		t.Fatal(err)
	}
	sourceService := sources.NewService(cfg, store)
	if err := sourceService.ImportLegacyMediaTypesIfEmpty(context.Background()); err != nil {
		t.Fatal(err)
	}
	noticeService := notices.NewService(cfg, store)
	if err := noticeService.Import(context.Background()); err != nil {
		t.Fatal(err)
	}
	handler := NewRouter(
		cfg,
		store,
		auth.NewService(store),
		feedback.NewService(cfg, store),
		releases.NewService(cfg, store, noticeService),
		sourceService,
		legacy.NewService(cfg, store),
		noticeService,
	)
	return handler, func() { _ = store.Close() }
}

func routeZipBytes(t *testing.T, files map[string]string) []byte {
	t.Helper()
	var buf bytes.Buffer
	writer := zip.NewWriter(&buf)
	for name, body := range files {
		entry, err := writer.Create(name)
		if err != nil {
			t.Fatal(err)
		}
		_, _ = entry.Write([]byte(body))
	}
	if err := writer.Close(); err != nil {
		t.Fatal(err)
	}
	return buf.Bytes()
}

func routeEncryptPackage(t *testing.T, plain []byte, keyMaterial string) []byte {
	t.Helper()
	key := sha256.Sum256([]byte(keyMaterial))
	block, err := aes.NewCipher(key[:])
	if err != nil {
		t.Fatal(err)
	}
	gcm, err := cipher.NewGCM(block)
	if err != nil {
		t.Fatal(err)
	}
	nonce := []byte("123456789012")
	sealed := gcm.Seal(nil, nonce, plain, []byte(feedback.PackageMagic))
	ciphertext := sealed[:len(sealed)-gcm.Overhead()]
	tag := sealed[len(sealed)-gcm.Overhead():]
	out := []byte(feedback.PackageMagic)
	out = append(out, nonce...)
	out = append(out, tag...)
	out = append(out, ciphertext...)
	return out
}

func routeSHA256Hex(data []byte) string {
	sum := sha256.Sum256(data)
	return hex.EncodeToString(sum[:])
}

func mustWriteJSON(t *testing.T, path string, payload any) {
	t.Helper()
	data, err := json.Marshal(payload)
	if err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(path, data, 0o644); err != nil {
		t.Fatal(err)
	}
}
