package handlers

import (
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"warpbox.dev/backend/libs/services"
)

func TestLoggedInUploadStoresOwnerAndAnonymousUploadDoesNot(t *testing.T) {
	app, cleanup := newTestApp(t)
	defer cleanup()

	user, err := app.authService.CreateBootstrapUser("daniel", "daniel@example.test", "password123")
	if err != nil {
		t.Fatalf("CreateBootstrapUser returned error: %v", err)
	}
	_, token, err := app.authService.Login("daniel@example.test", "password123")
	if err != nil {
		t.Fatalf("Login returned error: %v", err)
	}

	request := multipartUploadRequest(t, "/api/v1/upload", "file", "owned.txt", "owned")
	request.Header.Set("Accept", "application/json")
	request.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: token})
	response := httptest.NewRecorder()
	app.Upload(response, request)
	if response.Code != http.StatusCreated {
		t.Fatalf("owned upload status = %d, body = %s", response.Code, response.Body.String())
	}
	var ownedPayload services.UploadResult
	if err := json.Unmarshal(response.Body.Bytes(), &ownedPayload); err != nil {
		t.Fatalf("json.Unmarshal owned returned error: %v", err)
	}
	ownedBox, err := app.uploadService.GetBox(ownedPayload.BoxID)
	if err != nil {
		t.Fatalf("GetBox owned returned error: %v", err)
	}
	if ownedBox.OwnerID != user.ID {
		t.Fatalf("owned OwnerID = %q, want %q", ownedBox.OwnerID, user.ID)
	}

	owned := uploadThroughApp(t, app)
	anonymous, err := app.uploadService.GetBox(owned.BoxID)
	if err != nil {
		t.Fatalf("GetBox anonymous returned error: %v", err)
	}
	if anonymous.OwnerID != "" {
		t.Fatalf("anonymous OwnerID = %q, want empty", anonymous.OwnerID)
	}

	boxes, err := app.uploadService.ListBoxes(0)
	if err != nil {
		t.Fatalf("ListBoxes returned error: %v", err)
	}
	foundOwned := false
	for _, box := range boxes {
		if box.OwnerID == user.ID {
			foundOwned = true
		}
	}
	if !foundOwned {
		t.Fatalf("logged-in upload did not store owner id %q", user.ID)
	}
}

func TestBearerTokenUploadActsAsUser(t *testing.T) {
	app, cleanup := newTestApp(t)
	defer cleanup()

	user, err := app.authService.CreateBootstrapUser("daniel", "daniel@example.test", "password123")
	if err != nil {
		t.Fatalf("CreateBootstrapUser returned error: %v", err)
	}
	tokenResult, err := app.authService.CreateAPIToken(user.ID, "cli")
	if err != nil {
		t.Fatalf("CreateAPIToken returned error: %v", err)
	}

	request := multipartUploadRequest(t, "/api/v1/upload", "file", "owned.txt", "owned")
	request.Header.Set("Accept", "application/json")
	request.Header.Set("Authorization", "Bearer "+tokenResult.Plaintext)
	response := httptest.NewRecorder()
	app.Upload(response, request)
	if response.Code != http.StatusCreated {
		t.Fatalf("token upload status = %d, body = %s", response.Code, response.Body.String())
	}
	var payload services.UploadResult
	if err := json.Unmarshal(response.Body.Bytes(), &payload); err != nil {
		t.Fatalf("json.Unmarshal returned error: %v", err)
	}
	box, err := app.uploadService.GetBox(payload.BoxID)
	if err != nil {
		t.Fatalf("GetBox returned error: %v", err)
	}
	if box.OwnerID != user.ID {
		t.Fatalf("OwnerID = %q, want %q", box.OwnerID, user.ID)
	}

	// An invalid bearer token is an authentication failure, not an anonymous upload.
	badRequest := multipartUploadRequest(t, "/api/v1/upload", "file", "x.txt", "x")
	badRequest.Header.Set("Accept", "application/json")
	badRequest.Header.Set("Authorization", "Bearer wbx_bogus.secret")
	badResponse := httptest.NewRecorder()
	app.Upload(badResponse, badRequest)
	if badResponse.Code != http.StatusUnauthorized {
		t.Fatalf("invalid token upload status = %d, body = %s", badResponse.Code, badResponse.Body.String())
	}
}

func TestAnonymousUploadWithoutBearerStillWorks(t *testing.T) {
	app, cleanup := newTestApp(t)
	defer cleanup()

	response := httptest.NewRecorder()
	app.Upload(response, multipartUploadRequest(t, "/api/v1/upload", "file", "anonymous.txt", "anonymous"))
	if response.Code != http.StatusCreated {
		t.Fatalf("anonymous upload status = %d, body = %s", response.Code, response.Body.String())
	}
}

