package handlers

import (
	"archive/zip"
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"io"
	"net/http"
	"reflect"
	"strings"
	"sync"
	"testing"
	"time"
	"unsafe"

	"github.com/gofiber/fiber/v2"
	"github.com/valyala/fasthttp"

	filemanager "wucher/internal/domain/file_manager"
	"wucher/internal/domain/helicopter"
	helicopterfile "wucher/internal/domain/helicopter_file"
	takeoverdomain "wucher/internal/domain/takeover"
	helicopterfilerealtime "wucher/internal/realtime/helicopterfile"
	"wucher/internal/service"
	"wucher/internal/shared/pkg/uuidv7"
	responsedto "wucher/internal/transport/http/dto/response"
)

type helicopterFileServiceMock struct {
	createErr           error
	createFn            func(context.Context, *helicopterfile.HelicopterFile) error
	updateErr           error
	deleteErr           error
	getErr              error
	listErr             error
	helicopterExists    bool
	helicopterExistsErr error
	attachmentExists    bool

	getRow     *helicopterfile.HelicopterFile
	listRows   []helicopterfile.HelicopterFile
	listTotal  int64
	byHeliRows []helicopterfile.HelicopterFile
	byHeliErr  error
	uploadBulk []helicopterfile.UploadIntentBulkItemResult
	createBulk []helicopterfile.CreateBulkItemResult
}

func (m *helicopterFileServiceMock) Create(ctx context.Context, row *helicopterfile.HelicopterFile) error {
	if m.createFn != nil {
		return m.createFn(ctx, row)
	}
	return m.createErr
}

func (m *helicopterFileServiceMock) Update(context.Context, *helicopterfile.HelicopterFile) error {
	return m.updateErr
}

func (m *helicopterFileServiceMock) Delete(context.Context, []byte) error {
	return m.deleteErr
}

func (m *helicopterFileServiceMock) GetByID(context.Context, []byte) (*helicopterfile.HelicopterFile, error) {
	return m.getRow, m.getErr
}

func (m *helicopterFileServiceMock) List(context.Context, string, string, int, int) ([]helicopterfile.HelicopterFile, int64, error) {
	return m.listRows, m.listTotal, m.listErr
}

func (m *helicopterFileServiceMock) ListByHelicopter(context.Context, []byte) ([]helicopterfile.HelicopterFile, error) {
	return m.byHeliRows, m.byHeliErr
}

func (m *helicopterFileServiceMock) ListByHelicopterAndSection(context.Context, []byte, string) ([]helicopterfile.HelicopterFile, error) {
	return nil, nil
}

func (m *helicopterFileServiceMock) HelicopterExists(context.Context, []byte) (bool, error) {
	return m.helicopterExists, m.helicopterExistsErr
}

func (m *helicopterFileServiceMock) AttachmentExists(context.Context, []byte) (bool, error) {
	return m.attachmentExists, nil
}

func (m *helicopterFileServiceMock) RequestUploadIntentsBulk(context.Context, []helicopterfile.UploadIntentBulkInput, []byte) []helicopterfile.UploadIntentBulkItemResult {
	return m.uploadBulk
}

func (m *helicopterFileServiceMock) CreateFromUploadIntentsBulk(context.Context, []helicopterfile.CreateBulkInput, []byte) []helicopterfile.CreateBulkItemResult {
	return m.createBulk
}

type helicopterServiceMock struct {
	row *helicopter.Helicopter
	err error
}

type helicopterFileTakeoverServiceMock struct {
	row *takeoverdomain.TakeoverAc
	err error
}

type helicopterFileEventsMock struct {
	publishedUsers []string
	event          *helicopterfilerealtime.Event
	publishCalls   int
}

func (m *helicopterFileEventsMock) PublishToUsers(userUUIDs []string, event helicopterfilerealtime.Event) int {
	m.publishCalls++
	m.publishedUsers = append([]string(nil), userUUIDs...)
	copied := event
	m.event = &copied
	return len(userUUIDs)
}

type helicopterFileManagerMock struct {
	filemanager.Service
	createFileFn        func(context.Context, filemanager.CreateFileFromUploadIntentParams) (*filemanager.File, error)
	createAttachmentFn  func(context.Context, filemanager.CreateAttachmentParams) (*filemanager.Attachment, error)
	listAttachmentRefFn func(context.Context, filemanager.ListAttachmentsParams) ([]filemanager.Attachment, int64, error)
	getAttachmentByIDFn func(context.Context, []byte) (*filemanager.Attachment, error)
	getRootFn           func(context.Context) (*filemanager.Folder, error)
	getByFolderNameFn   func(context.Context, []byte, string) (*filemanager.File, error)
	getFileByIDFn       func(context.Context, []byte) (*filemanager.File, error)
	moveFileFn          func(context.Context, []byte, filemanager.MoveFileParams) (*filemanager.File, error)
	updateFileFn        func(context.Context, *filemanager.File) error
	markActiveErr       error
	reconcileErr        error
	lastCreateFile      *filemanager.CreateFileFromUploadIntentParams
	updateFileCalls     int
}

func (m *helicopterFileManagerMock) CreateFileNodeFromUploadIntent(ctx context.Context, p filemanager.CreateFileFromUploadIntentParams) (*filemanager.File, error) {
	copied := p
	copied.UploadIntentID = append([]byte(nil), p.UploadIntentID...)
	copied.FolderID = append([]byte(nil), p.FolderID...)
	copied.ActorID = append([]byte(nil), p.ActorID...)
	m.lastCreateFile = &copied
	if m.createFileFn != nil {
		return m.createFileFn(ctx, p)
	}
	return nil, nil
}

func (m *helicopterFileManagerMock) CreateAttachment(ctx context.Context, p filemanager.CreateAttachmentParams) (*filemanager.Attachment, error) {
	if m.createAttachmentFn != nil {
		return m.createAttachmentFn(ctx, p)
	}
	return nil, nil
}

func (m *helicopterFileManagerMock) ListAttachmentsByReference(ctx context.Context, p filemanager.ListAttachmentsParams) ([]filemanager.Attachment, int64, error) {
	if m.listAttachmentRefFn != nil {
		return m.listAttachmentRefFn(ctx, p)
	}
	return nil, 0, nil
}

func (m *helicopterFileManagerMock) GetAttachmentByID(ctx context.Context, id []byte) (*filemanager.Attachment, error) {
	if m.getAttachmentByIDFn != nil {
		return m.getAttachmentByIDFn(ctx, id)
	}
	return nil, nil
}

func (m *helicopterFileManagerMock) GetOrCreateSystemRootFolder(ctx context.Context) (*filemanager.Folder, error) {
	if m.getRootFn != nil {
		return m.getRootFn(ctx)
	}
	return nil, nil
}

func (m *helicopterFileManagerMock) GetFileByFolderAndName(ctx context.Context, folderID []byte, name string) (*filemanager.File, error) {
	if m.getByFolderNameFn != nil {
		return m.getByFolderNameFn(ctx, folderID, name)
	}
	return nil, nil
}

func (m *helicopterFileManagerMock) GetFileByID(ctx context.Context, id []byte) (*filemanager.File, error) {
	if m.getFileByIDFn != nil {
		return m.getFileByIDFn(ctx, id)
	}
	return nil, nil
}

func (m *helicopterFileManagerMock) MoveFileNode(ctx context.Context, id []byte, p filemanager.MoveFileParams) (*filemanager.File, error) {
	if m.moveFileFn != nil {
		return m.moveFileFn(ctx, id, p)
	}
	return nil, nil
}

func (m *helicopterFileManagerMock) UpdateFile(ctx context.Context, row *filemanager.File) error {
	m.updateFileCalls++
	if m.updateFileFn != nil {
		return m.updateFileFn(ctx, row)
	}
	return nil
}

func (m *helicopterFileManagerMock) MarkFileActive(context.Context, []byte, []byte) error {
	return m.markActiveErr
}

func (m *helicopterFileManagerMock) ReconcileFileLifecycle(context.Context, []byte, []byte) error {
	return m.reconcileErr
}

func (m *helicopterFileManagerMock) CleanupOrphanPendingFiles(context.Context, time.Time, time.Duration, int, bool) (filemanager.FileCleanupStats, error) {
	return filemanager.FileCleanupStats{}, nil
}

func (m *helicopterServiceMock) Create(context.Context, *helicopter.Helicopter) error { return nil }
func (m *helicopterServiceMock) Update(context.Context, *helicopter.Helicopter) error { return nil }
func (m *helicopterServiceMock) ApplySortKeyPatch(*helicopter.Helicopter, helicopter.SortKeyPatch) {
}
func (m *helicopterServiceMock) Delete(context.Context, []byte) error { return nil }
func (m *helicopterServiceMock) GetByID(context.Context, []byte) (*helicopter.Helicopter, error) {
	return m.row, m.err
}
func (m *helicopterServiceMock) List(context.Context, string, []string, string, int, int, [][]byte) ([]helicopter.Helicopter, int64, error) {
	return nil, 0, nil
}
func (m *helicopterServiceMock) NextReportNumber(context.Context, []byte) (string, int, error) {
	return "", 0, nil
}
func (m *helicopterServiceMock) IsInUse(context.Context, []byte) (bool, error) {
	return false, nil
}
func (m *helicopterServiceMock) InUseByAircraftIDs(context.Context, [][]byte) (map[string]bool, error) {
	return map[string]bool{}, nil
}
func (m *helicopterServiceMock) ActiveFlightIDByAircraftIDs(context.Context, [][]byte) (map[string][]byte, error) {
	return map[string][]byte{}, nil
}
func (m *helicopterServiceMock) ExistsByIdentifier(context.Context, string, []byte) (bool, error) {
	return false, nil
}

func (m *helicopterFileTakeoverServiceMock) GetByID(context.Context, []byte) (*takeoverdomain.TakeoverAc, error) {
	return m.row, m.err
}

func newHelicopterFileTestApp(h *HelicopterFileHandler) *fiber.App {
	app := fiber.New()
	app.Post("/upload", h.Upload)
	app.Post("/cancel-upload", h.CancelUpload)
	app.Post("/create", h.Create)
	app.Patch("/update", h.Update)
	app.Get("/get/:helicopter_id", h.Get)
	app.Get("/list", h.List)
	app.Delete("/delete", h.Delete)
	return app
}