func TestDisabledUserBearerTokenCannotUpload(t *testing.T) {
	app, cleanup := newTestApp(t)
	defer cleanup()

	user, err := app.authService.CreateBootstrapUser("daniel", "daniel@example.test", "password123")
	if err != nil {
		t.Fatalf("CreateBootstrapUser returned error: %v", err)
	}
	tokenResult, err := app.authService.CreateAPIToken(user.ID, "cli")
	if err != nil {
		t.Fatalf("CreateAPIToken returned error: %v", err)
	}
	if err := app.authService.DisableUser(user.ID, true); err != nil {
		t.Fatalf("DisableUser returned error: %v", err)
	}

	request := multipartUploadRequest(t, "/api/v1/upload", "file", "blocked.txt", "blocked")
	request.Header.Set("Accept", "application/json")
	request.Header.Set("Authorization", "Bearer "+tokenResult.Plaintext)
	response := httptest.NewRecorder()
	app.Upload(response, request)
	if response.Code != http.StatusUnauthorized {
		t.Fatalf("disabled bearer upload status = %d, body = %s", response.Code, response.Body.String())
	}
}

func TestInviteHandlerCreatesUserAndMarksInviteUsed(t *testing.T) {
	app, cleanup := newTestApp(t)
	defer cleanup()

	admin, err := app.authService.CreateBootstrapUser("admin", "admin@example.test", "password123")
	if err != nil {
		t.Fatalf("CreateBootstrapUser returned error: %v", err)
	}
	invite, err := app.authService.CreateInvite("friend@example.test", services.UserRoleUser, admin.ID, 0)
	if err != nil {
		t.Fatalf("CreateInvite returned error: %v", err)
	}

	request := httptest.NewRequest(http.MethodPost, "/invite/"+invite.Token, strings.NewReader("username=friend&password=password123"))
	request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	request.SetPathValue("token", invite.Token)
	response := httptest.NewRecorder()
	app.InvitePost(response, request)
	if response.Code != http.StatusSeeOther {
		t.Fatalf("InvitePost status = %d, body = %s", response.Code, response.Body.String())
	}
	if _, err := app.authService.AcceptInvite(invite.Token, "friend", "password123"); err == nil {
		t.Fatalf("invite token remained reusable")
	}
}

func TestNonOwnerCannotManageOwnedBox(t *testing.T) {
	app, cleanup := newTestApp(t)
	defer cleanup()

	owner, err := app.authService.CreateBootstrapUser("owner", "owner@example.test", "password123")
	if err != nil {
		t.Fatalf("CreateBootstrapUser returned error: %v", err)
	}
	invite, err := app.authService.CreateInvite("other@example.test", services.UserRoleUser, owner.ID, 0)
	if err != nil {
		t.Fatalf("CreateInvite returned error: %v", err)
	}
	other, err := app.authService.AcceptInvite(invite.Token, "other", "password123")
	if err != nil {
		t.Fatalf("AcceptInvite returned error: %v", err)
	}

	result := createOwnedBoxThroughApp(t, app, owner.ID)
	if err := app.uploadService.RenameOwnedBox(result.BoxID, other.ID, "stolen"); err == nil {
		t.Fatalf("RenameOwnedBox allowed non-owner")
	}
}

func TestAdminUploadBypassesMaxUploadSize(t *testing.T) {
	app, cleanup := newTestApp(t)
	defer cleanup()

	_, err := app.authService.CreateBootstrapUser("admin", "admin@example.test", "password123")
	if err != nil {
		t.Fatalf("CreateBootstrapUser returned error: %v", err)
	}
	_, token, err := app.authService.Login("admin@example.test", "password123")
	if err != nil {
		t.Fatalf("Login returned error: %v", err)
	}

	request := multipartUploadRequest(t, "/api/v1/upload", "file", "large.txt", strings.Repeat("x", int(app.uploadService.MaxUploadSize())+1))
	request.Header.Set("Accept", "application/json")
	request.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: token})
	response := httptest.NewRecorder()
	app.Upload(response, request)
	if response.Code != http.StatusCreated {
		t.Fatalf("admin upload status = %d, body = %s", response.Code, response.Body.String())
	}
}

func TestUnlimitedAnonymousUploadPolicyUsesNegativeOne(t *testing.T) {
	app, cleanup := newTestApp(t)
	defer cleanup()

	policy, err := app.settingsService.UploadPolicy()
	if err != nil {
		t.Fatalf("UploadPolicy returned error: %v", err)
	}
	policy.AnonymousMaxUploadMB = -1
	policy.AnonymousDailyUploadMB = -1
	if err := app.settingsService.UpdateUploadPolicy(policy); err != nil {
		t.Fatalf("UpdateUploadPolicy returned error: %v", err)
	}

	request := multipartUploadRequest(t, "/api/v1/upload", "file", "large.txt", strings.Repeat("x", int(app.uploadService.MaxUploadSize())+1))
	request.Header.Set("Accept", "application/json")
	response := httptest.NewRecorder()
	app.Upload(response, request)
	if response.Code != http.StatusCreated {
		t.Fatalf("unlimited anonymous upload status = %d, body = %s", response.Code, response.Body.String())
	}
}

func TestAnonymousUploadDisabled(t *testing.T) {
	app, cleanup := newTestApp(t)
	defer cleanup()
	policy := testPolicy(t, app)
	policy.AnonymousUploadsEnabled = false
	if err := app.settingsService.UpdateUploadPolicy(policy); err != nil {
		t.Fatalf("UpdateUploadPolicy returned error: %v", err)
	}

	request := multipartUploadRequest(t, "/api/v1/upload", "file", "note.txt", "hello")
	request.Header.Set("Accept", "application/json")
	response := httptest.NewRecorder()
	app.Upload(response, request)
	if response.Code != http.StatusForbidden {
		t.Fatalf("status = %d, want 403, body = %s", response.Code, response.Body.String())
	}
}

func TestAnonymousUploadLimits(t *testing.T) {
	app, cleanup := newTestApp(t)
	defer cleanup()
	policy := testPolicy(t, app)
	policy.AnonymousMaxUploadMB = 1
	policy.AnonymousDailyUploadMB = 0.001
	if err := app.settingsService.UpdateUploadPolicy(policy); err != nil {
		t.Fatalf("UpdateUploadPolicy returned error: %v", err)
	}

	large := multipartUploadRequest(t, "/api/v1/upload", "file", "large.txt", strings.Repeat("x", 2*1024*1024))
	large.Header.Set("Accept", "application/json")
	large.RemoteAddr = "192.0.2.10:1234"
	largeResponse := httptest.NewRecorder()
	app.Upload(largeResponse, large)
	if largeResponse.Code != http.StatusRequestEntityTooLarge {
		t.Fatalf("large status = %d, body = %s", largeResponse.Code, largeResponse.Body.String())
	}

	daily := multipartUploadRequest(t, "/api/v1/upload", "file", "note.txt", strings.Repeat("x", 2048))
	daily.Header.Set("Accept", "application/json")
	daily.RemoteAddr = "192.0.2.10:1234"
	dailyResponse := httptest.NewRecorder()
	app.Upload(dailyResponse, daily)
	if dailyResponse.Code != http.StatusTooManyRequests {
		t.Fatalf("daily status = %d, body = %s", dailyResponse.Code, dailyResponse.Body.String())
	}
}

func TestSignedInUploadQuotaAndOverride(t *testing.T) {
	app, cleanup := newTestApp(t)
	defer cleanup()
	user, err := app.authService.CreateBootstrapUser("admin", "admin@example.test", "password123")
	if err != nil {
		t.Fatalf("CreateBootstrapUser returned error: %v", err)
	}
	invite, err := app.authService.CreateInvite("user@example.test", services.UserRoleUser, user.ID, 0)
	if err != nil {
		t.Fatalf("CreateInvite returned error: %v", err)
	}
	normal, err := app.authService.AcceptInvite(invite.Token, "user", "password123")
	if err != nil {
		t.Fatalf("AcceptInvite returned error: %v", err)
	}
	_, token, err := app.authService.Login(normal.Email, "password123")
	if err != nil {
		t.Fatalf("Login returned error: %v", err)
	}
	policy := testPolicy(t, app)
	policy.DefaultUserStorageMB = 0.001
	policy.UserDailyUploadMB = 8
	if err := app.settingsService.UpdateUploadPolicy(policy); err != nil {
		t.Fatalf("UpdateUploadPolicy returned error: %v", err)
	}

	request := multipartUploadRequest(t, "/api/v1/upload", "file", "quota.txt", strings.Repeat("x", 2048))
	request.Header.Set("Accept", "application/json")
	request.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: token})
	response := httptest.NewRecorder()
	app.Upload(response, request)
	if response.Code != http.StatusRequestEntityTooLarge {
		t.Fatalf("quota status = %d, body = %s", response.Code, response.Body.String())
	}

	override := 10.0
	if err := app.authService.SetUserStorageQuota(normal.ID, &override); err != nil {
		t.Fatalf("SetUserStorageQuota returned error: %v", err)
	}
	request = multipartUploadRequest(t, "/api/v1/upload", "file", "quota.txt", strings.Repeat("x", 2048))
	request.Header.Set("Accept", "application/json")
	request.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: token})
	response = httptest.NewRecorder()
	app.Upload(response, request)
	if response.Code != http.StatusCreated {
		t.Fatalf("override status = %d, body = %s", response.Code, response.Body.String())
	}
}