func newHelicopterFileTestAppWithUser(h *HelicopterFileHandler, actor []byte) *fiber.App {
	app := fiber.New()
	app.Use(func(c *fiber.Ctx) error {
		if len(actor) > 0 {
			c.Locals("user_id", append([]byte(nil), actor...))
		}
		return c.Next()
	})
	app.Post("/upload", h.Upload)
	app.Post("/cancel-upload", h.CancelUpload)
	app.Post("/create", h.Create)
	app.Patch("/update", h.Update)
	app.Get("/get/:helicopter_id", h.Get)
	app.Get("/list", h.List)
	app.Delete("/delete", h.Delete)
	return app
}

func newHelicopterFileEventsTestApp(h *FileManagerEventsHandler, actor []byte) *fiber.App {
	app := fiber.New()
	app.Use(func(c *fiber.Ctx) error {
		if actor != nil {
			c.Locals("user_id", append([]byte(nil), actor...))
		}
		return c.Next()
	})
	app.Get("/api/v1/helicopter-files/events", h.Stream)
	return app
}

func doHelicopterFileRequest(t *testing.T, app *fiber.App, method, path string, body []byte) (*http.Response, []byte) {
	t.Helper()
	req, err := http.NewRequest(method, path, bytes.NewReader(body))
	if err != nil {
		t.Fatalf("new request: %v", err)
	}
	req.Header.Set("Content-Type", "application/json")
	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("app.Test: %v", err)
	}
	respBody, err := io.ReadAll(resp.Body)
	if err != nil {
		t.Fatalf("read body: %v", err)
	}
	return resp, respBody
}

func TestHelicopterFileEventsStream(t *testing.T) {
	hub := helicopterfilerealtime.NewHub(1, nil)
	actor := uuidv7.MustBytes()
	userUUID, _ := uuidv7.BytesToString(actor)
	helicopterID := uuidv7.MustBytes()
	helicopterFileID := uuidv7.MustBytes()
	fileID := uuidv7.MustBytes()

	app := fiber.New()
	fctx := &fasthttp.RequestCtx{}
	fctx.Request.SetRequestURI("/api/v1/helicopter-files/events?helicopter_id=" + mustUUIDString(t, helicopterID))
	c := app.AcquireCtx(fctx)
	c.Locals("user_id", actor)
	if err := NewHelicopterFileEventsHandler(hub).Stream(c); err != nil {
		t.Fatalf("unexpected stream setup error: %v", err)
	}

	hub.PublishToUsers([]string{userUUID}, helicopterfilerealtime.Event{
		HelicopterID:      mustUUIDString(t, helicopterID),
		HelicopterFileID:  mustUUIDString(t, helicopterFileID),
		FileID:            mustUUIDString(t, fileID),
		Section:           helicopterfile.SectionFlightPreparationAndWB,
		Name:              "helicopter.pdf",
		MimeType:          "application/pdf",
		AttachmentPending: true,
		Position:          3,
		UpdatedAt:         time.Now().UTC().Format(time.RFC3339),
	})
	go func() {
		time.Sleep(10 * time.Millisecond)
		closeFirstHelicopterFileHubSubscriberForUser(hub, userUUID)
	}()

	bodyStr := c.Context().Response.String()
	app.ReleaseCtx(c)
	if !strings.Contains(bodyStr, ": connected") {
		t.Fatalf("expected connected comment in body: %q", bodyStr)
	}
	if !strings.Contains(bodyStr, "event: helicopter.file.created") {
		t.Fatalf("expected helicopter.file.created event in body: %q", bodyStr)
	}
	if !strings.Contains(bodyStr, "\"file_id\"") || !strings.Contains(bodyStr, "\"helicopter_id\"") || !strings.Contains(bodyStr, "\"section\":\"fpwb\"") {
		t.Fatalf("expected helicopter file payload in body: %q", bodyStr)
	}
}

func closeFirstHelicopterFileHubSubscriberForUser(hub *helicopterfilerealtime.Hub, userUUID string) {
	if hub == nil {
		return
	}
	hubValue := reflect.ValueOf(hub).Elem()
	subscribersField := reflect.NewAt(hubValue.FieldByName("subscribers").Type(), unsafe.Pointer(hubValue.FieldByName("subscribers").UnsafeAddr())).Elem()
	byUser := subscribersField.MapIndex(reflect.ValueOf(strings.ToLower(strings.TrimSpace(userUUID))))
	if !byUser.IsValid() || byUser.Len() == 0 {
		return
	}
	iter := byUser.MapRange()
	if !iter.Next() {
		return
	}
	sub := iter.Value()
	if sub.IsNil() {
		return
	}
	subElem := sub.Elem()
	closeOnceField := reflect.NewAt(subElem.FieldByName("closeOnce").Type(), unsafe.Pointer(subElem.FieldByName("closeOnce").UnsafeAddr())).Elem()
	closeOncePtr := (*sync.Once)(unsafe.Pointer(closeOnceField.UnsafeAddr()))
	closeOncePtr.Do(func() {})
	doneField := reflect.NewAt(subElem.FieldByName("done").Type(), unsafe.Pointer(subElem.FieldByName("done").UnsafeAddr())).Elem()
	doneField.Close()
}

func withHelicopterFileCtx(t *testing.T, fn func(*fiber.Ctx)) {
	t.Helper()
	app := fiber.New()
	app.Get("/", func(c *fiber.Ctx) error {
		fn(c)
		return c.SendStatus(fiber.StatusOK)
	})
	req, _ := http.NewRequest(http.MethodGet, "/", nil)
	if _, err := app.Test(req); err != nil {
		t.Fatalf("app.Test: %v", err)
	}
}

func TestHelicopterFileHandler_HelperCoverage(t *testing.T) {
	t.Run("constructor and fluent setters", func(t *testing.T) {
		h := NewHelicopterFileHandler(&helicopterFileServiceMock{})
		if h == nil || h.validate == nil {
			t.Fatalf("expected handler initialized")
		}
		if h.WithFileManagerService(nil) != h {
			t.Fatalf("expected WithFileManagerService to return same handler")
		}
		if h.WithHelicopterService(nil) != h {
			t.Fatalf("expected WithHelicopterService to return same handler")
		}
		if h.WithFileStorage(nil) != h {
			t.Fatalf("expected WithFileStorage to return same handler")
		}
	})

	t.Run("section validation helper", func(t *testing.T) {
		if !isValidHelicopterFileSection(helicopterfile.SectionBeforeFirstFlightInspection) {
			t.Fatalf("expected before section valid")
		}
		if !isValidHelicopterFileSection(helicopterfile.SectionFlightPreparationAndWB) {
			t.Fatalf("expected prepare section valid")
		}
		if !isValidHelicopterFileSection(helicopterfile.SectionAfterLastFlightInspection) {
			t.Fatalf("expected after section valid")
		}
		if isValidHelicopterFileSection("other") {
			t.Fatalf("expected invalid section rejected")
		}
	})

	t.Run("bulk item helpers", func(t *testing.T) {
		uploadErr := helicopterFileUploadBulkErrorItem(1, http.StatusBadRequest, "Upload failed", "detail")
		if uploadErr.Success || uploadErr.Error == nil || uploadErr.Error.Status != http.StatusBadRequest {
			t.Fatalf("unexpected upload bulk error item: %#v", uploadErr)
		}
		createErr := helicopterFileCreateBulkErrorItem(2, http.StatusUnprocessableEntity, "Create failed", "detail")
		if createErr.Success || createErr.Error == nil || createErr.Error.Status != http.StatusUnprocessableEntity {
			t.Fatalf("unexpected create bulk error item: %#v", createErr)
		}
	})
}

func TestHelicopterFileHandler_Endpoints(t *testing.T) {
	t.Run("upload returns 400 when file manager missing", func(t *testing.T) {
		app := newHelicopterFileTestApp(NewHelicopterFileHandler(&helicopterFileServiceMock{}))
		resp, _ := doHelicopterFileRequest(t, app, http.MethodPost, "/upload", []byte(`{"data":{"type":"helicopter_file_upload","attributes":{"name":"a.pdf","content_type":"application/pdf","size_bytes":1}}}`))
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})

	t.Run("upload bulk validation and multi-status response", func(t *testing.T) {
		svc := &helicopterFileServiceMock{
			uploadBulk: []helicopterfile.UploadIntentBulkItemResult{
				{Index: 0, Success: true, Grant: &filemanager.FileUploadIntentGrant{
					UploadIntentID: uuidv7.MustBytes(),
					Name:           "a.pdf",
					MimeType:       "application/pdf",
					SizeBytes:      10,
					UploadURL:      "https://upload",
					Headers: map[string]string{
						"content-type": "application/pdf",
					},
				}},
				{Index: 1, Success: false, Error: &helicopterfile.BulkItemError{Status: http.StatusBadRequest, Title: "Upload failed", Detail: "invalid"}}},
		}
		h := NewHelicopterFileHandler(svc).WithFileManagerService(service.NewFileManagerService(nil, nil, nil, service.FileManagerServiceDependencies{}))
		app := newHelicopterFileTestApp(h)

		resp, _ := doHelicopterFileRequest(t, app, http.MethodPost, "/upload", []byte(`{"data":[]}`))
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422 for empty bulk, got %d", resp.StatusCode)
		}

		body := []byte(`{"data":[{"type":"helicopter_file_upload","attributes":{"name":"a.pdf","content_type":"application/pdf","size_bytes":10}},{"type":"helicopter_file_upload","attributes":{"name":"b.pdf","content_type":"application/pdf","size_bytes":20}}]}`)
		resp, _ = doHelicopterFileRequest(t, app, http.MethodPost, "/upload", body)
		if resp.StatusCode != http.StatusMultiStatus {
			t.Fatalf("expected 207, got %d", resp.StatusCode)
		}
	})

	t.Run("upload single early branches", func(t *testing.T) {
		fileSvc := service.NewFileManagerService(nil, nil, nil, service.FileManagerServiceDependencies{})
		app := newHelicopterFileTestApp(NewHelicopterFileHandler(&helicopterFileServiceMock{}).WithFileManagerService(fileSvc))

		resp, _ := doHelicopterFileRequest(t, app, http.MethodPost, "/upload", []byte(`{"data":`))
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400 invalid json, got %d", resp.StatusCode)
		}

		resp, _ = doHelicopterFileRequest(t, app, http.MethodPost, "/upload", []byte(`{"data":{"type":"wrong","attributes":{"name":"a.pdf","content_type":"application/pdf","size_bytes":1}}}`))
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422 invalid type, got %d", resp.StatusCode)
		}

		resp, _ = doHelicopterFileRequest(t, app, http.MethodPost, "/upload", []byte(`{"data":{"type":"helicopter_file_upload","attributes":{"name":"a.pdf","content_type":"application/pdf","size_bytes":1}}}`))
		if resp.StatusCode != http.StatusServiceUnavailable {
			t.Fatalf("expected 503 upload intent action error, got %d", resp.StatusCode)
		}
	})

	t.Run("cancel upload validation", func(t *testing.T) {
		app := newHelicopterFileTestApp(NewHelicopterFileHandler(&helicopterFileServiceMock{}))
		resp, _ := doHelicopterFileRequest(t, app, http.MethodPost, "/cancel-upload", []byte(`{"data":[]}`))
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400 without file manager, got %d", resp.StatusCode)
		}
	})

	t.Run("cancel upload with configured file manager covers early branches", func(t *testing.T) {
		fileSvc := service.NewFileManagerService(nil, nil, nil, service.FileManagerServiceDependencies{})
		app := newHelicopterFileTestApp(NewHelicopterFileHandler(&helicopterFileServiceMock{}).WithFileManagerService(fileSvc))

		resp, _ := doHelicopterFileRequest(t, app, http.MethodPost, "/cancel-upload", []byte(`{"data":{"type":"x"}}`))
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422 non-array payload, got %d", resp.StatusCode)
		}

		resp, _ = doHelicopterFileRequest(t, app, http.MethodPost, "/cancel-upload", []byte(`{"data":[]}`))
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422 empty array, got %d", resp.StatusCode)
		}

		resp, _ = doHelicopterFileRequest(t, app, http.MethodPost, "/cancel-upload", []byte(`{"data":[{"type":"wrong","attributes":{"upload_intent_uuid":"bad"}}]}`))
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200 invalid type item bulk response, got %d", resp.StatusCode)
		}
	})

	t.Run("cancel upload invalid json invalid uuid action error and success", func(t *testing.T) {
		cancelErr := errors.New("cancel failed")
		fileSvc := &fileManagerFileHandlerServiceMock{
			cancelUploadFn: func(_ context.Context, params filemanager.CancelFileUploadIntentParams) error {
				if strings.HasPrefix(mustUUIDString(t, params.UploadIntentID), "0") {
					return nil
				}
				return cancelErr
			},
		}
		app := newHelicopterFileTestApp(NewHelicopterFileHandler(&helicopterFileServiceMock{}).WithFileManagerService(fileSvc))

		resp, _ := doHelicopterFileRequest(t, app, http.MethodPost, "/cancel-upload", []byte(`{"data":`))
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400 invalid json, got %d", resp.StatusCode)
		}

		resp, _ = doHelicopterFileRequest(t, app, http.MethodPost, "/cancel-upload", []byte(`{"data":[{"type":"helicopter_file_cancel_upload","attributes":{"upload_intent_uuid":"bad"}}]}`))
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200 bulk response for invalid upload_intent_uuid item, got %d", resp.StatusCode)
		}

		firstID := mustUUIDString(t, uuidv7.MustBytes())
		secondID := mustUUIDString(t, uuidv7.MustBytes())
		body := []byte(`{"data":[{"type":"helicopter_file_cancel_upload","attributes":{"upload_intent_uuid":"` + firstID + `"}},{"type":"helicopter_file_cancel_upload","attributes":{"upload_intent_uuid":"` + secondID + `"}}]}`)
		resp, _ = doHelicopterFileRequest(t, app, http.MethodPost, "/cancel-upload", body)
		if resp.StatusCode != http.StatusMultiStatus && resp.StatusCode != http.StatusServiceUnavailable && resp.StatusCode != http.StatusOK {
			t.Fatalf("expected mapped bulk response status, got %d", resp.StatusCode)
		}
	})

	t.Run("create validation branches", func(t *testing.T) {
		svc := &helicopterFileServiceMock{}
		app := newHelicopterFileTestApp(NewHelicopterFileHandler(svc))

		resp, _ := doHelicopterFileRequest(t, app, http.MethodPost, "/create", []byte(`{"data":`))
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400 invalid json, got %d", resp.StatusCode)
		}

		body := []byte(`{"data":{"type":"helicopter_file_create","attributes":{"section":"bad","helicopter_id":"` + mustUUIDString(t, uuidv7.MustBytes()) + `","upload_intent_uuid":"` + mustUUIDString(t, uuidv7.MustBytes()) + `","file_name":"A.pdf"}}}`)
		resp, _ = doHelicopterFileRequest(t, app, http.MethodPost, "/create", body)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422 invalid section, got %d", resp.StatusCode)
		}

		validHeli := mustUUIDString(t, uuidv7.MustBytes())
		body = []byte(`{"data":{"type":"helicopter_file_create","attributes":{"section":"bfi","helicopter_id":"` + validHeli + `","upload_intent_uuid":"` + mustUUIDString(t, uuidv7.MustBytes()) + `","file_name":"A.pdf"}}}`)
		resp, _ = doHelicopterFileRequest(t, app, http.MethodPost, "/create", body)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422 helicopter not found, got %d", resp.StatusCode)
		}

		svc.helicopterExists = true
		body = []byte(`{"data":{"type":"helicopter_file_create","attributes":{"section":"bfi","helicopter_id":"` + validHeli + `","upload_intent_uuid":"bad","file_name":"A.pdf"}}}`)
		resp, _ = doHelicopterFileRequest(t, app, http.MethodPost, "/create", body)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422 invalid upload_intent_uuid, got %d", resp.StatusCode)
		}

		body = []byte(`{"data":{"type":"helicopter_file_create","attributes":{"section":"bfi","helicopter_id":"` + validHeli + `","upload_intent_uuid":"` + mustUUIDString(t, uuidv7.MustBytes()) + `","file_name":""}}}`)
		resp, _ = doHelicopterFileRequest(t, app, http.MethodPost, "/create", body)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422 empty file_name, got %d", resp.StatusCode)
		}

		body = []byte(`{"data":{"type":"helicopter_file_create","attributes":{"section":"bfi","helicopter_id":"` + validHeli + `","upload_intent_uuid":"` + mustUUIDString(t, uuidv7.MustBytes()) + `","file_name":"A.pdf"}}}`)
		resp, _ = doHelicopterFileRequest(t, app, http.MethodPost, "/create", body)
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400 when file manager not configured, got %d", resp.StatusCode)
		}

		svc.helicopterExistsErr = errors.New("lookup failed")
		resp, _ = doHelicopterFileRequest(t, app, http.MethodPost, "/create", body)
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400 helicopter lookup error, got %d", resp.StatusCode)
		}
		svc.helicopterExistsErr = nil
		svc.helicopterExists = true

		body = []byte(`{"data":{"type":"helicopter_file_create","attributes":{"section":"bfi","helicopter_id":"` + validHeli + `","upload_intent_uuid":"` + mustUUIDString(t, uuidv7.MustBytes()) + `","file_name":"A.pdf","folder_uuid":"bad"}}}`)
		resp, _ = doHelicopterFileRequest(t, app, http.MethodPost, "/create", body)
		if resp.StatusCode != http.StatusBadRequest && resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 400/422 invalid folder_uuid, got %d", resp.StatusCode)
		}
	})

	t.Run("create bulk multi-status", func(t *testing.T) {
		row := &helicopterfile.HelicopterFile{
			ID:               uuidv7.MustBytes(),
			HelicopterID:     uuidv7.MustBytes(),
			FileAttachmentID: uuidv7.MustBytes(),
			Section:          helicopterfile.SectionBeforeFirstFlightInspection,
			CreatedAt:        time.Now().UTC(),
			UpdatedAt:        time.Now().UTC(),
		}
		svc := &helicopterFileServiceMock{
			createBulk: []helicopterfile.CreateBulkItemResult{
				{Index: 0, Success: true, Row: row},
				{Index: 1, Success: false, Error: &helicopterfile.BulkItemError{Status: http.StatusBadRequest, Title: "Create failed", Detail: "invalid"}},
			},
		}
		app := newHelicopterFileTestApp(NewHelicopterFileHandler(svc))
		body := []byte(`{"data":[{"type":"helicopter_file_create","attributes":{"section":"bfi","helicopter_id":"` + mustUUIDString(t, uuidv7.MustBytes()) + `","upload_intent_uuid":"` + mustUUIDString(t, uuidv7.MustBytes()) + `","file_name":"A.pdf"}},{"type":"helicopter_file_create","attributes":{"section":"afi","helicopter_id":"` + mustUUIDString(t, uuidv7.MustBytes()) + `","upload_intent_uuid":"` + mustUUIDString(t, uuidv7.MustBytes()) + `","file_name":"B.pdf"}}]}`)
		resp, _ := doHelicopterFileRequest(t, app, http.MethodPost, "/create", body)
		if resp.StatusCode != http.StatusMultiStatus {
			t.Fatalf("expected 207, got %d", resp.StatusCode)
		}
	})

	t.Run("create single success", func(t *testing.T) {
		heliID := uuidv7.MustBytes()
		fileID := uuidv7.MustBytes()
		attachmentID := uuidv7.MustBytes()
		svc := &helicopterFileServiceMock{
			helicopterExists: true,
			createFn: func(_ context.Context, row *helicopterfile.HelicopterFile) error {
				if row != nil && len(row.ID) == 0 {
					row.ID = fileID
				}
				if row != nil && row.UpdatedAt.IsZero() {
					now := time.Now().UTC()
					row.CreatedAt = now
					row.UpdatedAt = now
				}
				return nil
			},
		}
		fm := &helicopterFileManagerMock{
			createFileFn: func(_ context.Context, _ filemanager.CreateFileFromUploadIntentParams) (*filemanager.File, error) {
				return &filemanager.File{
					ID:        fileID,
					Status:    filemanager.FileStatusReady,
					CreatedAt: time.Now().UTC(),
					UpdatedAt: time.Now().UTC(),
				}, nil
			},
			createAttachmentFn: func(_ context.Context, _ filemanager.CreateAttachmentParams) (*filemanager.Attachment, error) {
				return &filemanager.Attachment{ID: attachmentID, FileID: fileID}, nil
			},
		}
		app := newHelicopterFileTestApp(NewHelicopterFileHandler(svc).WithFileManagerService(fm))
		body := []byte(`{"data":{"type":"helicopter_file_create","attributes":{"section":"bfi","helicopter_id":"` + mustUUIDString(t, heliID) + `","upload_intent_uuid":"` + mustUUIDString(t, uuidv7.MustBytes()) + `","file_name":"A.pdf"}}}`)
		resp, respBody := doHelicopterFileRequest(t, app, http.MethodPost, "/create", body)
		if resp.StatusCode != http.StatusCreated {
			t.Fatalf("expected 201, got %d body=%s", resp.StatusCode, string(respBody))
		}
		if fm.lastCreateFile == nil || !fm.lastCreateFile.IsTemplate {
			t.Fatalf("expected helicopter file create flow to mark is_template=true")
		}
	})

	t.Run("create publishes helicopter file event", func(t *testing.T) {
		actorID := uuidv7.MustBytes()
		heliID := uuidv7.MustBytes()
		helicopterFileID := uuidv7.MustBytes()
		fileID := uuidv7.MustBytes()
		svc := &helicopterFileServiceMock{
			helicopterExists: true,
			createFn: func(_ context.Context, row *helicopterfile.HelicopterFile) error {
				if row != nil && len(row.ID) == 0 {
					row.ID = helicopterFileID
				}
				if row != nil && row.UpdatedAt.IsZero() {
					now := time.Now().UTC()
					row.CreatedAt = now
					row.UpdatedAt = now
				}
				return nil
			},
		}
		fm := &helicopterFileManagerMock{
			createFileFn: func(_ context.Context, _ filemanager.CreateFileFromUploadIntentParams) (*filemanager.File, error) {
				return &filemanager.File{
					ID:        fileID,
					Name:      "A.pdf",
					MimeType:  "application/pdf",
					Status:    filemanager.FileStatusReady,
					CreatedAt: time.Now().UTC(),
					UpdatedAt: time.Now().UTC(),
				}, nil
			},
		}
		events := &helicopterFileEventsMock{}
		app := newHelicopterFileTestAppWithUser(
			NewHelicopterFileHandler(svc).
				WithFileManagerService(fm).
				WithHelicopterFileEvents(events),
			actorID,
		)
		body := []byte(`{"data":{"type":"helicopter_file_create","attributes":{"section":"bfi","helicopter_id":"` + mustUUIDString(t, heliID) + `","upload_intent_uuid":"` + mustUUIDString(t, uuidv7.MustBytes()) + `","file_name":"A.pdf"}}}`)
		resp, respBody := doHelicopterFileRequest(t, app, http.MethodPost, "/create", body)
		if resp.StatusCode != http.StatusCreated {
			t.Fatalf("expected 201, got %d body=%s", resp.StatusCode, string(respBody))
		}
		if events.publishCalls != 1 {
			t.Fatalf("expected one helicopter file event publish, got %d", events.publishCalls)
		}
		if len(events.publishedUsers) != 1 {
			t.Fatalf("expected one published user, got %v", events.publishedUsers)
		}
		expectedUser, _ := uuidv7.BytesToString(actorID)
		if events.publishedUsers[0] != expectedUser {
			t.Fatalf("expected event for user %s, got %s", expectedUser, events.publishedUsers[0])
		}
		if events.event == nil {
			t.Fatalf("expected published event")
		}
		expectedHeli, _ := uuidv7.BytesToString(heliID)
		expectedHeliFileID, _ := uuidv7.BytesToString(helicopterFileID)
		expectedFile, _ := uuidv7.BytesToString(fileID)
		if events.event.HelicopterID != expectedHeli || events.event.FileID != expectedFile || events.event.HelicopterFileID != expectedHeliFileID {
			t.Fatalf("unexpected published event: %+v", events.event)
		}
		if events.event.EventType != helicopterfilerealtime.DefaultEventType {
			t.Fatalf("expected default helicopter file event type, got %s", events.event.EventType)
		}
		if !events.event.AttachmentPending {
			t.Fatalf("expected attachment pending event, got %+v", events.event)
		}
	})

	t.Run("create sanitizes malformed xlsx templates before validation", func(t *testing.T) {
		heliID := uuidv7.MustBytes()
		fileID := uuidv7.MustBytes()
		objectKey := "files/template.xlsx"
		original := testMalformedXLSXZip(t)
		storage := &templateCopyStorageMock{
			objects: map[string][]byte{
				objectKey: append([]byte(nil), original...),
			},
			contentTypes: map[string]string{
				objectKey: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
			},
		}
		svc := &helicopterFileServiceMock{helicopterExists: true}
		fm := &helicopterFileManagerMock{
			createFileFn: func(_ context.Context, _ filemanager.CreateFileFromUploadIntentParams) (*filemanager.File, error) {
				return &filemanager.File{
					ID:        fileID,
					Name:      "template.xlsx",
					ObjectKey: objectKey,
					MimeType:  "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
					SizeBytes: int64(len(original)),
					Status:    filemanager.FileStatusReady,
					CreatedAt: time.Now().UTC(),
					UpdatedAt: time.Now().UTC(),
				}, nil
			},
		}
		app := newHelicopterFileTestApp(
			NewHelicopterFileHandler(svc).
				WithFileManagerService(fm).
				WithFileStorage(storage),
		)
		body := []byte(`{"data":{"type":"helicopter_file_create","attributes":{"section":"bfi","helicopter_id":"` + mustUUIDString(t, heliID) + `","upload_intent_uuid":"` + mustUUIDString(t, uuidv7.MustBytes()) + `","file_name":"template.xlsx"}}}`)
		resp, respBody := doHelicopterFileRequest(t, app, http.MethodPost, "/create", body)
		if resp.StatusCode != http.StatusCreated {
			t.Fatalf("expected 201, got %d body=%s", resp.StatusCode, string(respBody))
		}
		if fm.updateFileCalls != 1 {
			t.Fatalf("expected sanitized xlsx metadata to be persisted once, got %d", fm.updateFileCalls)
		}
		vml := zipEntryPayload(t, storage.objects[objectKey], "xl/drawings/vmlDrawing1.vml")
		if !bytes.Contains(vml, []byte(`margin-left:73.5pt`)) {
			t.Fatalf("expected uploaded template to be sanitized, got: %s", string(vml))
		}
	})

	t.Run("create duplicate name resolves existing file", func(t *testing.T) {
		heliID := uuidv7.MustBytes()
		fileID := uuidv7.MustBytes()
		attachmentID := uuidv7.MustBytes()
		rootID := uuidv7.MustBytes()
		svc := &helicopterFileServiceMock{helicopterExists: true}
		fm := &helicopterFileManagerMock{
			createFileFn: func(_ context.Context, _ filemanager.CreateFileFromUploadIntentParams) (*filemanager.File, error) {
				return nil, filemanager.ErrDuplicateName
			},
			getRootFn: func(_ context.Context) (*filemanager.Folder, error) {
				return &filemanager.Folder{ID: rootID}, nil
			},
			getByFolderNameFn: func(_ context.Context, folderID []byte, _ string) (*filemanager.File, error) {
				return &filemanager.File{ID: fileID, Status: filemanager.FileStatusReady, FolderID: folderID, CreatedAt: time.Now().UTC(), UpdatedAt: time.Now().UTC()}, nil
			},
			createAttachmentFn: func(_ context.Context, _ filemanager.CreateAttachmentParams) (*filemanager.Attachment, error) {
				return &filemanager.Attachment{ID: attachmentID, FileID: fileID}, nil
			},
		}
		app := newHelicopterFileTestApp(NewHelicopterFileHandler(svc).WithFileManagerService(fm))
		body := []byte(`{"data":{"type":"helicopter_file_create","attributes":{"section":"bfi","helicopter_id":"` + mustUUIDString(t, heliID) + `","upload_intent_uuid":"` + mustUUIDString(t, uuidv7.MustBytes()) + `","file_name":"A.pdf"}}}`)
		resp, _ := doHelicopterFileRequest(t, app, http.MethodPost, "/create", body)
		if resp.StatusCode != http.StatusCreated {
			t.Fatalf("expected 201, got %d", resp.StatusCode)
		}
	})

	t.Run("create service write error", func(t *testing.T) {
		heliID := uuidv7.MustBytes()
		fileID := uuidv7.MustBytes()
		attachmentID := uuidv7.MustBytes()
		svc := &helicopterFileServiceMock{
			helicopterExists: true,
			createErr:        errors.New("insert failed"),
		}
		fm := &helicopterFileManagerMock{
			createFileFn: func(_ context.Context, _ filemanager.CreateFileFromUploadIntentParams) (*filemanager.File, error) {
				return &filemanager.File{
					ID:        fileID,
					Status:    filemanager.FileStatusReady,
					CreatedAt: time.Now().UTC(),
					UpdatedAt: time.Now().UTC(),
				}, nil
			},
			createAttachmentFn: func(_ context.Context, _ filemanager.CreateAttachmentParams) (*filemanager.Attachment, error) {
				return &filemanager.Attachment{ID: attachmentID, FileID: fileID}, nil
			},
		}
		app := newHelicopterFileTestApp(NewHelicopterFileHandler(svc).WithFileManagerService(fm))
		body := []byte(`{"data":{"type":"helicopter_file_create","attributes":{"section":"bfi","helicopter_id":"` + mustUUIDString(t, heliID) + `","upload_intent_uuid":"` + mustUUIDString(t, uuidv7.MustBytes()) + `","file_name":"A.pdf"}}}`)
		req, _ := http.NewRequest(http.MethodPost, "/create", bytes.NewReader(body))
		req.Header.Set("Content-Type", "application/json")
		resp, err := app.Test(req, 7000)
		if err != nil {
			t.Fatalf("app.Test: %v", err)
		}
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})

	t.Run("create resolves attachment from existing list when already attached", func(t *testing.T) {
		heliID := uuidv7.MustBytes()
		fileID := uuidv7.MustBytes()
		attachmentID := uuidv7.MustBytes()
		svc := &helicopterFileServiceMock{helicopterExists: true}
		fm := &helicopterFileManagerMock{
			createFileFn: func(_ context.Context, _ filemanager.CreateFileFromUploadIntentParams) (*filemanager.File, error) {
				return &filemanager.File{
					ID:        fileID,
					Status:    filemanager.FileStatusReady,
					CreatedAt: time.Now().UTC(),
					UpdatedAt: time.Now().UTC(),
				}, nil
			},
			createAttachmentFn: func(_ context.Context, _ filemanager.CreateAttachmentParams) (*filemanager.Attachment, error) {
				return nil, filemanager.ErrAttachmentAlreadyExists
			},
			listAttachmentRefFn: func(_ context.Context, _ filemanager.ListAttachmentsParams) ([]filemanager.Attachment, int64, error) {
				return []filemanager.Attachment{{ID: attachmentID, FileID: fileID}}, 1, nil
			},
		}
		app := newHelicopterFileTestApp(NewHelicopterFileHandler(svc).WithFileManagerService(fm))
		body := []byte(`{"data":{"type":"helicopter_file_create","attributes":{"section":"bfi","helicopter_id":"` + mustUUIDString(t, heliID) + `","upload_intent_uuid":"` + mustUUIDString(t, uuidv7.MustBytes()) + `","file_name":"A.pdf"}}}`)
		resp, _ := doHelicopterFileRequest(t, app, http.MethodPost, "/create", body)
		if resp.StatusCode != http.StatusCreated {
			t.Fatalf("expected 201, got %d", resp.StatusCode)
		}
	})

	t.Run("create returns processing failed when file pipeline fails", func(t *testing.T) {
		heliID := uuidv7.MustBytes()
		fileID := uuidv7.MustBytes()
		svc := &helicopterFileServiceMock{helicopterExists: true}
		fm := &helicopterFileManagerMock{
			createFileFn: func(_ context.Context, _ filemanager.CreateFileFromUploadIntentParams) (*filemanager.File, error) {
				return &filemanager.File{ID: fileID, Status: filemanager.FileStatusProcessing}, nil
			},
			getFileByIDFn: func(_ context.Context, _ []byte) (*filemanager.File, error) {
				return &filemanager.File{ID: fileID, Status: filemanager.FileStatusFailed, FailureReason: "pipeline failed"}, nil
			},
		}
		app := newHelicopterFileTestApp(NewHelicopterFileHandler(svc).WithFileManagerService(fm))
		body := []byte(`{"data":{"type":"helicopter_file_create","attributes":{"section":"bfi","helicopter_id":"` + mustUUIDString(t, heliID) + `","upload_intent_uuid":"` + mustUUIDString(t, uuidv7.MustBytes()) + `","file_name":"A.pdf"}}}`)
		resp, _ := doHelicopterFileRequest(t, app, http.MethodPost, "/create", body)
		if resp.StatusCode != http.StatusCreated {
			t.Fatalf("expected 201 pending create, got %d", resp.StatusCode)
		}
	})

	t.Run("create processing poll get file error", func(t *testing.T) {
		heliID := uuidv7.MustBytes()
		fileID := uuidv7.MustBytes()
		svc := &helicopterFileServiceMock{helicopterExists: true}
		fm := &helicopterFileManagerMock{
			createFileFn: func(_ context.Context, _ filemanager.CreateFileFromUploadIntentParams) (*filemanager.File, error) {
				return &filemanager.File{ID: fileID, Status: filemanager.FileStatusProcessing}, nil
			},
			getFileByIDFn: func(_ context.Context, _ []byte) (*filemanager.File, error) {
				return nil, errors.New("poll failed")
			},
		}
		app := newHelicopterFileTestApp(NewHelicopterFileHandler(svc).WithFileManagerService(fm))
		body := []byte(`{"data":{"type":"helicopter_file_create","attributes":{"section":"bfi","helicopter_id":"` + mustUUIDString(t, heliID) + `","upload_intent_uuid":"` + mustUUIDString(t, uuidv7.MustBytes()) + `","file_name":"A.pdf"}}}`)
		resp, _ := doHelicopterFileRequest(t, app, http.MethodPost, "/create", body)
		if resp.StatusCode != http.StatusCreated {
			t.Fatalf("expected 201 pending create, got %d", resp.StatusCode)
		}
	})

	t.Run("create returns processing retry when file stays processing", func(t *testing.T) {
		t.Skip("fiber test timeout too short for processing retry branch")
		heliID := uuidv7.MustBytes()
		fileID := uuidv7.MustBytes()
		svc := &helicopterFileServiceMock{helicopterExists: true}
		fm := &helicopterFileManagerMock{
			createFileFn: func(_ context.Context, _ filemanager.CreateFileFromUploadIntentParams) (*filemanager.File, error) {
				return &filemanager.File{ID: fileID, Status: filemanager.FileStatusProcessing}, nil
			},
			getFileByIDFn: func(_ context.Context, _ []byte) (*filemanager.File, error) {
				return &filemanager.File{ID: fileID, Status: filemanager.FileStatusProcessing}, nil
			},
		}
		app := newHelicopterFileTestApp(NewHelicopterFileHandler(svc).WithFileManagerService(fm))
		body := []byte(`{"data":{"type":"helicopter_file_create","attributes":{"section":"bfi","helicopter_id":"` + mustUUIDString(t, heliID) + `","upload_intent_uuid":"` + mustUUIDString(t, uuidv7.MustBytes()) + `","file_name":"A.pdf"}}}`)
		resp, _ := doHelicopterFileRequest(t, app, http.MethodPost, "/create", body)
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})

	t.Run("create duplicate root resolve error", func(t *testing.T) {
		heliID := uuidv7.MustBytes()
		svc := &helicopterFileServiceMock{helicopterExists: true}
		fm := &helicopterFileManagerMock{
			createFileFn: func(_ context.Context, _ filemanager.CreateFileFromUploadIntentParams) (*filemanager.File, error) {
				return nil, filemanager.ErrDuplicateName
			},
			getRootFn: func(_ context.Context) (*filemanager.Folder, error) {
				return nil, errors.New("root failed")
			},
		}
		app := newHelicopterFileTestApp(NewHelicopterFileHandler(svc).WithFileManagerService(fm))
		body := []byte(`{"data":{"type":"helicopter_file_create","attributes":{"section":"bfi","helicopter_id":"` + mustUUIDString(t, heliID) + `","upload_intent_uuid":"` + mustUUIDString(t, uuidv7.MustBytes()) + `","file_name":"A.pdf"}}}`)
		resp, _ := doHelicopterFileRequest(t, app, http.MethodPost, "/create", body)
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})

	t.Run("create duplicate unresolved existing file", func(t *testing.T) {
		heliID := uuidv7.MustBytes()
		svc := &helicopterFileServiceMock{helicopterExists: true}
		fm := &helicopterFileManagerMock{
			createFileFn: func(_ context.Context, _ filemanager.CreateFileFromUploadIntentParams) (*filemanager.File, error) {
				return nil, filemanager.ErrDuplicateName
			},
			getRootFn: func(_ context.Context) (*filemanager.Folder, error) {
				return &filemanager.Folder{ID: uuidv7.MustBytes()}, nil
			},
			getByFolderNameFn: func(_ context.Context, _ []byte, _ string) (*filemanager.File, error) {
				return nil, errors.New("lookup failed")
			},
		}
		app := newHelicopterFileTestApp(NewHelicopterFileHandler(svc).WithFileManagerService(fm))
		body := []byte(`{"data":{"type":"helicopter_file_create","attributes":{"section":"bfi","helicopter_id":"` + mustUUIDString(t, heliID) + `","upload_intent_uuid":"` + mustUUIDString(t, uuidv7.MustBytes()) + `","file_name":"A.pdf"}}}`)
		resp, _ := doHelicopterFileRequest(t, app, http.MethodPost, "/create", body)
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})

	t.Run("create returns bad request when file node missing", func(t *testing.T) {
		heliID := uuidv7.MustBytes()
		svc := &helicopterFileServiceMock{helicopterExists: true}
		fm := &helicopterFileManagerMock{
			createFileFn: func(_ context.Context, _ filemanager.CreateFileFromUploadIntentParams) (*filemanager.File, error) {
				return nil, nil
			},
		}
		app := newHelicopterFileTestApp(NewHelicopterFileHandler(svc).WithFileManagerService(fm))
		body := []byte(`{"data":{"type":"helicopter_file_create","attributes":{"section":"bfi","helicopter_id":"` + mustUUIDString(t, heliID) + `","upload_intent_uuid":"` + mustUUIDString(t, uuidv7.MustBytes()) + `","file_name":"A.pdf"}}}`)
		resp, _ := doHelicopterFileRequest(t, app, http.MethodPost, "/create", body)
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})

	t.Run("update validation branches", func(t *testing.T) {
		app := newHelicopterFileTestApp(NewHelicopterFileHandler(&helicopterFileServiceMock{}))
		resp, _ := doHelicopterFileRequest(t, app, http.MethodPatch, "/update", []byte(`{"data":`))
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400 invalid json, got %d", resp.StatusCode)
		}

		resp, _ = doHelicopterFileRequest(t, app, http.MethodPatch, "/update", []byte(`{"data":{"type":"helicopter_file","id":"bad","attributes":{}}}`))
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422 invalid id, got %d", resp.StatusCode)
		}

		id := mustUUIDString(t, uuidv7.MustBytes())
		resp, _ = doHelicopterFileRequest(t, app, http.MethodPatch, "/update", []byte(`{"data":{"type":"helicopter_file","id":"`+id+`","attributes":{}}}`))
		if resp.StatusCode != http.StatusNotFound {
			t.Fatalf("expected 404 not found, got %d", resp.StatusCode)
		}

		svc := &helicopterFileServiceMock{
			getRow: &helicopterfile.HelicopterFile{
				ID:               uuidv7.MustBytes(),
				HelicopterID:     uuidv7.MustBytes(),
				FileAttachmentID: uuidv7.MustBytes(),
			},
		}
		appWithRow := newHelicopterFileTestApp(NewHelicopterFileHandler(svc))
		resp, _ = doHelicopterFileRequest(t, appWithRow, http.MethodPatch, "/update", []byte(`{"data":{"type":"helicopter_file","id":"`+id+`","attributes":{"section":"wrong"}}}`))
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422 invalid section, got %d", resp.StatusCode)
		}

		rawBadHeli := "bad-heli"
		resp, _ = doHelicopterFileRequest(t, appWithRow, http.MethodPatch, "/update", []byte(`{"data":{"type":"helicopter_file","id":"`+id+`","attributes":{"helicopter_id":"`+rawBadHeli+`"}}}`))
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422 invalid helicopter_id, got %d", resp.StatusCode)
		}

		svc.helicopterExistsErr = errors.New("exists failed")
		validHeli := mustUUIDString(t, uuidv7.MustBytes())
		resp, _ = doHelicopterFileRequest(t, appWithRow, http.MethodPatch, "/update", []byte(`{"data":{"type":"helicopter_file","id":"`+id+`","attributes":{"helicopter_id":"`+validHeli+`"}}}`))
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400 helicopter exists error, got %d", resp.StatusCode)
		}
		svc.helicopterExistsErr = nil
		svc.helicopterExists = false
		resp, _ = doHelicopterFileRequest(t, appWithRow, http.MethodPatch, "/update", []byte(`{"data":{"type":"helicopter_file","id":"`+id+`","attributes":{"helicopter_id":"`+validHeli+`"}}}`))
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422 helicopter not found, got %d", resp.StatusCode)
		}

		resp, _ = doHelicopterFileRequest(t, appWithRow, http.MethodPatch, "/update", []byte(`{"data":{"type":"helicopter_file","id":"`+id+`","attributes":{"file_name":" "}}}`))
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422 empty file_name, got %d", resp.StatusCode)
		}

		resp, _ = doHelicopterFileRequest(t, appWithRow, http.MethodPatch, "/update", []byte(`{"data":{"type":"helicopter_file","id":"`+id+`","attributes":{"file_name":"A.pdf"}}}`))
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400 when file manager missing, got %d", resp.StatusCode)
		}

		svc.updateErr = errors.New("update failed")
		resp, _ = doHelicopterFileRequest(t, appWithRow, http.MethodPatch, "/update", []byte(`{"data":{"type":"helicopter_file","id":"`+id+`","attributes":{}}}`))
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400 update error, got %d", resp.StatusCode)
		}
		svc.updateErr = nil
		svc.getErr = errors.New("get failed")
		resp, _ = doHelicopterFileRequest(t, appWithRow, http.MethodPatch, "/update", []byte(`{"data":{"type":"helicopter_file","id":"`+id+`","attributes":{}}}`))
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400 get failed, got %d", resp.StatusCode)
		}
	})

	t.Run("update success minimal", func(t *testing.T) {
		id := uuidv7.MustBytes()
		svc := &helicopterFileServiceMock{
			getRow: &helicopterfile.HelicopterFile{
				ID:               id,
				HelicopterID:     uuidv7.MustBytes(),
				FileAttachmentID: uuidv7.MustBytes(),
				Section:          helicopterfile.SectionBeforeFirstFlightInspection,
				CreatedAt:        time.Now().UTC(),
				UpdatedAt:        time.Now().UTC(),
			},
		}
		app := newHelicopterFileTestApp(NewHelicopterFileHandler(svc))
		resp, body := doHelicopterFileRequest(t, app, http.MethodPatch, "/update", []byte(`{"data":{"type":"helicopter_file","id":"`+mustUUIDString(t, id)+`","attributes":{}}}`))
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d body=%s", resp.StatusCode, string(body))
		}
	})

	t.Run("update file_name path success", func(t *testing.T) {
		id := uuidv7.MustBytes()
		fileID := uuidv7.MustBytes()
		attID := uuidv7.MustBytes()
		svc := &helicopterFileServiceMock{
			getRow: &helicopterfile.HelicopterFile{
				ID:               id,
				HelicopterID:     uuidv7.MustBytes(),
				FileAttachmentID: attID,
				Section:          helicopterfile.SectionBeforeFirstFlightInspection,
				CreatedAt:        time.Now().UTC(),
				UpdatedAt:        time.Now().UTC(),
			},
		}
		fm := &helicopterFileManagerMock{
			getAttachmentByIDFn: func(_ context.Context, _ []byte) (*filemanager.Attachment, error) {
				return &filemanager.Attachment{ID: attID, FileID: fileID}, nil
			},
			moveFileFn: func(_ context.Context, _ []byte, _ filemanager.MoveFileParams) (*filemanager.File, error) {
				return &filemanager.File{ID: fileID}, nil
			},
		}
		app := newHelicopterFileTestApp(NewHelicopterFileHandler(svc).WithFileManagerService(fm))
		resp, _ := doHelicopterFileRequest(t, app, http.MethodPatch, "/update", []byte(`{"data":{"type":"helicopter_file","id":"`+mustUUIDString(t, id)+`","attributes":{"file_name":"Renamed.pdf"}}}`))
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d", resp.StatusCode)
		}
	})

	t.Run("update bulk mixed results", func(t *testing.T) {
		id := uuidv7.MustBytes()
		svc := &helicopterFileServiceMock{
			getRow: &helicopterfile.HelicopterFile{
				ID:               id,
				HelicopterID:     uuidv7.MustBytes(),
				FileAttachmentID: uuidv7.MustBytes(),
				Section:          helicopterfile.SectionBeforeFirstFlightInspection,
				CreatedAt:        time.Now().UTC(),
				UpdatedAt:        time.Now().UTC(),
			},
		}
		app := newHelicopterFileTestApp(NewHelicopterFileHandler(svc))

		body := []byte(`{
				"data": [
					{"type":"helicopter_file_update","id":"` + mustUUIDString(t, id) + `","attributes":{"position":2}},
					{"type":"helicopter_file_update","id":"bad-id","attributes":{"position":3}}
				]
			}`)
		resp, raw := doHelicopterFileRequest(t, app, http.MethodPatch, "/update", body)
		if resp.StatusCode != http.StatusMultiStatus {
			t.Fatalf("expected 207, got %d body=%s", resp.StatusCode, string(raw))
		}
		if !strings.Contains(string(raw), `"partial_success":true`) {
			t.Fatalf("expected partial_success true, got %s", string(raw))
		}
	})

	t.Run("update bulk invalid envelope branches", func(t *testing.T) {
		app := newHelicopterFileTestApp(NewHelicopterFileHandler(&helicopterFileServiceMock{}))

		resp, _ := doHelicopterFileRequest(t, app, http.MethodPatch, "/update", []byte(`{"data":[]}`))
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422 for empty bulk, got %d", resp.StatusCode)
		}

		items := make([]string, 0, 101)
		for i := 0; i < 101; i++ {
			items = append(items, `{"type":"helicopter_file_update","id":"`+mustUUIDString(t, uuidv7.MustBytes())+`","attributes":{}}`)
		}
		body := []byte(`{"data":[` + strings.Join(items, ",") + `]}`)
		resp, _ = doHelicopterFileRequest(t, app, http.MethodPatch, "/update", body)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422 for too many bulk items, got %d", resp.StatusCode)
		}
	})

	t.Run("update one file rename and lifecycle error branches", func(t *testing.T) {
		id := uuidv7.MustBytes()
		attID := uuidv7.MustBytes()
		baseRow := &helicopterfile.HelicopterFile{
			ID:               id,
			HelicopterID:     uuidv7.MustBytes(),
			FileAttachmentID: attID,
			Section:          helicopterfile.SectionBeforeFirstFlightInspection,
			CreatedAt:        time.Now().UTC(),
			UpdatedAt:        time.Now().UTC(),
		}

		t.Run("attachment lookup error", func(t *testing.T) {
			svc := &helicopterFileServiceMock{getRow: baseRow}
			fm := &helicopterFileManagerMock{
				getAttachmentByIDFn: func(context.Context, []byte) (*filemanager.Attachment, error) {
					return nil, errors.New("attachment failed")
				},
			}
			app := newHelicopterFileTestApp(NewHelicopterFileHandler(svc).WithFileManagerService(fm))
			resp, _ := doHelicopterFileRequest(t, app, http.MethodPatch, "/update", []byte(`{"data":{"type":"helicopter_file","id":"`+mustUUIDString(t, id)+`","attributes":{"file_name":"Renamed.pdf"}}}`))
			if resp.StatusCode != http.StatusBadRequest {
				t.Fatalf("expected 400, got %d", resp.StatusCode)
			}
		})

		t.Run("attachment not found", func(t *testing.T) {
			svc := &helicopterFileServiceMock{getRow: baseRow}
			fm := &helicopterFileManagerMock{
				getAttachmentByIDFn: func(context.Context, []byte) (*filemanager.Attachment, error) {
					return nil, nil
				},
			}
			app := newHelicopterFileTestApp(NewHelicopterFileHandler(svc).WithFileManagerService(fm))
			resp, _ := doHelicopterFileRequest(t, app, http.MethodPatch, "/update", []byte(`{"data":{"type":"helicopter_file","id":"`+mustUUIDString(t, id)+`","attributes":{"file_name":"Renamed.pdf"}}}`))
			if resp.StatusCode != http.StatusNotFound {
				t.Fatalf("expected 404, got %d", resp.StatusCode)
			}
		})

		t.Run("move file error", func(t *testing.T) {
			svc := &helicopterFileServiceMock{getRow: baseRow}
			fm := &helicopterFileManagerMock{
				getAttachmentByIDFn: func(context.Context, []byte) (*filemanager.Attachment, error) {
					return &filemanager.Attachment{ID: attID, FileID: uuidv7.MustBytes()}, nil
				},
				moveFileFn: func(context.Context, []byte, filemanager.MoveFileParams) (*filemanager.File, error) {
					return nil, errors.New("move failed")
				},
			}
			app := newHelicopterFileTestApp(NewHelicopterFileHandler(svc).WithFileManagerService(fm))
			resp, _ := doHelicopterFileRequest(t, app, http.MethodPatch, "/update", []byte(`{"data":{"type":"helicopter_file","id":"`+mustUUIDString(t, id)+`","attributes":{"file_name":"Renamed.pdf"}}}`))
			if resp.StatusCode != http.StatusBadRequest {
				t.Fatalf("expected 400, got %d", resp.StatusCode)
			}
		})
	})

	t.Run("update bulk success", func(t *testing.T) {
		id := uuidv7.MustBytes()
		svc := &helicopterFileServiceMock{
			getRow: &helicopterfile.HelicopterFile{
				ID:               id,
				HelicopterID:     uuidv7.MustBytes(),
				FileAttachmentID: uuidv7.MustBytes(),
				Section:          helicopterfile.SectionBeforeFirstFlightInspection,
				CreatedAt:        time.Now().UTC(),
				UpdatedAt:        time.Now().UTC(),
			},
		}
		app := newHelicopterFileTestApp(NewHelicopterFileHandler(svc))

		body := []byte(`{
				"data": [
					{"type":"helicopter_file_update","id":"` + mustUUIDString(t, id) + `","attributes":{"position":2}}
				]
			}`)
		resp, raw := doHelicopterFileRequest(t, app, http.MethodPatch, "/update", body)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d body=%s", resp.StatusCode, string(raw))
		}
		if !strings.Contains(string(raw), `"success":1`) {
			t.Fatalf("expected bulk meta success 1, got %s", string(raw))
		}
	})

	t.Run("get list and delete", func(t *testing.T) {
		helicopterID := uuidv7.MustBytes()
		fileID := uuidv7.MustBytes()
		attachmentID := uuidv7.MustBytes()
		rows := []helicopterfile.HelicopterFile{
			{
				ID:               uuidv7.MustBytes(),
				HelicopterID:     helicopterID,
				FileAttachmentID: attachmentID,
				Section:          helicopterfile.SectionBeforeFirstFlightInspection,
				CreatedAt:        time.Now().UTC(),
				UpdatedAt:        time.Now().UTC(),
				FileAttachment: &filemanager.Attachment{
					ID: attachmentID,
					File: &filemanager.File{
						ID:        fileID,
						Name:      "A.pdf",
						ObjectKey: "files/A.pdf",
						SizeBytes: 11,
					},
				},
			},
		}
		svc := &helicopterFileServiceMock{
			helicopterExists: true,
			byHeliRows:       rows,
			listRows:         rows,
			listTotal:        1,
		}
		h := NewHelicopterFileHandler(svc).
			WithHelicopterService(&helicopterServiceMock{row: &helicopter.Helicopter{ID: helicopterID, Designation: "OE-XHZ"}}).
			WithFileStorage(&fileManagerDownloadURLStorageMock{presignedURL: "https://signed/file.pdf"})
		app := newHelicopterFileTestApp(h)

		resp, _ := doHelicopterFileRequest(t, app, http.MethodGet, "/get/bad", nil)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422 invalid helicopter uuid, got %d", resp.StatusCode)
		}

		resp, _ = doHelicopterFileRequest(t, app, http.MethodGet, "/get/"+mustUUIDString(t, helicopterID), nil)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200 get success, got %d", resp.StatusCode)
		}

		resp, _ = doHelicopterFileRequest(t, app, http.MethodGet, "/list?page[number]=0&page[size]=101", nil)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200 list success, got %d", resp.StatusCode)
		}

		takeoverID := uuidv7.MustBytes()
		actorID := uuidv7.MustBytes()
		filterRows := []helicopterfile.HelicopterFile{
			{
				ID:           uuidv7.MustBytes(),
				HelicopterID: helicopterID,
				SourceFileID: uuidv7.MustBytes(),
				Section:      helicopterfile.SectionFlightPreparationAndWB,
				CreatedAt:    time.Now().UTC(),
				UpdatedAt:    time.Now().UTC(),
				SourceFile: &filemanager.File{
					ID:         uuidv7.MustBytes(),
					Name:       "takeover-only.pdf",
					ObjectKey:  "files/takeover-only.pdf",
					SizeBytes:  11,
					TakeoverID: takeoverID,
				},
			},
			{
				ID:           uuidv7.MustBytes(),
				HelicopterID: helicopterID,
				SourceFileID: uuidv7.MustBytes(),
				Section:      helicopterfile.SectionFlightPreparationAndWB,
				CreatedAt:    time.Now().UTC(),
				UpdatedAt:    time.Now().UTC(),
				SourceFile: &filemanager.File{
					ID:        uuidv7.MustBytes(),
					Name:      "public.pdf",
					ObjectKey: "files/public.pdf",
					SizeBytes: 12,
				},
			},
		}
		filterSvc := &helicopterFileServiceMock{
			helicopterExists: true,
			byHeliRows:       filterRows,
			listRows:         filterRows,
			listTotal:        1,
		}
		filterApp := newHelicopterFileTestAppWithUser(
			NewHelicopterFileHandler(filterSvc).
				WithTakeoverService(&helicopterFileTakeoverServiceMock{
					row: &takeoverdomain.TakeoverAc{
						ID:        takeoverID,
						CreatedBy: uuidv7.MustBytes(),
					},
				}).
				WithHelicopterService(&helicopterServiceMock{row: &helicopter.Helicopter{ID: helicopterID, Designation: "OE-XHZ"}}).
				WithFileStorage(&fileManagerDownloadURLStorageMock{presignedURL: "https://signed/file.pdf"}),
			actorID,
		)

		resp, raw := doHelicopterFileRequest(t, filterApp, http.MethodGet, "/get/"+mustUUIDString(t, helicopterID), nil)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200 filtered get, got %d body=%s", resp.StatusCode, string(raw))
		}
		var filteredGet struct {
			Data struct {
				Attributes struct {
					Files struct {
						Prepare []responsedto.HelicopterFileCollectionItem `json:"prepare"`
					} `json:"files"`
				} `json:"attributes"`
			} `json:"data"`
		}
		if err := json.Unmarshal(raw, &filteredGet); err != nil {
			t.Fatalf("unmarshal filtered get: %v", err)
		}
		if len(filteredGet.Data.Attributes.Files.Prepare) != 1 {
			t.Fatalf("expected 1 visible prepare item, got %#v", filteredGet.Data.Attributes.Files.Prepare)
		}
		if got := filteredGet.Data.Attributes.Files.Prepare[0].Attachment.FileName; got != "public.pdf" {
			t.Fatalf("expected public file, got %q", got)
		}

		resp, raw = doHelicopterFileRequest(t, filterApp, http.MethodGet, "/list", nil)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200 filtered list, got %d body=%s", resp.StatusCode, string(raw))
		}
		var filteredList struct {
			Data []struct {
				Attributes struct {
					Files struct {
						Prepare []responsedto.HelicopterFileCollectionItem `json:"prepare"`
					} `json:"files"`
				} `json:"attributes"`
			} `json:"data"`
		}
		if err := json.Unmarshal(raw, &filteredList); err != nil {
			t.Fatalf("unmarshal filtered list: %v", err)
		}
		if len(filteredList.Data) != 1 || len(filteredList.Data[0].Attributes.Files.Prepare) != 1 {
			t.Fatalf("expected only visible helicopter file in list, got %#v", filteredList.Data)
		}

		resp, _ = doHelicopterFileRequest(t, app, http.MethodDelete, "/delete", []byte(`{"data":{"type":"helicopter_file_delete","id":"bad"}}`))
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422 invalid delete id, got %d", resp.StatusCode)
		}

		resp, _ = doHelicopterFileRequest(t, app, http.MethodDelete, "/delete", []byte(`{"data":{"type":"helicopter_file_delete","id":"`+mustUUIDString(t, uuidv7.MustBytes())+`"}}`))
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200 delete success, got %d", resp.StatusCode)
		}
	})

	t.Run("get and delete error branches", func(t *testing.T) {
		helicopterID := uuidv7.MustBytes()
		svc := &helicopterFileServiceMock{
			helicopterExistsErr: errors.New("lookup failed"),
			deleteErr:           errors.New("delete failed"),
		}
		app := newHelicopterFileTestApp(NewHelicopterFileHandler(svc))

		resp, _ := doHelicopterFileRequest(t, app, http.MethodGet, "/get/"+mustUUIDString(t, helicopterID), nil)
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400 get error, got %d", resp.StatusCode)
		}

		resp, _ = doHelicopterFileRequest(t, app, http.MethodDelete, "/delete", []byte(`{"data":{"type":"helicopter_file_delete","id":"`+mustUUIDString(t, uuidv7.MustBytes())+`"}}`))
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400 delete error, got %d", resp.StatusCode)
		}
	})

	t.Run("delete invalid json and validation error", func(t *testing.T) {
		app := newHelicopterFileTestApp(NewHelicopterFileHandler(&helicopterFileServiceMock{}))
		resp, _ := doHelicopterFileRequest(t, app, http.MethodDelete, "/delete", []byte(`{"data":`))
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400 invalid json, got %d", resp.StatusCode)
		}

		resp, _ = doHelicopterFileRequest(t, app, http.MethodDelete, "/delete", []byte(`{"data":{"type":"","id":""}}`))
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422 validation error, got %d", resp.StatusCode)
		}
	})

	t.Run("get not found and list error", func(t *testing.T) {
		svc := &helicopterFileServiceMock{
			helicopterExists: false,
			listErr:          errors.New("list failed"),
		}
		app := newHelicopterFileTestApp(NewHelicopterFileHandler(svc))

		resp, _ := doHelicopterFileRequest(t, app, http.MethodGet, "/get/"+mustUUIDString(t, uuidv7.MustBytes()), nil)
		if resp.StatusCode != http.StatusNotFound {
			t.Fatalf("expected 404 helicopter not found, got %d", resp.StatusCode)
		}

		resp, body := doHelicopterFileRequest(t, app, http.MethodGet, "/list", nil)
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400 list error, got %d body=%s", resp.StatusCode, string(body))
		}
	})
}