func TestSignedInDailyCap(t *testing.T) {
	app, cleanup := newTestApp(t)
	defer cleanup()
	admin, err := app.authService.CreateBootstrapUser("admin", "admin@example.test", "password123")
	if err != nil {
		t.Fatalf("CreateBootstrapUser returned error: %v", err)
	}
	invite, err := app.authService.CreateInvite("user@example.test", services.UserRoleUser, admin.ID, 0)
	if err != nil {
		t.Fatalf("CreateInvite returned error: %v", err)
	}
	user, err := app.authService.AcceptInvite(invite.Token, "user", "password123")
	if err != nil {
		t.Fatalf("AcceptInvite returned error: %v", err)
	}
	_, token, err := app.authService.Login(user.Email, "password123")
	if err != nil {
		t.Fatalf("Login returned error: %v", err)
	}
	policy := testPolicy(t, app)
	policy.UserDailyUploadMB = 0.001
	if err := app.settingsService.UpdateUploadPolicy(policy); err != nil {
		t.Fatalf("UpdateUploadPolicy returned error: %v", err)
	}
	request := multipartUploadRequest(t, "/api/v1/upload", "file", "daily.txt", strings.Repeat("x", 2048))
	request.Header.Set("Accept", "application/json")
	request.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: token})
	response := httptest.NewRecorder()
	app.Upload(response, request)
	if response.Code != http.StatusTooManyRequests {
		t.Fatalf("daily status = %d, body = %s", response.Code, response.Body.String())
	}
}

func TestLayeredUploadLimits(t *testing.T) {
	app, cleanup := newTestApp(t)
	defer cleanup()
	policy := testPolicy(t, app)
	policy.AnonymousDailyBoxes = 1
	policy.AnonymousActiveBoxes = 10
	policy.AnonymousMaxDays = 3
	policy.LocalStorageMaxGB = 0.001
	if err := app.settingsService.UpdateUploadPolicy(policy); err != nil {
		t.Fatalf("UpdateUploadPolicy returned error: %v", err)
	}

	first := uploadThroughApp(t, app)
	if first.BoxID == "" {
		t.Fatalf("first upload did not return a box id")
	}
	secondRequest := multipartUploadRequest(t, "/api/v1/upload", "file", "second.txt", "hello")
	secondRequest.Header.Set("Accept", "application/json")
	secondResponse := httptest.NewRecorder()
	app.Upload(secondResponse, secondRequest)
	if secondResponse.Code != http.StatusTooManyRequests {
		t.Fatalf("daily box status = %d, body = %s", secondResponse.Code, secondResponse.Body.String())
	}

	policy.AnonymousDailyBoxes = 10
	if err := app.settingsService.UpdateUploadPolicy(policy); err != nil {
		t.Fatalf("UpdateUploadPolicy returned error: %v", err)
	}
	expiryRequest := multipartUploadRequestWithField(t, "/api/v1/upload", "file", "expiry.txt", "hello", "max_days", "30")
	expiryRequest.Header.Set("Accept", "application/json")
	expiryResponse := httptest.NewRecorder()
	app.Upload(expiryResponse, expiryRequest)
	if expiryResponse.Code != http.StatusRequestEntityTooLarge && expiryResponse.Code != http.StatusTooManyRequests {
		t.Fatalf("expiry/box status = %d, body = %s", expiryResponse.Code, expiryResponse.Body.String())
	}
}

func TestUserPolicyOverrideChangesUploadEnforcement(t *testing.T) {
	app, cleanup := newTestApp(t)
	defer cleanup()
	admin, err := app.authService.CreateBootstrapUser("admin", "admin@example.test", "password123")
	if err != nil {
		t.Fatalf("CreateBootstrapUser returned error: %v", err)
	}
	invite, err := app.authService.CreateInvite("user@example.test", services.UserRoleUser, admin.ID, 0)
	if err != nil {
		t.Fatalf("CreateInvite returned error: %v", err)
	}
	user, err := app.authService.AcceptInvite(invite.Token, "user", "password123")
	if err != nil {
		t.Fatalf("AcceptInvite returned error: %v", err)
	}
	dailyBoxes := 1
	maxDays := 1
	if err := app.authService.SetUserPolicy(user.ID, services.UserPolicy{DailyBoxes: &dailyBoxes, MaxDays: &maxDays}); err != nil {
		t.Fatalf("SetUserPolicy returned error: %v", err)
	}
	_, token, err := app.authService.Login(user.Email, "password123")
	if err != nil {
		t.Fatalf("Login returned error: %v", err)
	}
	first := multipartUploadRequest(t, "/api/v1/upload", "file", "one.txt", "hello")
	first.Header.Set("Accept", "application/json")
	first.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: token})
	firstResponse := httptest.NewRecorder()
	app.Upload(firstResponse, first)
	if firstResponse.Code != http.StatusCreated {
		t.Fatalf("first status = %d, body = %s", firstResponse.Code, firstResponse.Body.String())
	}
	second := multipartUploadRequest(t, "/api/v1/upload", "file", "two.txt", "hello")
	second.Header.Set("Accept", "application/json")
	second.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: token})
	secondResponse := httptest.NewRecorder()
	app.Upload(secondResponse, second)
	if secondResponse.Code != http.StatusTooManyRequests {
		t.Fatalf("second status = %d, body = %s", secondResponse.Code, secondResponse.Body.String())
	}
}