func TestHelicopterFileHandler_validateStoredHelicopterFileContent(t *testing.T) {
	corruptStorage := &templateCopyStorageMock{
		getFn: func(_ context.Context, _ string) (io.ReadCloser, filemanager.ObjectMetadata, error) {
			// Not a valid office document (plain text bytes).
			return io.NopCloser(strings.NewReader("this is not a docx")), filemanager.ObjectMetadata{}, nil
		},
	}
	h := NewHelicopterFileHandler(&helicopterFileServiceMock{}).WithFileStorage(corruptStorage)

	// A corrupt office document is rejected.
	if err := h.validateStoredHelicopterFileContent(context.Background(), &filemanager.File{
		Name:      "broken.docx",
		ObjectKey: "files/broken.docx",
	}); err == nil {
		t.Fatalf("expected corrupt docx to be rejected")
	}

	// A non-office file is not inspected and passes through.
	if err := h.validateStoredHelicopterFileContent(context.Background(), &filemanager.File{
		Name:      "photo.pdf",
		ObjectKey: "files/photo.pdf",
	}); err != nil {
		t.Fatalf("expected non-office file to pass, got %v", err)
	}

	// A nil file is a no-op.
	if err := h.validateStoredHelicopterFileContent(context.Background(), nil); err != nil {
		t.Fatalf("expected nil file to pass, got %v", err)
	}
}