func TestLocalStorageCapRejectsUpload(t *testing.T) {
	app, cleanup := newTestApp(t)
	defer cleanup()
	policy := testPolicy(t, app)
	policy.AnonymousMaxUploadMB = 4
	policy.AnonymousDailyUploadMB = 8
	policy.LocalStorageMaxGB = 0.001
	if err := app.settingsService.UpdateUploadPolicy(policy); err != nil {
		t.Fatalf("UpdateUploadPolicy returned error: %v", err)
	}
	request := multipartUploadRequest(t, "/api/v1/upload", "file", "large.txt", strings.Repeat("x", 2*1024*1024))
	request.Header.Set("Accept", "application/json")
	response := httptest.NewRecorder()
	app.Upload(response, request)
	if response.Code != http.StatusRequestEntityTooLarge {
		t.Fatalf("storage cap status = %d, body = %s", response.Code, response.Body.String())
	}
}

func TestAdminSettingsPostChangesUploadEnforcement(t *testing.T) {
	app, cleanup := newTestApp(t)
	defer cleanup()
	_, err := app.authService.CreateBootstrapUser("admin", "admin@example.test", "password123")
	if err != nil {
		t.Fatalf("CreateBootstrapUser returned error: %v", err)
	}
	_, token, err := app.authService.Login("admin@example.test", "password123")
	if err != nil {
		t.Fatalf("Login returned error: %v", err)
	}

	settingsForm := strings.NewReader("anonymous_max_upload_mb=512&anonymous_daily_upload_mb=2048&user_daily_upload_mb=8192&default_user_storage_mb=51200&usage_retention_days=30&csrf_token=test-csrf")
	settingsRequest := httptest.NewRequest(http.MethodPost, "/admin/settings", settingsForm)
	settingsRequest.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	settingsRequest.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: token})
	settingsRequest.AddCookie(&http.Cookie{Name: csrfCookieName, Value: "test-csrf"})
	settingsResponse := httptest.NewRecorder()
	app.AdminSettingsPost(settingsResponse, settingsRequest)
	if settingsResponse.Code != http.StatusSeeOther {
		t.Fatalf("AdminSettingsPost status = %d, body = %s", settingsResponse.Code, settingsResponse.Body.String())
	}

	uploadRequest := multipartUploadRequest(t, "/api/v1/upload", "file", "note.txt", "hello")
	uploadRequest.Header.Set("Accept", "application/json")
	uploadResponse := httptest.NewRecorder()
	app.Upload(uploadResponse, uploadRequest)
	if uploadResponse.Code != http.StatusForbidden {
		t.Fatalf("upload status = %d, want 403, body = %s", uploadResponse.Code, uploadResponse.Body.String())
	}
}

func TestAdminUserQuotaPostChangesEnforcement(t *testing.T) {
	app, cleanup := newTestApp(t)
	defer cleanup()
	admin, err := app.authService.CreateBootstrapUser("admin", "admin@example.test", "password123")
	if err != nil {
		t.Fatalf("CreateBootstrapUser returned error: %v", err)
	}
	invite, err := app.authService.CreateInvite("user@example.test", services.UserRoleUser, admin.ID, 0)
	if err != nil {
		t.Fatalf("CreateInvite returned error: %v", err)
	}
	user, err := app.authService.AcceptInvite(invite.Token, "user", "password123")
	if err != nil {
		t.Fatalf("AcceptInvite returned error: %v", err)
	}
	_, adminToken, err := app.authService.Login(admin.Email, "password123")
	if err != nil {
		t.Fatalf("admin Login returned error: %v", err)
	}

	quotaRequest := httptest.NewRequest(http.MethodPost, "/admin/users/"+user.ID+"/quota", strings.NewReader("storage_quota_mb=0.001&csrf_token=test-csrf"))
	quotaRequest.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	quotaRequest.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: adminToken})
	quotaRequest.AddCookie(&http.Cookie{Name: csrfCookieName, Value: "test-csrf"})
	quotaRequest.SetPathValue("userID", user.ID)
	quotaResponse := httptest.NewRecorder()
	app.AdminUpdateUserQuota(quotaResponse, quotaRequest)
	if quotaResponse.Code != http.StatusSeeOther {
		t.Fatalf("AdminUpdateUserQuota status = %d, body = %s", quotaResponse.Code, quotaResponse.Body.String())
	}

	_, userToken, err := app.authService.Login(user.Email, "password123")
	if err != nil {
		t.Fatalf("user Login returned error: %v", err)
	}
	uploadRequest := multipartUploadRequest(t, "/api/v1/upload", "file", "quota.txt", strings.Repeat("x", 2048))
	uploadRequest.Header.Set("Accept", "application/json")
	uploadRequest.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: userToken})
	uploadResponse := httptest.NewRecorder()
	app.Upload(uploadResponse, uploadRequest)
	if uploadResponse.Code != http.StatusRequestEntityTooLarge {
		t.Fatalf("upload status = %d, want 413, body = %s", uploadResponse.Code, uploadResponse.Body.String())
	}
}