func TestHelicopterFileHandler_validateStoredHelicopterFileContent_XLSXUsesSingleRead(t *testing.T) {
	var getCalls int
	storage := &templateCopyStorageMock{
		getFn: func(_ context.Context, _ string) (io.ReadCloser, filemanager.ObjectMetadata, error) {
			getCalls++
			return io.NopCloser(bytes.NewReader(createValidXLSXPayload(t))), filemanager.ObjectMetadata{}, nil
		},
	}
	h := NewHelicopterFileHandler(&helicopterFileServiceMock{}).WithFileStorage(storage)

	if err := h.validateStoredHelicopterFileContent(context.Background(), &filemanager.File{
		Name:      "template.xlsx",
		ObjectKey: "files/template.xlsx",
	}); err != nil {
		t.Fatalf("expected xlsx to pass validation, got %v", err)
	}
	if getCalls != 1 {
		t.Fatalf("expected one storage read for xlsx, got %d", getCalls)
	}
}

func createValidXLSXPayload(t *testing.T) []byte {
	t.Helper()
	var buf bytes.Buffer
	zw := zip.NewWriter(&buf)
	entries := map[string]string{
		"[Content_Types].xml": `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
			`<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">` +
			`<Default Extension="xml" ContentType="application/xml"/>` +
			`<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>` +
			`<Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>` +
			`</Types>`,
		"xl/workbook.xml": `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
			`<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"` +
			` xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">` +
			`<sheets><sheet name="Sheet1" sheetId="1" r:id="rId1"/></sheets>` +
			`</workbook>`,
		"xl/_rels/workbook.xml.rels": `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
			`<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">` +
			`<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>` +
			`</Relationships>`,
		"xl/worksheets/sheet1.xml": `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
			`<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">` +
			`<sheetData></sheetData>` +
			`</worksheet>`,
	}
	for name, content := range entries {
		w, err := zw.Create(name)
		if err != nil {
			t.Fatalf("create %s: %v", name, err)
		}
		if _, err := w.Write([]byte(content)); err != nil {
			t.Fatalf("write %s: %v", name, err)
		}
	}
	if err := zw.Close(); err != nil {
		t.Fatalf("close xlsx: %v", err)
	}
	return buf.Bytes()
}