func TestHomeReflectsUploadPolicySettings(t *testing.T) {
	app, cleanup := newTestApp(t)
	defer cleanup()
	policy := testPolicy(t, app)
	policy.AnonymousMaxUploadMB = 123
	policy.AnonymousDailyUploadMB = 456
	if err := app.settingsService.UpdateUploadPolicy(policy); err != nil {
		t.Fatalf("UpdateUploadPolicy returned error: %v", err)
	}

	request := httptest.NewRequest(http.MethodGet, "/", nil)
	response := httptest.NewRecorder()
	app.Home(response, request)
	if response.Code != http.StatusOK {
		t.Fatalf("Home status = %d", response.Code)
	}
	body := response.Body.String()
	if !strings.Contains(body, "Max file size: 123 MB") || !strings.Contains(body, "456 MB") {
		t.Fatalf("home did not reflect policy settings: %s", body)
	}
	if !strings.Contains(body, "warpbox.dev · test ·") {
		t.Fatalf("home footer did not include app version: %s", body)
	}
}

func TestAPIDocsHeaderReflectsLoggedInUser(t *testing.T) {
	app, cleanup := newTestApp(t)
	defer cleanup()
	_, err := app.authService.CreateBootstrapUser("admin", "admin@example.test", "password123")
	if err != nil {
		t.Fatalf("CreateBootstrapUser returned error: %v", err)
	}
	_, token, err := app.authService.Login("admin@example.test", "password123")
	if err != nil {
		t.Fatalf("Login returned error: %v", err)
	}

	request := httptest.NewRequest(http.MethodGet, "/api", nil)
	request.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: token})
	response := httptest.NewRecorder()
	app.APIDocs(response, request)
	if response.Code != http.StatusOK {
		t.Fatalf("APIDocs status = %d", response.Code)
	}
	body := response.Body.String()
	header := body[:strings.Index(body, "<main")]
	if !strings.Contains(header, "Dashboard") || strings.Contains(header, "Sign in") || strings.Contains(header, "Health") {
		t.Fatalf("api header did not reflect logged-in state: %s", body)
	}
}

func TestAPIDocsHeaderReflectsLoggedOutUser(t *testing.T) {
	app, cleanup := newTestApp(t)
	defer cleanup()

	request := httptest.NewRequest(http.MethodGet, "/api", nil)
	response := httptest.NewRecorder()
	app.APIDocs(response, request)
	if response.Code != http.StatusOK {
		t.Fatalf("APIDocs status = %d", response.Code)
	}
	body := response.Body.String()
	header := body[:strings.Index(body, "<main")]
	if !strings.Contains(header, "Sign in") || !strings.Contains(header, ">API<") || strings.Contains(header, "Health") || strings.Contains(header, "Dashboard") {
		t.Fatalf("api header did not reflect logged-out state: %s", body)
	}
}

func TestAdminStorageProviderPagesOnlyRenderRelevantFields(t *testing.T) {
	app, cleanup := newTestApp(t)
	defer cleanup()
	adminToken := createAdminSession(t, app)

	request := httptest.NewRequest(http.MethodGet, "/admin/storage/new/sftp", nil)
	request.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: adminToken})
	response := httptest.NewRecorder()
	app.AdminNewStorageProvider(response, request)
	if response.Code != http.StatusOK {
		t.Fatalf("AdminNewStorageProvider status = %d, body = %s", response.Code, response.Body.String())
	}
	body := response.Body.String()
	if !strings.Contains(body, "Private key") || !strings.Contains(body, "SSH host key") {
		t.Fatalf("sftp page did not render sftp fields: %s", body)
	}
	for _, unwanted := range []string{"Bucket display name", "WebDAV URL", "Share</span>", "Access key"} {
		if strings.Contains(body, unwanted) {
			t.Fatalf("sftp page rendered irrelevant field %q: %s", unwanted, body)
		}
	}

	cfg, err := app.uploadService.Storage().CreateBackend(services.StorageBackendConfig{
		Provider: services.StorageProviderSFTP,
		Name:     "NAS",
		Host:     "files.example.test",
		Username: "warpbox",
		Password: "secret",
	})
	if err != nil {
		t.Fatalf("CreateBackend returned error: %v", err)
	}
	editRequest := httptest.NewRequest(http.MethodGet, "/admin/storage/"+cfg.ID+"/edit", nil)
	editRequest.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: adminToken})
	editRequest.SetPathValue("backendID", cfg.ID)
	editResponse := httptest.NewRecorder()
	app.AdminEditStorageForm(editResponse, editRequest)
	if editResponse.Code != http.StatusOK {
		t.Fatalf("AdminEditStorageForm status = %d, body = %s", editResponse.Code, editResponse.Body.String())
	}
	editBody := editResponse.Body.String()
	if !strings.Contains(editBody, "Immutable provider") || strings.Contains(editBody, "Bucket display name") || strings.Contains(editBody, "WebDAV URL") {
		t.Fatalf("edit page did not stay provider-specific: %s", editBody)
	}
}