func TestHelicopterFileHandler_HidesTemplateDerivedFiles(t *testing.T) {
	helicopterID := uuidv7.MustBytes()
	rows := []helicopterfile.HelicopterFile{
		{ // a real template — must stay visible
			ID:           uuidv7.MustBytes(),
			HelicopterID: helicopterID,
			SourceFileID: uuidv7.MustBytes(),
			Section:      helicopterfile.SectionFlightPreparationAndWB,
			CreatedAt:    time.Now().UTC(),
			UpdatedAt:    time.Now().UTC(),
			SourceFile: &filemanager.File{
				ID:         uuidv7.MustBytes(),
				Name:       "template.pdf",
				ObjectKey:  "files/template.pdf",
				SizeBytes:  10,
				IsTemplate: true,
			},
		},
		{ // a document created from a template — must be hidden (kept in DB)
			ID:           uuidv7.MustBytes(),
			HelicopterID: helicopterID,
			SourceFileID: uuidv7.MustBytes(),
			Section:      helicopterfile.SectionFlightPreparationAndWB,
			CreatedAt:    time.Now().UTC(),
			UpdatedAt:    time.Now().UTC(),
			SourceFile: &filemanager.File{
				ID:           uuidv7.MustBytes(),
				Name:         "created-from-template.pdf",
				ObjectKey:    "files/created-from-template.pdf",
				SizeBytes:    11,
				TemplateUUID: uuidv7.MustBytes(),
			},
		},
	}
	svc := &helicopterFileServiceMock{
		helicopterExists: true,
		byHeliRows:       rows,
		listRows:         rows,
		listTotal:        1,
	}
	h := NewHelicopterFileHandler(svc).
		WithHelicopterService(&helicopterServiceMock{row: &helicopter.Helicopter{ID: helicopterID, Designation: "OE-XHZ"}}).
		WithFileStorage(&fileManagerDownloadURLStorageMock{presignedURL: "https://signed/file.pdf"})
	app := newHelicopterFileTestApp(h)

	resp, raw := doHelicopterFileRequest(t, app, http.MethodGet, "/get/"+mustUUIDString(t, helicopterID), nil)
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("expected 200, got %d body=%s", resp.StatusCode, string(raw))
	}
	var got struct {
		Data struct {
			Attributes struct {
				Files struct {
					Prepare []responsedto.HelicopterFileCollectionItem `json:"prepare"`
				} `json:"files"`
			} `json:"attributes"`
		} `json:"data"`
	}
	if err := json.Unmarshal(raw, &got); err != nil {
		t.Fatalf("unmarshal: %v", err)
	}
	if len(got.Data.Attributes.Files.Prepare) != 1 {
		t.Fatalf("expected only the template to be visible, got %#v", got.Data.Attributes.Files.Prepare)
	}
	if name := got.Data.Attributes.Files.Prepare[0].Attachment.FileName; name != "template.pdf" {
		t.Fatalf("expected template.pdf visible, got %q", name)
	}
}

func TestHelicopterFileHandler_ResourceHelpers(t *testing.T) {
	row := &helicopterfile.HelicopterFile{
		ID:               uuidv7.MustBytes(),
		HelicopterID:     uuidv7.MustBytes(),
		FileAttachmentID: uuidv7.MustBytes(),
		Section:          helicopterfile.SectionAfterLastFlightInspection,
		CreatedBy:        uuidv7.MustBytes(),
		UpdatedBy:        uuidv7.MustBytes(),
		CreatedAt:        time.Now().UTC(),
		UpdatedAt:        time.Now().UTC(),
		Helicopter:       &helicopter.Helicopter{Designation: "OE-XHZ"},
		FileAttachment: &filemanager.Attachment{
			File: &filemanager.File{
				ID:        uuidv7.MustBytes(),
				Name:      "report.pdf",
				ObjectKey: "files/report.pdf",
				SizeBytes: 100,
			},
		},
	}
	storage := &fileManagerDownloadURLStorageMock{presignedURL: "https://signed/report.pdf"}
	res := helicopterFileResource(context.Background(), storage, row)
	if res.Type != "helicopter_file" {
		t.Fatalf("unexpected resource type %q", res.Type)
	}
	attrs := helicopterFileItemAttributesMap(context.Background(), storage, row)
	if attrs["section"] != helicopterfile.SectionAfterLastFlightInspection {
		t.Fatalf("unexpected section in attributes: %#v", attrs)
	}
	item := helicopterFileCollectionItem(context.Background(), storage, row)
	if item.Attachment.DownloadURL != "https://signed/report.pdf" {
		t.Fatalf("expected presigned download url, got %#v", item.Attachment)
	}
}