func TestAdminStorageEditRejectsProviderMutation(t *testing.T) {
	app, cleanup := newTestApp(t)
	defer cleanup()
	adminToken := createAdminSession(t, app)
	cfg, err := app.uploadService.Storage().CreateBackend(services.StorageBackendConfig{
		Provider: services.StorageProviderSFTP,
		Name:     "NAS",
		Host:     "files.example.test",
		Username: "warpbox",
		Password: "secret",
	})
	if err != nil {
		t.Fatalf("CreateBackend returned error: %v", err)
	}

	form := strings.NewReader("provider=s3&name=Changed&endpoint=https://s3.example.test&bucket=bucket&access_key=access&secret_key=secret&use_ssl=on&csrf_token=test-csrf")
	request := httptest.NewRequest(http.MethodPost, "/admin/storage/"+cfg.ID+"/edit", form)
	request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	request.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: adminToken})
	request.AddCookie(&http.Cookie{Name: csrfCookieName, Value: "test-csrf"})
	request.SetPathValue("backendID", cfg.ID)
	response := httptest.NewRecorder()
	app.AdminEditStorage(response, request)
	if response.Code != http.StatusSeeOther {
		t.Fatalf("AdminEditStorage status = %d, body = %s", response.Code, response.Body.String())
	}
	stored, err := app.uploadService.Storage().BackendConfig(cfg.ID)
	if err != nil {
		t.Fatalf("BackendConfig returned error: %v", err)
	}
	if stored.Provider != services.StorageProviderSFTP || stored.Type != services.StorageBackendSFTP || stored.Name != "NAS" {
		t.Fatalf("storage backend mutated despite rejected provider change: %+v", stored)
	}
}

func TestAdminStorageJobRoutesRequireAdminAndCSRF(t *testing.T) {
	app, cleanup := newTestApp(t)
	defer cleanup()

	unauthorized := httptest.NewRecorder()
	app.AdminRunStorageCleanup(unauthorized, httptest.NewRequest(http.MethodPost, "/admin/storage/jobs/cleanup", nil))
	if unauthorized.Code != http.StatusSeeOther {
		t.Fatalf("unauthorized cleanup status = %d", unauthorized.Code)
	}

	adminToken := createAdminSession(t, app)
	missingCSRFRequest := httptest.NewRequest(http.MethodPost, "/admin/storage/jobs/cleanup", nil)
	missingCSRFRequest.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: adminToken})
	missingCSRFResponse := httptest.NewRecorder()
	app.AdminRunStorageCleanup(missingCSRFResponse, missingCSRFRequest)
	if missingCSRFResponse.Code != http.StatusForbidden {
		t.Fatalf("missing csrf cleanup status = %d", missingCSRFResponse.Code)
	}

	request := httptest.NewRequest(http.MethodPost, "/admin/storage/jobs/cleanup", strings.NewReader("csrf_token=test-csrf"))
	request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	request.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: adminToken})
	request.AddCookie(&http.Cookie{Name: csrfCookieName, Value: "test-csrf"})
	response := httptest.NewRecorder()
	app.AdminRunStorageCleanup(response, request)
	if response.Code != http.StatusSeeOther {
		t.Fatalf("authorized cleanup status = %d, body = %s", response.Code, response.Body.String())
	}
}