func TestHelicopterFileHandler_BulkResponseHelpers(t *testing.T) {
	withHelicopterFileCtx(t, func(c *fiber.Ctx) {
		items := []responsedto.HelicopterFileUploadIntentBulkItem{
			{Index: 0, Success: true},
			{Index: 1, Success: false, Error: &responsedto.HelicopterFileBulkError{Status: http.StatusBadRequest, Title: "bad", Detail: "bad"}},
		}
		if err := writeHelicopterFileUploadIntentBulkResponse(c, items); err != nil {
			t.Fatalf("write upload bulk response: %v", err)
		}
	})

	withHelicopterFileCtx(t, func(c *fiber.Ctx) {
		items := []responsedto.HelicopterFileUploadIntentBulkItem{
			{Index: 0, Success: true},
		}
		if err := writeHelicopterFileUploadIntentBulkResponse(c, items); err != nil {
			t.Fatalf("write upload bulk all-success response: %v", err)
		}
	})

	withHelicopterFileCtx(t, func(c *fiber.Ctx) {
		items := []responsedto.HelicopterFileCreateBulkItem{
			{Index: 0, Success: false, Error: &responsedto.HelicopterFileBulkError{Status: http.StatusUnprocessableEntity, Title: "bad", Detail: "bad"}},
		}
		if err := writeHelicopterFileCreateBulkResponse(c, items); err != nil {
			t.Fatalf("write create bulk response: %v", err)
		}
	})

	withHelicopterFileCtx(t, func(c *fiber.Ctx) {
		items := []responsedto.HelicopterFileUploadIntentBulkItem{
			{Index: 0, Success: false},
		}
		if err := writeHelicopterFileUploadIntentBulkResponse(c, items); err != nil {
			t.Fatalf("write upload bulk default error response: %v", err)
		}
	})

	t.Run("write upload bulk all-failed uses item status", func(t *testing.T) {
		app := fiber.New()
		app.Get("/", func(c *fiber.Ctx) error {
			items := []responsedto.HelicopterFileUploadIntentBulkItem{
				{Index: 0, Success: false, Error: &responsedto.HelicopterFileBulkError{Status: http.StatusBadRequest, Title: "bad", Detail: "bad"}},
			}
			return writeHelicopterFileUploadIntentBulkResponse(c, items)
		})
		req, _ := http.NewRequest(http.MethodGet, "/", nil)
		resp, err := app.Test(req)
		if err != nil {
			t.Fatalf("app.Test: %v", err)
		}
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
		body, _ := io.ReadAll(resp.Body)
		if !strings.Contains(string(body), `"partial_success":false`) {
			t.Fatalf("expected partial_success false, got %s", string(body))
		}
	})
}