func TestAdminStorageSpeedTestStartsBackgroundJob(t *testing.T) {
	app, cleanup := newTestApp(t)
	defer cleanup()
	adminToken := createAdminSession(t, app)
	if _, err := app.uploadService.Storage().TestBackend(services.StorageBackendLocal); err != nil {
		t.Fatalf("TestBackend returned error: %v", err)
	}

	request := httptest.NewRequest(http.MethodPost, "/admin/storage/local/speed-test", strings.NewReader("mode=custom&custom_file_count=2&custom_file_size_mb=0.001&csrf_token=test-csrf"))
	request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	request.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: adminToken})
	request.AddCookie(&http.Cookie{Name: csrfCookieName, Value: "test-csrf"})
	request.SetPathValue("backendID", services.StorageBackendLocal)
	response := httptest.NewRecorder()
	app.AdminStartStorageSpeedTest(response, request)
	if response.Code != http.StatusSeeOther {
		t.Fatalf("AdminStartStorageSpeedTest status = %d, body = %s", response.Code, response.Body.String())
	}
	tests, err := app.uploadService.Storage().ListSpeedTests(services.StorageBackendLocal, 10)
	if err != nil {
		t.Fatalf("ListSpeedTests returned error: %v", err)
	}
	if len(tests) != 1 {
		t.Fatalf("speed tests len = %d, want 1", len(tests))
	}
	if tests[0].Mode != services.StorageSpeedModeCustom || tests[0].CustomFileCount != 2 || tests[0].CustomFileSizeMB != 0.001 {
		t.Fatalf("custom speed test options were not stored: %+v", tests[0])
	}
	location := response.Header().Get("Location")
	if !strings.Contains(location, "/admin/storage/local/tests") {
		t.Fatalf("speed test redirect location = %q", location)
	}
}

func TestAdminStorageTestingPageRendersHistory(t *testing.T) {
	app, cleanup := newTestApp(t)
	defer cleanup()
	adminToken := createAdminSession(t, app)
	if _, err := app.uploadService.Storage().TestBackend(services.StorageBackendLocal); err != nil {
		t.Fatalf("TestBackend returned error: %v", err)
	}
	test, err := app.uploadService.Storage().StartSpeedTest(services.StorageBackendLocal, services.StorageSpeedModeSmall)
	if err != nil {
		t.Fatalf("StartSpeedTest returned error: %v", err)
	}
	app.uploadService.Storage().RunSpeedTest(context.Background(), test.ID)

	request := httptest.NewRequest(http.MethodGet, "/admin/storage/local/tests", nil)
	request.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: adminToken})
	request.SetPathValue("backendID", services.StorageBackendLocal)
	response := httptest.NewRecorder()
	app.AdminStorageTests(response, request)
	if response.Code != http.StatusOK {
		t.Fatalf("AdminStorageTests status = %d, body = %s", response.Code, response.Body.String())
	}
	body := response.Body.String()
	if !strings.Contains(body, "New Test") || !strings.Contains(body, "Many small files") || strings.Contains(body, "storage-test-menu") {
		t.Fatalf("testing page missing expected page-based controls: %s", body)
	}

	jsonRequest := httptest.NewRequest(http.MethodGet, "/admin/storage/local/tests.json", nil)
	jsonRequest.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: adminToken})
	jsonRequest.SetPathValue("backendID", services.StorageBackendLocal)
	jsonResponse := httptest.NewRecorder()
	app.AdminStorageTestsJSON(jsonResponse, jsonRequest)
	if jsonResponse.Code != http.StatusOK {
		t.Fatalf("AdminStorageTestsJSON status = %d, body = %s", jsonResponse.Code, jsonResponse.Body.String())
	}
	if !strings.Contains(jsonResponse.Body.String(), `"progress":100`) || !strings.Contains(jsonResponse.Body.String(), `"stage":"complete"`) {
		t.Fatalf("tests json missing progress fields: %s", jsonResponse.Body.String())
	}
}

func createOwnedBoxThroughApp(t *testing.T, app *App, userID string) services.UploadResult {
	t.Helper()
	user, err := app.authService.UserByID(userID)
	if err != nil {
		t.Fatalf("UserByID returned error: %v", err)
	}
	_, token, err := app.authService.Login(user.Email, "password123")
	if err != nil {
		t.Fatalf("Login returned error: %v", err)
	}
	request := multipartUploadRequest(t, "/api/v1/upload", "file", "owned.txt", "owned")
	request.Header.Set("Accept", "application/json")
	request.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: token})
	response := httptest.NewRecorder()
	app.Upload(response, request)
	if response.Code != http.StatusCreated {
		t.Fatalf("upload status = %d, body = %s", response.Code, response.Body.String())
	}
	var payload services.UploadResult
	if err := json.Unmarshal(response.Body.Bytes(), &payload); err != nil {
		t.Fatalf("json.Unmarshal returned error: %v", err)
	}
	return payload
}

func createAdminSession(t *testing.T, app *App) string {
	t.Helper()
	_, err := app.authService.CreateBootstrapUser("admin", "admin@example.test", "password123")
	if err != nil && !strings.Contains(err.Error(), "registration is closed") {
		t.Fatalf("CreateBootstrapUser returned error: %v", err)
	}
	_, token, err := app.authService.Login("admin@example.test", "password123")
	if err != nil {
		t.Fatalf("Login returned error: %v", err)
	}
	return token
}

func testPolicy(t *testing.T, app *App) services.UploadPolicySettings {
	t.Helper()
	policy, err := app.settingsService.UploadPolicy()
	if err != nil {
		t.Fatalf("UploadPolicy returned error: %v", err)
	}
	return policy
}
