package handlers

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"io"
	"net/http"
	"strings"
	"testing"
	"time"

	mysqldriver "github.com/go-sql-driver/mysql"
	"github.com/gofiber/fiber/v2"
	"gorm.io/gorm"

	complaint "wucher/internal/domain/complaint"
	filemanager "wucher/internal/domain/file_manager"
	"wucher/internal/domain/helicopter"
	"wucher/internal/service"
	sharedconst "wucher/internal/shared/const"
	"wucher/internal/shared/pkg/uuidv7"
	responsedto "wucher/internal/transport/http/dto/response"
)

func TestApplyHelicopterFleetStatus_GroundedSetsInactive(t *testing.T) {
	res := responsedto.HelicopterResource{}
	res.Attributes.IsActive = true
	row := &helicopter.Helicopter{IsActive: true, AirOnGround: false}

	// A non-empty groundReason simulates an open-complaint grounding (not persisted
	// in air_on_ground), and is reflected verbatim as the AOG reason.
	reason := "Grounded due to an open complaint — MEL A grace period expired"
	applyHelicopterFleetStatus(&res, row, false, reason, false, nil)

	if res.Attributes.Status != helicopter.StatusAOG {
		t.Fatalf("expected status AOG, got %q", res.Attributes.Status)
	}
	if !res.Attributes.AOG {
		t.Fatal("expected aog=true")
	}
	if res.Attributes.IsActive {
		t.Fatal("expected is_active=false for a grounded (AOG) helicopter")
	}
	if res.Attributes.AOGReason != reason {
		t.Fatalf("expected enriched AOG reason %q, got %q", reason, res.Attributes.AOGReason)
	}
}

func TestApplyHelicopterFleetStatus_AvailableStaysActive(t *testing.T) {
	res := responsedto.HelicopterResource{}
	res.Attributes.IsActive = true
	row := &helicopter.Helicopter{IsActive: true, AirOnGround: false}

	applyHelicopterFleetStatus(&res, row, false, "", false, nil)

	if res.Attributes.Status != helicopter.StatusAvailable {
		t.Fatalf("expected status available, got %q", res.Attributes.Status)
	}
	if !res.Attributes.IsActive {
		t.Fatal("expected is_active=true for an available helicopter")
	}
}

func TestComplaintGroundingReason_MELvsNonMEL(t *testing.T) {
	mel := complaint.Complaint{MELSeverity: complaint.MELSeverityA}
	if got := complaintGroundingReason(mel); !strings.Contains(got, "MEL A") || !strings.Contains(got, "grace period expired") {
		t.Fatalf("unexpected MEL reason: %q", got)
	}
	nonMEL := complaint.Complaint{MELSeverity: complaint.MELSeverityNonMEL}
	if got := complaintGroundingReason(nonMEL); !strings.Contains(got, "non-MEL") {
		t.Fatalf("unexpected non-MEL reason: %q", got)
	}
}

type memHelicopterRepo struct {
	rows map[string]*helicopter.Helicopter

	createErr error
	updateErr error
	deleteErr error
	getErr    error
	listErr   error
	nextErr   error
	existsErr error

	listRows  []helicopter.Helicopter
	listTotal int64
}

type helicopterPresignStorageMock struct {
	url string
	err error
}

type helicopterAttachmentRepoMock struct {
	getByIDFn func(id []byte) (*filemanager.Attachment, error)
}

type helicopterPINVerifierMock struct {
	consumeFn func(ctx context.Context, userID []byte, action, token string) error
}

type helicopterPINSessionVerifierMock struct {
	helicopterPINVerifierMock
	accessCookieName string
	parseSessionFn   func(ctx context.Context, accessToken string) (string, error)
	consumeSessionFn func(ctx context.Context, userID []byte, action, token, sessionID string) error
}

func intPtr(v int) *int {
	return &v
}

func (m *helicopterAttachmentRepoMock) Create(context.Context, *filemanager.Attachment) error {
	return nil
}

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

func (m *helicopterAttachmentRepoMock) GetByID(_ context.Context, id []byte) (*filemanager.Attachment, error) {
	if m.getByIDFn != nil {
		return m.getByIDFn(id)
	}
	return nil, nil
}

func (m *helicopterAttachmentRepoMock) GetByRefAndFile(context.Context, string, string, []byte) (*filemanager.Attachment, error) {
	return nil, nil
}

func (m *helicopterAttachmentRepoMock) ListByReference(context.Context, filemanager.AttachmentListFilter) ([]filemanager.Attachment, int64, error) {
	return nil, 0, nil
}

func (m *helicopterPINVerifierMock) ConsumeSecurityPINActionToken(ctx context.Context, userID []byte, action, token string) error {
	if m.consumeFn != nil {
		return m.consumeFn(ctx, userID, action, token)
	}
	return nil
}

func (m *helicopterPINSessionVerifierMock) AccessCookieName() string {
	if strings.TrimSpace(m.accessCookieName) == "" {
		return "access_token"
	}
	return m.accessCookieName
}

func (m *helicopterPINSessionVerifierMock) ParseAccessTokenSessionID(ctx context.Context, accessToken string) (string, error) {
	if m.parseSessionFn != nil {
		return m.parseSessionFn(ctx, accessToken)
	}
	return "", nil
}

func (m *helicopterPINSessionVerifierMock) ConsumeSecurityPINActionTokenInSession(ctx context.Context, userID []byte, action, token, sessionID string) error {
	if m.consumeSessionFn != nil {
		return m.consumeSessionFn(ctx, userID, action, token, sessionID)
	}
	return nil
}

func (m *helicopterPresignStorageMock) PresignGetObject(_ context.Context, _ string, _ time.Duration) (string, error) {
	if m.err != nil {
		return "", m.err
	}
	return m.url, nil
}

func newMemHelicopterRepo() *memHelicopterRepo {
	return &memHelicopterRepo{rows: map[string]*helicopter.Helicopter{}}
}

func (r *memHelicopterRepo) Create(_ context.Context, h *helicopter.Helicopter) error {
	if r.createErr != nil {
		return r.createErr
	}
	if len(h.ID) == 0 {
		h.ID = uuidv7.MustBytes()
	}
	now := time.Now().UTC()
	if h.CreatedAt.IsZero() {
		h.CreatedAt = now
	}
	h.UpdatedAt = now
	clone := *h
	r.rows[string(clone.ID)] = &clone
	return nil
}

func (r *memHelicopterRepo) Update(_ context.Context, h *helicopter.Helicopter) error {
	if r.updateErr != nil {
		return r.updateErr
	}
	if existing, ok := r.rows[string(h.ID)]; ok {
		_ = existing
	}
	h.UpdatedAt = time.Now().UTC()
	clone := *h
	r.rows[string(clone.ID)] = &clone
	return nil
}

func (r *memHelicopterRepo) Delete(_ context.Context, id []byte) error {
	if r.deleteErr != nil {
		return r.deleteErr
	}
	delete(r.rows, string(id))
	return nil
}

func (r *memHelicopterRepo) GetByID(_ context.Context, id []byte) (*helicopter.Helicopter, error) {
	if r.getErr != nil {
		return nil, r.getErr
	}
	h := r.rows[string(id)]
	if h == nil {
		return nil, nil
	}
	clone := *h
	return &clone, nil
}

func (r *memHelicopterRepo) List(_ context.Context, _ string, _ []string, _ string, _ int, _ int, _ [][]byte) ([]helicopter.Helicopter, int64, error) {
	if r.listErr != nil {
		return nil, 0, r.listErr
	}
	if r.listRows != nil {
		return r.listRows, r.listTotal, nil
	}
	out := make([]helicopter.Helicopter, 0, len(r.rows))
	for _, row := range r.rows {
		out = append(out, *row)
	}
	return out, int64(len(out)), nil
}

func (r *memHelicopterRepo) NextReportNumber(_ context.Context, id []byte) (string, int, error) {
	if r.nextErr != nil {
		return "", 0, r.nextErr
	}
	h := r.rows[string(id)]
	if h == nil {
		return "", 0, gorm.ErrRecordNotFound
	}
	h.ReportSequence++
	return fmt.Sprintf("%s-%04d", h.Identifier, h.ReportSequence), h.ReportSequence, nil
}

func (r *memHelicopterRepo) ExistsByIdentifier(_ context.Context, identifier string, excludeID []byte) (bool, error) {
	if r.existsErr != nil {
		return false, r.existsErr
	}
	target := strings.ToUpper(strings.TrimSpace(identifier))
	for _, row := range r.rows {
		if len(excludeID) > 0 && string(row.ID) == string(excludeID) {
			continue
		}
		if strings.ToUpper(strings.TrimSpace(row.Identifier)) == target {
			return true, nil
		}
	}
	return false, nil
}

func (r *memHelicopterRepo) IsInUse(_ context.Context, id []byte) (bool, error) {
	_ = id
	return false, nil
}

func (r *memHelicopterRepo) InUseByAircraftIDs(_ context.Context, ids [][]byte) (map[string]bool, error) {
	_ = ids
	return map[string]bool{}, nil
}
func (r *memHelicopterRepo) ActiveFlightIDByAircraftIDs(_ context.Context, _ [][]byte) (map[string][]byte, error) {
	return map[string][]byte{}, nil
}

func TestNewHelicopterHandler(t *testing.T) {
	h := NewHelicopterHandler(service.NewHelicopterService(newMemHelicopterRepo()))
	if h == nil || h.validate == nil {
		t.Fatalf("expected handler and validator initialized")
	}
}

func TestHelicopterHandler_Create(t *testing.T) {
	t.Run("success", func(t *testing.T) {
		repo := newMemHelicopterRepo()
		h := NewHelicopterHandler(service.NewHelicopterService(repo))
		app := fiber.New()
		actor := uuidv7.MustBytes()
		app.Use(func(c *fiber.Ctx) error {
			c.Locals("user_id", actor)
			return c.Next()
		})
		app.Post("/api/v1/helicopters/create", h.Create)

		payload := []byte(`{"data":{"type":"helicopter","attributes":{"designation":" OE-XHZ ","identifier":"1234","type":" EC-13 ","sortkey":3,"visible":true}}}`)
		req, _ := http.NewRequest(http.MethodPost, "/api/v1/helicopters/create", bytes.NewReader(payload))
		req.Header.Set("Content-Type", "application/json")
		resp, err := app.Test(req)
		if err != nil {
			t.Fatalf("app.Test: %v", err)
		}
		if resp.StatusCode != http.StatusCreated {
			t.Fatalf("expected 201, got %d", resp.StatusCode)
		}

		if len(repo.rows) != 1 {
			t.Fatalf("expected 1 created row, got %d", len(repo.rows))
		}
		for _, row := range repo.rows {
			if row.Designation != "OE-XHZ" {
				t.Fatalf("expected designation trimmed, got %q", row.Designation)
			}
			if row.Identifier != "1234" {
				t.Fatalf("expected identifier unchanged, got %q", row.Identifier)
			}
			if row.ReportSequence != 0 {
				t.Fatalf("expected report_sequence initialized to 0, got %d", row.ReportSequence)
			}
			if row.SortKey == nil || *row.SortKey != 3 {
				t.Fatalf("expected sortkey set, got %#v", row.SortKey)
			}
			if string(row.CreatedBy) != string(actor) {
				t.Fatalf("expected created_by from actor")
			}
		}
	})

	t.Run("invalid json", func(t *testing.T) {
		repo := newMemHelicopterRepo()
		h := NewHelicopterHandler(service.NewHelicopterService(repo))
		app := fiber.New()
		app.Post("/api/v1/helicopters/create", h.Create)

		req, _ := http.NewRequest(http.MethodPost, "/api/v1/helicopters/create", bytes.NewReader([]byte(`{"data":`)))
		req.Header.Set("Content-Type", "application/json")
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})

	t.Run("validation error", func(t *testing.T) {
		repo := newMemHelicopterRepo()
		h := NewHelicopterHandler(service.NewHelicopterService(repo))
		app := fiber.New()
		app.Post("/api/v1/helicopters/create", h.Create)

		req, _ := http.NewRequest(http.MethodPost, "/api/v1/helicopters/create", bytes.NewReader([]byte(`{"data":{"type":"","attributes":{}}}`)))
		req.Header.Set("Content-Type", "application/json")
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})

	t.Run("duplicate identifier (case-insensitive) is denied", func(t *testing.T) {
		repo := newMemHelicopterRepo()
		repo.rows[string(uuidv7.MustBytes())] = &helicopter.Helicopter{
			ID:             uuidv7.MustBytes(),
			Designation:    "OLD",
			Identifier:     "SN-0001",
			ReportSequence: 1,
			Type:           "EC-13",
			IsActive:       true,
		}
		h := NewHelicopterHandler(service.NewHelicopterService(repo))
		app := fiber.New()
		app.Post("/api/v1/helicopters/create", h.Create)

		payload := []byte(`{"data":{"type":"helicopter","attributes":{"designation":" OE-NEW ","identifier":"sn-0001","type":" EC-13 "}}}`)
		req, _ := http.NewRequest(http.MethodPost, "/api/v1/helicopters/create", bytes.NewReader(payload))
		req.Header.Set("Content-Type", "application/json")
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusConflict {
			t.Fatalf("expected 409, got %d", resp.StatusCode)
		}
		body, _ := io.ReadAll(resp.Body)
		if !strings.Contains(string(body), "Serial number already exists") {
			t.Fatalf("expected duplicate serial number message, got %s", string(body))
		}
		if !strings.Contains(string(body), "/data/attributes/identifier") {
			t.Fatalf("expected identifier pointer in error source, got %s", string(body))
		}
		if len(repo.rows) != 1 {
			t.Fatalf("expected no additional row on duplicate identifier")
		}
	})

	t.Run("missing required attributes", func(t *testing.T) {
		repo := newMemHelicopterRepo()
		h := NewHelicopterHandler(service.NewHelicopterService(repo))
		app := fiber.New()
		app.Post("/api/v1/helicopters/create", h.Create)

		payload := []byte(`{"data":{"type":"helicopter","attributes":{"designation":"","identifier":"","type":""}}}`)
		req, _ := http.NewRequest(http.MethodPost, "/api/v1/helicopters/create", bytes.NewReader(payload))
		req.Header.Set("Content-Type", "application/json")
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})

	t.Run("create error", func(t *testing.T) {
		repo := newMemHelicopterRepo()
		repo.createErr = errors.New("db error")
		h := NewHelicopterHandler(service.NewHelicopterService(repo))
		app := fiber.New()
		app.Post("/api/v1/helicopters/create", h.Create)

		payload := []byte(`{"data":{"type":"helicopter","attributes":{"designation":"OE-XHZ","identifier":"1234","type":"EC-13"}}}`)
		req, _ := http.NewRequest(http.MethodPost, "/api/v1/helicopters/create", bytes.NewReader(payload))
		req.Header.Set("Content-Type", "application/json")
		resp, _ := app.Test(req)
		body, _ := io.ReadAll(resp.Body)
		if resp.StatusCode != http.StatusInternalServerError {
			t.Fatalf("expected 500, got %d", resp.StatusCode)
		}
		if strings.Contains(string(body), "db error") {
			t.Fatalf("expected raw error to be hidden, got %s", string(body))
		}
		if !strings.Contains(string(body), "an internal error occurred") {
			t.Fatalf("expected generic internal message, got %s", string(body))
		}
	})

	t.Run("aog true requires reason", func(t *testing.T) {
		repo := newMemHelicopterRepo()
		h := NewHelicopterHandler(service.NewHelicopterService(repo))
		app := fiber.New()
		app.Post("/api/v1/helicopters/create", h.Create)

		payload := []byte(`{"data":{"type":"helicopter","attributes":{"designation":"OE-XHZ","identifier":"1234","type":"EC-13","aog":true}}}`)
		req, _ := http.NewRequest(http.MethodPost, "/api/v1/helicopters/create", bytes.NewReader(payload))
		req.Header.Set("Content-Type", "application/json")
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})
}

func TestHelicopterHandler_Update(t *testing.T) {
	repo := newMemHelicopterRepo()
	id := uuidv7.MustBytes()
	repo.rows[string(id)] = &helicopter.Helicopter{
		ID:             id,
		Designation:    "OE-XHZ",
		Identifier:     "1234",
		ReportSequence: 1,
		Type:           "EC-13",
		IsActive:       true,
	}

	h := NewHelicopterHandler(service.NewHelicopterService(repo))
	app := fiber.New()
	app.Patch("/api/v1/helicopters/update/:id", h.Update)

	t.Run("success", func(t *testing.T) {
		idStr, _ := uuidv7.BytesToString(id)
		payload := []byte(`{"data":{"type":"helicopter","id":"` + idStr + `","attributes":{"designation":"OE-NEW","identifier":"9876","sortkey":7}}}`)
		req, _ := http.NewRequest(http.MethodPatch, "/api/v1/helicopters/update/"+idStr, bytes.NewReader(payload))
		req.Header.Set("Content-Type", "application/json")
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d", resp.StatusCode)
		}
		updated := repo.rows[string(id)]
		if updated.Designation != "OE-NEW" {
			t.Fatalf("expected designation updated, got %q", updated.Designation)
		}
		if updated.Identifier != "9876" {
			t.Fatalf("expected identifier unchanged, got %q", updated.Identifier)
		}
		if updated.ReportSequence != 1 {
			t.Fatalf("expected report_sequence unchanged, got %d", updated.ReportSequence)
		}
		if updated.SortKey == nil || *updated.SortKey != 7 {
			t.Fatalf("expected sortkey updated, got %#v", updated.SortKey)
		}
	})

	t.Run("empty file_uuid clears existing attachment", func(t *testing.T) {
		idStr, _ := uuidv7.BytesToString(id)
		fotoID := uuidv7.MustBytes()
		repo.rows[string(id)].FotoAttachmentID = append([]byte(nil), fotoID...)

		payload := []byte(`{"data":{"type":"helicopter","id":"` + idStr + `","attributes":{"designation":"OE-KEEP","file_uuid":""}}}`)
		req, _ := http.NewRequest(http.MethodPatch, "/api/v1/helicopters/update/"+idStr, bytes.NewReader(payload))
		req.Header.Set("Content-Type", "application/json")
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d", resp.StatusCode)
		}
		if repo.rows[string(id)].FotoAttachmentID != nil {
			t.Fatalf("expected foto attachment to be cleared")
		}
	})

	t.Run("existing attachment uuid is accepted on update", func(t *testing.T) {
		idStr, _ := uuidv7.BytesToString(id)
		attachmentID := uuidv7.MustBytes()
		attachmentIDStr, _ := uuidv7.BytesToString(attachmentID)
		fileSvc := service.NewFileManagerService(nil, nil, &helicopterAttachmentRepoMock{
			getByIDFn: func(gotID []byte) (*filemanager.Attachment, error) {
				if string(gotID) != string(attachmentID) {
					return nil, nil
				}
				return &filemanager.Attachment{ID: append([]byte(nil), attachmentID...)}, nil
			},
		}, service.FileManagerServiceDependencies{})
		hWithFileManager := NewHelicopterHandler(service.NewHelicopterService(repo)).WithFileManagerService(fileSvc)
		appWithFileManager := fiber.New()
		appWithFileManager.Patch("/api/v1/helicopters/update/:id", hWithFileManager.Update)

		payload := []byte(`{"data":{"type":"helicopter","id":"` + idStr + `","attributes":{"designation":"OE-ATTACH","file_uuid":"` + attachmentIDStr + `"}}}`)
		req, _ := http.NewRequest(http.MethodPatch, "/api/v1/helicopters/update/"+idStr, bytes.NewReader(payload))
		req.Header.Set("Content-Type", "application/json")
		resp, _ := appWithFileManager.Test(req)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d", resp.StatusCode)
		}
		if string(repo.rows[string(id)].FotoAttachmentID) != string(attachmentID) {
			t.Fatalf("expected existing attachment uuid to be accepted")
		}
	})

	t.Run("invalid path uuid", func(t *testing.T) {
		req, _ := http.NewRequest(http.MethodPatch, "/api/v1/helicopters/update/bad-uuid", bytes.NewReader([]byte(`{}`)))
		req.Header.Set("Content-Type", "application/json")
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})

	t.Run("invalid json", func(t *testing.T) {
		idStr, _ := uuidv7.BytesToString(id)
		req, _ := http.NewRequest(http.MethodPatch, "/api/v1/helicopters/update/"+idStr, bytes.NewReader([]byte(`{"data":`)))
		req.Header.Set("Content-Type", "application/json")
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})

	t.Run("id required", func(t *testing.T) {
		idStr, _ := uuidv7.BytesToString(id)
		payload := []byte(`{"data":{"type":"helicopter","id":"","attributes":{"designation":"X"}}}`)
		req, _ := http.NewRequest(http.MethodPatch, "/api/v1/helicopters/update/"+idStr, bytes.NewReader(payload))
		req.Header.Set("Content-Type", "application/json")
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})

	t.Run("id mismatch", func(t *testing.T) {
		idStr, _ := uuidv7.BytesToString(id)
		other, _ := uuidv7.BytesToString(uuidv7.MustBytes())
		payload := []byte(`{"data":{"type":"helicopter","id":"` + other + `","attributes":{"designation":"X"}}}`)
		req, _ := http.NewRequest(http.MethodPatch, "/api/v1/helicopters/update/"+idStr, bytes.NewReader(payload))
		req.Header.Set("Content-Type", "application/json")
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})

	t.Run("no attrs", func(t *testing.T) {
		idStr, _ := uuidv7.BytesToString(id)
		payload := []byte(`{"data":{"type":"helicopter","id":"` + idStr + `","attributes":{}}}`)
		req, _ := http.NewRequest(http.MethodPatch, "/api/v1/helicopters/update/"+idStr, bytes.NewReader(payload))
		req.Header.Set("Content-Type", "application/json")
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})

	t.Run("not found", func(t *testing.T) {
		missing := uuidv7.MustBytes()
		missingStr, _ := uuidv7.BytesToString(missing)
		payload := []byte(`{"data":{"type":"helicopter","id":"` + missingStr + `","attributes":{"designation":"X"}}}`)
		req, _ := http.NewRequest(http.MethodPatch, "/api/v1/helicopters/update/"+missingStr, bytes.NewReader(payload))
		req.Header.Set("Content-Type", "application/json")
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusNotFound {
			t.Fatalf("expected 404, got %d", resp.StatusCode)
		}
	})

	t.Run("invalid designation", func(t *testing.T) {
		idStr, _ := uuidv7.BytesToString(id)
		payload := []byte(`{"data":{"type":"helicopter","id":"` + idStr + `","attributes":{"designation":" "}}}`)
		req, _ := http.NewRequest(http.MethodPatch, "/api/v1/helicopters/update/"+idStr, bytes.NewReader(payload))
		req.Header.Set("Content-Type", "application/json")
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})

	t.Run("invalid identifier", func(t *testing.T) {
		idStr, _ := uuidv7.BytesToString(id)
		payload := []byte(`{"data":{"type":"helicopter","id":"` + idStr + `","attributes":{"identifier":"   "}}}`)
		req, _ := http.NewRequest(http.MethodPatch, "/api/v1/helicopters/update/"+idStr, bytes.NewReader(payload))
		req.Header.Set("Content-Type", "application/json")
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})

	t.Run("invalid type", func(t *testing.T) {
		idStr, _ := uuidv7.BytesToString(id)
		payload := []byte(`{"data":{"type":"helicopter","id":"` + idStr + `","attributes":{"type":"   "}}}`)
		req, _ := http.NewRequest(http.MethodPatch, "/api/v1/helicopters/update/"+idStr, bytes.NewReader(payload))
		req.Header.Set("Content-Type", "application/json")
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})

	t.Run("success update all fields", func(t *testing.T) {
		idStr, _ := uuidv7.BytesToString(id)
		payload := []byte(`{"data":{"type":"helicopter","id":"` + idStr + `","attributes":{"designation":"OE-ALL","identifier":"4321","type":"EC-145","engine_1":"TM1","engine_2":"TM2","consumption_lt_min":8.8,"nr1":true,"nr2":false,"lh":true,"rh":false,"mgb":true,"igb":false,"tgb":true,"utc_offset":2,"dry":true,"note":"all fields updated","is_active":false}}}`)
		req, _ := http.NewRequest(http.MethodPatch, "/api/v1/helicopters/update/"+idStr, bytes.NewReader(payload))
		req.Header.Set("Content-Type", "application/json")
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d", resp.StatusCode)
		}

		updated := repo.rows[string(id)]
		if updated.Type != "EC-145" || updated.Engine1 != "TM1" || updated.Engine2 != "TM2" {
			t.Fatalf("expected scalar fields updated, got %+v", *updated)
		}
		if !updated.Dry || updated.IsActive {
			t.Fatalf("expected boolean fields updated, got %+v", *updated)
		}
		if updated.Note != "all fields updated" || updated.Identifier != "4321" {
			t.Fatalf("expected note/identifier updated, got %+v", *updated)
		}
	})

	t.Run("update error", func(t *testing.T) {
		idStr, _ := uuidv7.BytesToString(id)
		repo.updateErr = errors.New("update failed")
		payload := []byte(`{"data":{"type":"helicopter","id":"` + idStr + `","attributes":{"designation":"OE-ZZZ"}}}`)
		req, _ := http.NewRequest(http.MethodPatch, "/api/v1/helicopters/update/"+idStr, bytes.NewReader(payload))
		req.Header.Set("Content-Type", "application/json")
		resp, _ := app.Test(req)
		repo.updateErr = nil
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})

	t.Run("aog true requires reason", func(t *testing.T) {
		idNoReason := uuidv7.MustBytes()
		repo.rows[string(idNoReason)] = &helicopter.Helicopter{
			ID:          idNoReason,
			Designation: "OE-NOREASON",
			Identifier:  "2000-0001",
			Type:        "EC-13",
			AirOnGround: false,
			AOGReason:   "",
			IsActive:    true,
		}
		idStr, _ := uuidv7.BytesToString(idNoReason)
		payload := []byte(`{"data":{"type":"helicopter","id":"` + idStr + `","attributes":{"aog":true}}}`)
		req, _ := http.NewRequest(http.MethodPatch, "/api/v1/helicopters/update/"+idStr, bytes.NewReader(payload))
		req.Header.Set("Content-Type", "application/json")
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})
}

func TestHelicopterHandler_AOGPINVerification(t *testing.T) {
	t.Run("create aog false does not require pin", func(t *testing.T) {
		repo := newMemHelicopterRepo()
		actor := uuidv7.MustBytes()
		h := NewHelicopterHandler(service.NewHelicopterService(repo)).WithPINVerifier(&helicopterPINVerifierMock{
			consumeFn: func(_ context.Context, _ []byte, _ string, _ string) error {
				t.Fatalf("expected pin verifier not to be called when aog is false")
				return nil
			},
		})
		app := fiber.New()
		app.Use(func(c *fiber.Ctx) error {
			c.Locals("user_id", actor)
			return c.Next()
		})
		app.Post("/api/v1/helicopters/create", h.Create)

		payload := []byte(`{"data":{"type":"helicopter","attributes":{"designation":"OE-NO-PIN","identifier":"7778","type":"EC-145","aog":false}}}`)
		req, _ := http.NewRequest(http.MethodPost, "/api/v1/helicopters/create", bytes.NewReader(payload))
		req.Header.Set("Content-Type", "application/json")
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusCreated {
			t.Fatalf("expected 201, got %d", resp.StatusCode)
		}
		if len(repo.rows) != 1 {
			t.Fatalf("expected helicopter created without pin when aog is false")
		}
	})

	t.Run("create aog true requires pin", func(t *testing.T) {
		repo := newMemHelicopterRepo()
		actor := uuidv7.MustBytes()
		h := NewHelicopterHandler(service.NewHelicopterService(repo)).WithPINVerifier(&helicopterPINVerifierMock{
			consumeFn: func(_ context.Context, userID []byte, action, token string) error {
				if string(userID) != string(actor) {
					t.Fatalf("unexpected actor passed to pin verifier")
				}
				if action != sharedconst.PinActionHelicopterAOGChange {
					t.Fatalf("unexpected pin action %q", action)
				}
				if token == "" {
					return errors.New("missing token")
				}
				return nil
			},
		})
		app := fiber.New()
		app.Use(func(c *fiber.Ctx) error {
			c.Locals("user_id", actor)
			return c.Next()
		})
		app.Post("/api/v1/helicopters/create", h.Create)

		payload := []byte(`{"data":{"type":"helicopter","attributes":{"designation":"OE-AOG","identifier":"7777","type":"EC-145","aog":true,"aog_reason":"grounded"}}}`)
		req, _ := http.NewRequest(http.MethodPost, "/api/v1/helicopters/create", bytes.NewReader(payload))
		req.Header.Set("Content-Type", "application/json")
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusAccepted {
			t.Fatalf("expected 202, got %d", resp.StatusCode)
		}
		if len(repo.rows) != 0 {
			t.Fatalf("expected no helicopter created without pin verification")
		}
	})

	t.Run("update aog change requires pin", func(t *testing.T) {
		repo := newMemHelicopterRepo()
		actor := uuidv7.MustBytes()
		id := uuidv7.MustBytes()
		repo.rows[string(id)] = &helicopter.Helicopter{
			ID:          id,
			Designation: "OE-XHZ",
			Identifier:  "1234-0001",
			Type:        "EC-13",
			AirOnGround: false,
			IsActive:    true,
		}
		h := NewHelicopterHandler(service.NewHelicopterService(repo)).WithPINVerifier(&helicopterPINVerifierMock{
			consumeFn: func(_ context.Context, userID []byte, action, token string) error {
				if string(userID) != string(actor) {
					t.Fatalf("unexpected actor passed to pin verifier")
				}
				if action != sharedconst.PinActionHelicopterAOGChange {
					t.Fatalf("unexpected pin action %q", action)
				}
				if token == "" {
					return errors.New("missing token")
				}
				return nil
			},
		})
		app := fiber.New()
		app.Use(func(c *fiber.Ctx) error {
			c.Locals("user_id", actor)
			return c.Next()
		})
		app.Patch("/api/v1/helicopters/update/:id", h.Update)

		idStr, _ := uuidv7.BytesToString(id)
		payload := []byte(`{"data":{"type":"helicopter","id":"` + idStr + `","attributes":{"aog":true,"aog_reason":"grounded"}}}`)
		req, _ := http.NewRequest(http.MethodPatch, "/api/v1/helicopters/update/"+idStr, bytes.NewReader(payload))
		req.Header.Set("Content-Type", "application/json")
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusAccepted {
			t.Fatalf("expected 202, got %d", resp.StatusCode)
		}
		if repo.rows[string(id)].AirOnGround {
			t.Fatalf("expected aog state not to change without pin verification")
		}
	})

	t.Run("update without touching aog does not require pin", func(t *testing.T) {
		repo := newMemHelicopterRepo()
		id := uuidv7.MustBytes()
		repo.rows[string(id)] = &helicopter.Helicopter{
			ID:          id,
			Designation: "OE-XHZ",
			Identifier:  "1234-0001",
			Type:        "EC-13",
			AirOnGround: false,
			IsActive:    true,
		}
		pinVerifierCalled := false
		h := NewHelicopterHandler(service.NewHelicopterService(repo)).WithPINVerifier(&helicopterPINVerifierMock{
			consumeFn: func(_ context.Context, _ []byte, _ string, _ string) error {
				pinVerifierCalled = true
				return nil
			},
		})
		app := fiber.New()
		app.Patch("/api/v1/helicopters/update/:id", h.Update)

		idStr, _ := uuidv7.BytesToString(id)
		payload := []byte(`{"data":{"type":"helicopter","id":"` + idStr + `","attributes":{"designation":"OE-UPDATED"}}}`)
		req, _ := http.NewRequest(http.MethodPatch, "/api/v1/helicopters/update/"+idStr, bytes.NewReader(payload))
		req.Header.Set("Content-Type", "application/json")
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d", resp.StatusCode)
		}
		if pinVerifierCalled {
			t.Fatalf("expected pin verifier not to be called when aog is untouched")
		}
	})

	t.Run("update aog uses session-bound token when verifier supports it", func(t *testing.T) {
		repo := newMemHelicopterRepo()
		actor := uuidv7.MustBytes()
		id := uuidv7.MustBytes()
		repo.rows[string(id)] = &helicopter.Helicopter{
			ID:          id,
			Designation: "OE-XHZ",
			Identifier:  "1234-0001",
			Type:        "EC-13",
			AirOnGround: false,
			IsActive:    true,
		}
		h := NewHelicopterHandler(service.NewHelicopterService(repo)).WithPINVerifier(&helicopterPINSessionVerifierMock{
			accessCookieName: "access_token",
			parseSessionFn: func(_ context.Context, accessToken string) (string, error) {
				if accessToken != "access.jwt.token" {
					t.Fatalf("unexpected access token %q", accessToken)
				}
				return "sid-123", nil
			},
			consumeSessionFn: func(_ context.Context, userID []byte, action, token, sessionID string) error {
				if string(userID) != string(actor) {
					t.Fatalf("unexpected actor passed to pin verifier")
				}
				if action != sharedconst.PinActionHelicopterAOGChange {
					t.Fatalf("unexpected pin action %q", action)
				}
				if token != "pin-token" {
					t.Fatalf("unexpected pin token %q", token)
				}
				if sessionID != "sid-123" {
					t.Fatalf("unexpected session id %q", sessionID)
				}
				return nil
			},
		})
		app := fiber.New()
		app.Use(func(c *fiber.Ctx) error {
			c.Locals("user_id", actor)
			return c.Next()
		})
		app.Patch("/api/v1/helicopters/update/:id", h.Update)

		idStr, _ := uuidv7.BytesToString(id)
		payload := []byte(`{"data":{"type":"helicopter","id":"` + idStr + `","attributes":{"aog":true,"aog_reason":"grounded"}}}`)
		req, _ := http.NewRequest(http.MethodPatch, "/api/v1/helicopters/update/"+idStr, bytes.NewReader(payload))
		req.Header.Set("Content-Type", "application/json")
		req.Header.Set(pinVerificationHeader, "pin-token")
		req.AddCookie(&http.Cookie{Name: "access_token", Value: "access.jwt.token"})
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d", resp.StatusCode)
		}
		if !repo.rows[string(id)].AirOnGround {
			t.Fatalf("expected aog state to change after valid session-bound pin verification")
		}
	})
}

func TestHelicopterHandler_GetWithFoto(t *testing.T) {
	repo := newMemHelicopterRepo()
	id := uuidv7.MustBytes()
	fotoID := uuidv7.MustBytes()
	repo.rows[string(id)] = &helicopter.Helicopter{
		ID:               id,
		Designation:      "OE-XHZ",
		Identifier:       "1234-0001",
		Type:             "EC-13",
		FotoAttachmentID: fotoID,
		FotoAttachment: &filemanager.Attachment{
			ID:     fotoID,
			FileID: uuidv7.MustBytes(),
			File:   &filemanager.File{ID: uuidv7.MustBytes(), ObjectKey: "helicopter/oe-xhz.jpg"},
		},
		CreatedAt: time.Now().UTC(),
		UpdatedAt: time.Now().UTC(),
	}

	h := NewHelicopterHandler(service.NewHelicopterService(repo)).WithFileStorage(&helicopterPresignStorageMock{
		url: "https://signed.example/helicopter/oe-xhz.jpg?sig=abc",
	})
	app := fiber.New()
	app.Get("/api/v1/helicopters/get/:id", h.Get)

	idStr, _ := uuidv7.BytesToString(id)
	req, _ := http.NewRequest(http.MethodGet, "/api/v1/helicopters/get/"+idStr, nil)
	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("app.Test: %v", err)
	}
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("expected 200, got %d", resp.StatusCode)
	}
	body, _ := io.ReadAll(resp.Body)
	fotoIDStr, _ := uuidv7.BytesToString(fotoID)
	bodyStr := string(body)
	if !strings.Contains(bodyStr, `"uuid":"`+fotoIDStr+`"`) {
		t.Fatalf("expected foto uuid in response, body=%s", bodyStr)
	}
	if !strings.Contains(bodyStr, `"download_url":"https://signed.example/helicopter/oe-xhz.jpg?sig=abc"`) {
		t.Fatalf("expected foto download_url in response, body=%s", bodyStr)
	}
	if !strings.Contains(bodyStr, `"original_size_bytes":0`) {
		t.Fatalf("expected foto original_size_bytes in response, body=%s", bodyStr)
	}
}

func TestHelicopterHandler_Delete(t *testing.T) {
	repo := newMemHelicopterRepo()
	id := uuidv7.MustBytes()
	repo.rows[string(id)] = &helicopter.Helicopter{ID: id, Identifier: "1234"}

	h := NewHelicopterHandler(service.NewHelicopterService(repo))
	app := fiber.New()
	app.Delete("/api/v1/helicopters/delete/:id", h.Delete)

	t.Run("success", func(t *testing.T) {
		idStr, _ := uuidv7.BytesToString(id)
		req, _ := http.NewRequest(http.MethodDelete, "/api/v1/helicopters/delete/"+idStr, nil)
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d", resp.StatusCode)
		}
	})

	t.Run("invalid uuid", func(t *testing.T) {
		req, _ := http.NewRequest(http.MethodDelete, "/api/v1/helicopters/delete/bad", nil)
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})

	t.Run("not found", func(t *testing.T) {
		repo := newMemHelicopterRepo()
		h := NewHelicopterHandler(service.NewHelicopterService(repo))
		app := fiber.New()
		app.Delete("/api/v1/helicopters/delete/:id", h.Delete)

		id := uuidv7.MustBytes()
		idStr, _ := uuidv7.BytesToString(id)
		req, _ := http.NewRequest(http.MethodDelete, "/api/v1/helicopters/delete/"+idStr, nil)
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusNotFound {
			t.Fatalf("expected 404, got %d", resp.StatusCode)
		}
	})

	t.Run("delete error", func(t *testing.T) {
		repo.deleteErr = errors.New("delete failed")
		id2 := uuidv7.MustBytes()
		repo.rows[string(id2)] = &helicopter.Helicopter{ID: id2, Identifier: "ERR-DEL"}
		id2Str, _ := uuidv7.BytesToString(id2)
		req, _ := http.NewRequest(http.MethodDelete, "/api/v1/helicopters/delete/"+id2Str, nil)
		resp, _ := app.Test(req)
		repo.deleteErr = nil
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})

	t.Run("delete relation conflict", func(t *testing.T) {
		repo.deleteErr = &mysqldriver.MySQLError{
			Number:  1451,
			Message: "Cannot delete or update a parent row: a foreign key constraint fails",
		}
		id2 := uuidv7.MustBytes()
		repo.rows[string(id2)] = &helicopter.Helicopter{ID: id2, Identifier: "REL-DEL"}
		id2Str, _ := uuidv7.BytesToString(id2)
		req, _ := http.NewRequest(http.MethodDelete, "/api/v1/helicopters/delete/"+id2Str, nil)
		resp, _ := app.Test(req)
		repo.deleteErr = nil
		if resp.StatusCode != http.StatusConflict {
			t.Fatalf("expected 409, got %d", resp.StatusCode)
		}
		body, _ := io.ReadAll(resp.Body)
		bodyStr := string(body)
		if !strings.Contains(bodyStr, `"code":"HELICOPTER_RELATION_DELETE_ERROR"`) {
			t.Fatalf("expected HELICOPTER_RELATION_DELETE_ERROR, got %s", bodyStr)
		}
		if !strings.Contains(bodyStr, `"error_code":"4090303"`) {
			t.Fatalf("expected 4090303, got %s", bodyStr)
		}
	})

	t.Run("identifier lookup error", func(t *testing.T) {
		repo := newMemHelicopterRepo()
		repo.existsErr = errors.New("lookup failed")
		h := NewHelicopterHandler(service.NewHelicopterService(repo))
		app := fiber.New()
		app.Post("/api/v1/helicopters/create", h.Create)

		payload := []byte(`{"data":{"type":"helicopter","attributes":{"designation":"OE-ERR","identifier":"SN-ERR","type":"EC-13"}}}`)
		req, _ := http.NewRequest(http.MethodPost, "/api/v1/helicopters/create", bytes.NewReader(payload))
		req.Header.Set("Content-Type", "application/json")
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})

	t.Run("aog requires reason", func(t *testing.T) {
		repo := newMemHelicopterRepo()
		h := NewHelicopterHandler(service.NewHelicopterService(repo))
		app := fiber.New()
		app.Post("/api/v1/helicopters/create", h.Create)

		payload := []byte(`{"data":{"type":"helicopter","attributes":{"designation":"OE-XHZ","identifier":"SN-1","type":"EC-13","aog":true,"aog_reason":""}}}`)
		req, _ := http.NewRequest(http.MethodPost, "/api/v1/helicopters/create", bytes.NewReader(payload))
		req.Header.Set("Content-Type", "application/json")
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})

	t.Run("aog requires pin token", func(t *testing.T) {
		repo := newMemHelicopterRepo()
		h := NewHelicopterHandler(service.NewHelicopterService(repo))
		app := fiber.New()
		actor := uuidv7.MustBytes()
		app.Use(func(c *fiber.Ctx) error {
			c.Locals("user_id", actor)
			return c.Next()
		})
		app.Post("/api/v1/helicopters/create", h.Create)

		payload := []byte(`{"data":{"type":"helicopter","attributes":{"designation":"OE-XHZ","identifier":"SN-2","type":"EC-13","aog":true,"aog_reason":"maintenance"}}}`)
		req, _ := http.NewRequest(http.MethodPost, "/api/v1/helicopters/create", bytes.NewReader(payload))
		req.Header.Set("Content-Type", "application/json")
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusUnauthorized {
			t.Fatalf("expected 401, got %d", resp.StatusCode)
		}
	})

	t.Run("aog pin verifier rejects token", func(t *testing.T) {
		repo := newMemHelicopterRepo()
		pin := &helicopterPINVerifierMock{
			consumeFn: func(_ context.Context, _ []byte, _ string, _ string) error {
				return errors.New("pin required")
			},
		}
		h := NewHelicopterHandler(service.NewHelicopterService(repo)).WithPINVerifier(pin)
		app := fiber.New()
		actor := uuidv7.MustBytes()
		app.Use(func(c *fiber.Ctx) error {
			c.Locals("user_id", actor)
			return c.Next()
		})
		app.Post("/api/v1/helicopters/create", h.Create)

		payload := []byte(`{"data":{"type":"helicopter","attributes":{"designation":"OE-XHZ","identifier":"SN-3","type":"EC-13","aog":true,"aog_reason":"maintenance"}}}`)
		req, _ := http.NewRequest(http.MethodPost, "/api/v1/helicopters/create", bytes.NewReader(payload))
		req.Header.Set("Content-Type", "application/json")
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusAccepted {
			t.Fatalf("expected 202, got %d", resp.StatusCode)
		}
	})

	t.Run("aog pin verifier accepted continues create", func(t *testing.T) {
		repo := newMemHelicopterRepo()
		pin := &helicopterPINVerifierMock{
			consumeFn: func(_ context.Context, _ []byte, _ string, _ string) error {
				return nil
			},
		}
		h := NewHelicopterHandler(service.NewHelicopterService(repo)).WithPINVerifier(pin)
		app := fiber.New()
		actor := uuidv7.MustBytes()
		app.Use(func(c *fiber.Ctx) error {
			c.Locals("user_id", actor)
			return c.Next()
		})
		app.Post("/api/v1/helicopters/create", h.Create)

		payload := []byte(`{"data":{"type":"helicopter","attributes":{"designation":"OE-XHZ","identifier":"SN-4","type":"EC-13","aog":true,"aog_reason":"maintenance"}}}`)
		req, _ := http.NewRequest(http.MethodPost, "/api/v1/helicopters/create", bytes.NewReader(payload))
		req.Header.Set("Content-Type", "application/json")
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusCreated {
			t.Fatalf("expected 201, got %d", resp.StatusCode)
		}
	})

	t.Run("file_uuid invalid returns validation error", func(t *testing.T) {
		repo := newMemHelicopterRepo()
		h := NewHelicopterHandler(service.NewHelicopterService(repo))
		app := fiber.New()
		app.Post("/api/v1/helicopters/create", h.Create)

		payload := []byte(`{"data":{"type":"helicopter","attributes":{"designation":"OE-XHZ","identifier":"SN-5","type":"EC-13","file_uuid":"bad-uuid"}}}`)
		req, _ := http.NewRequest(http.MethodPost, "/api/v1/helicopters/create", bytes.NewReader(payload))
		req.Header.Set("Content-Type", "application/json")
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusBadRequest && resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 400/422, got %d", resp.StatusCode)
		}
	})
}

func TestHelicopterHandler_NextReportNumber(t *testing.T) {
	repo := newMemHelicopterRepo()
	id := uuidv7.MustBytes()
	repo.rows[string(id)] = &helicopter.Helicopter{ID: id, Identifier: "1234"}

	h := NewHelicopterHandler(service.NewHelicopterService(repo))
	app := fiber.New()
	app.Post("/api/v1/helicopters/:id/reports/next-number", h.NextReportNumber)

	t.Run("success", func(t *testing.T) {
		idStr, _ := uuidv7.BytesToString(id)
		req, _ := http.NewRequest(http.MethodPost, "/api/v1/helicopters/"+idStr+"/reports/next-number", nil)
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d", resp.StatusCode)
		}
		body, _ := io.ReadAll(resp.Body)
		if !strings.Contains(string(body), "1234-0001") {
			t.Fatalf("expected report number in response body, got %s", string(body))
		}
	})

	t.Run("invalid uuid", func(t *testing.T) {
		req, _ := http.NewRequest(http.MethodPost, "/api/v1/helicopters/bad/reports/next-number", nil)
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})

	t.Run("not found from generator", func(t *testing.T) {
		missing := uuidv7.MustBytes()
		missingStr, _ := uuidv7.BytesToString(missing)
		req, _ := http.NewRequest(http.MethodPost, "/api/v1/helicopters/"+missingStr+"/reports/next-number", nil)
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusNotFound {
			t.Fatalf("expected 404, got %d", resp.StatusCode)
		}
	})

	t.Run("generate failed", func(t *testing.T) {
		repo.nextErr = errors.New("boom")
		idStr, _ := uuidv7.BytesToString(id)
		req, _ := http.NewRequest(http.MethodPost, "/api/v1/helicopters/"+idStr+"/reports/next-number", nil)
		resp, _ := app.Test(req)
		repo.nextErr = nil
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})

	t.Run("not found on post-check", func(t *testing.T) {
		repo.nextErr = nil
		repo.getErr = errors.New("gone")
		idStr, _ := uuidv7.BytesToString(id)
		req, _ := http.NewRequest(http.MethodPost, "/api/v1/helicopters/"+idStr+"/reports/next-number", nil)
		resp, _ := app.Test(req)
		repo.getErr = nil
		if resp.StatusCode != http.StatusNotFound {
			t.Fatalf("expected 404, got %d", resp.StatusCode)
		}
	})
}

func TestHelicopterHandler_Get(t *testing.T) {
	repo := newMemHelicopterRepo()
	id := uuidv7.MustBytes()
	repo.rows[string(id)] = &helicopter.Helicopter{ID: id, Designation: "OE-XHZ", Identifier: "1234", Type: "EC-13"}

	h := NewHelicopterHandler(service.NewHelicopterService(repo))
	app := fiber.New()
	app.Get("/api/v1/helicopters/get/:id", h.Get)

	t.Run("success", func(t *testing.T) {
		idStr, _ := uuidv7.BytesToString(id)
		req, _ := http.NewRequest(http.MethodGet, "/api/v1/helicopters/get/"+idStr, nil)
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d", resp.StatusCode)
		}
	})

	t.Run("invalid uuid", func(t *testing.T) {
		req, _ := http.NewRequest(http.MethodGet, "/api/v1/helicopters/get/bad", nil)
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})

	t.Run("not found", func(t *testing.T) {
		missing := uuidv7.MustBytes()
		missingStr, _ := uuidv7.BytesToString(missing)
		req, _ := http.NewRequest(http.MethodGet, "/api/v1/helicopters/get/"+missingStr, nil)
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusNotFound {
			t.Fatalf("expected 404, got %d", resp.StatusCode)
		}
	})
}

func TestHelicopterHandler_List(t *testing.T) {
	repo := newMemHelicopterRepo()
	repo.listRows = []helicopter.Helicopter{{ID: uuidv7.MustBytes(), Designation: "OE-XHZ", Identifier: "1234", Type: "EC-13", CreatedAt: time.Now(), UpdatedAt: time.Now()}}
	repo.listTotal = 1

	h := NewHelicopterHandler(service.NewHelicopterService(repo))
	app := fiber.New()
	app.Get("/api/v1/helicopters/get-all", h.List)

	t.Run("success", func(t *testing.T) {
		req, _ := http.NewRequest(http.MethodGet, "/api/v1/helicopters/get-all?filter[search]=OE&page[number]=1&page[size]=20", nil)
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d", resp.StatusCode)
		}
	})

	t.Run("list failed", func(t *testing.T) {
		repo.listErr = errors.New("list failed")
		req, _ := http.NewRequest(http.MethodGet, "/api/v1/helicopters/get-all", nil)
		resp, _ := app.Test(req)
		repo.listErr = nil
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})
}

func TestHelicopterHandler_ListDatatable(t *testing.T) {
	repo := newMemHelicopterRepo()
	repo.listRows = []helicopter.Helicopter{{ID: uuidv7.MustBytes(), Designation: "OE-XHZ", Identifier: "1234", Type: "EC-13", CreatedAt: time.Now(), UpdatedAt: time.Now()}}
	repo.listTotal = 1

	h := NewHelicopterHandler(service.NewHelicopterService(repo))
	app := fiber.New()
	app.Get("/api/v1/helicopters/get-all/dt", h.ListDatatable)

	t.Run("success", func(t *testing.T) {
		req, _ := http.NewRequest(http.MethodGet, "/api/v1/helicopters/get-all/dt?page=1&limit=10&draw=3&search=OE", nil)
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d", resp.StatusCode)
		}
	})

	t.Run("list failed", func(t *testing.T) {
		repo.listErr = errors.New("list failed")
		req, _ := http.NewRequest(http.MethodGet, "/api/v1/helicopters/get-all/dt", nil)
		resp, _ := app.Test(req)
		repo.listErr = nil
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})
}

func TestHelicopterResource(t *testing.T) {
	id := uuidv7.MustBytes()
	row := &helicopter.Helicopter{
		ID:          id,
		Designation: "OE-XHZ",
		Identifier:  "1234",
		Type:        "EC-13",
		SortKey:     intPtr(10),
		Engine1:     "TM",
		Engine2:     "TM",
		Consumption: 4.1,
		NR1:         true,
		NR2:         true,
		LH:          true,
		RH:          true,
		MGB:         true,
		IGB:         false,
		TGB:         true,
		UTCOffset:   -1,
		Dry:         false,
		Note:        "primary",
		IsActive:    true,
		CreatedBy:   uuidv7.MustBytes(),
		CreatedAt:   time.Now().UTC(),
		UpdatedAt:   time.Now().UTC(),
	}

	res := helicopterResource(row)
	if res.Type != "helicopter" {
		t.Fatalf("expected resource type helicopter, got %s", res.Type)
	}
	if res.Attributes.Identifier != "1234" {
		t.Fatalf("expected identifier mapped, got %q", res.Attributes.Identifier)
	}
	if res.Attributes.SortKey == nil || *res.Attributes.SortKey != 10 {
		t.Fatalf("expected sortkey mapped, got %#v", res.Attributes.SortKey)
	}
	if res.Attributes.IsInUse {
		t.Fatalf("expected is_in_use default false")
	}
	if res.Attributes.CreatedBy == nil || res.Attributes.CreatedBy.ID == "" {
		t.Fatalf("expected created_by mapped")
	}
	if res.Attributes.CreatedAt == "" || res.Attributes.UpdatedAt == "" {
		t.Fatalf("expected created_at and updated_at mapped")
	}
}

func TestHelicopterHandler_DefaultHelpers(t *testing.T) {
	if boolOrDefault(nil, true) != true {
		t.Fatalf("expected bool default")
	}
	vBool := false
	if boolOrDefault(&vBool, true) != false {
		t.Fatalf("expected bool pointer value")
	}

	if intOrDefault(nil, 7) != 7 {
		t.Fatalf("expected int default")
	}
	vInt := 12
	if intOrDefault(&vInt, 7) != 12 {
		t.Fatalf("expected int pointer value")
	}

	if floatOrDefault(nil, 1.5) != 1.5 {
		t.Fatalf("expected float default")
	}
	vFloat := 9.9
	if floatOrDefault(&vFloat, 1.5) != 9.9 {
		t.Fatalf("expected float pointer value")
	}

	if stringOrDefault(nil, "a") != "a" {
		t.Fatalf("expected string default")
	}
	vStr := "b"
	if stringOrDefault(&vStr, "a") != "b" {
		t.Fatalf("expected string pointer value")
	}
}

func TestNormalizeHelicopterIdentifier(t *testing.T) {
	tests := []struct {
		name string
		raw  string
		want string
	}{
		{name: "empty", raw: "", want: ""},
		{name: "plain", raw: "1234", want: "1234"},
		{name: "trim spaces", raw: "  ABC  ", want: "ABC"},
		{name: "keep free text", raw: "s/n 1234", want: "S/N 1234"},
		{name: "keep free text upper", raw: "S/N 1234", want: "S/N 1234"},
		{name: "spaces only", raw: "   ", want: ""},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			got := normalizeHelicopterIdentifier(tc.raw)
			if got != tc.want {
				t.Fatalf("normalizeHelicopterIdentifier(%q) = %q, want %q", tc.raw, got, tc.want)
			}
		})
	}
}

func TestHelicopterHandler_HelperBranches(t *testing.T) {
	t.Run("with setters return same handler", func(t *testing.T) {
		h := NewHelicopterHandler(service.NewHelicopterService(newMemHelicopterRepo()))
		if h.WithFileManagerService(nil) != h {
			t.Fatalf("expected WithFileManagerService to return same handler")
		}
		if h.WithFileStorage(nil) != h {
			t.Fatalf("expected WithFileStorage to return same handler")
		}
	})

	t.Run("resolve foto attachment via file uuid without file manager writes validation error", func(t *testing.T) {
		h := NewHelicopterHandler(service.NewHelicopterService(newMemHelicopterRepo()))
		app := fiber.New()
		app.Get("/", func(c *fiber.Ctx) error {
			raw := "bad-uuid"
			_, handled := h.resolveFotoAttachmentID(c, uuidv7.MustBytes(), &raw, "Create failed")
			if !handled {
				t.Fatalf("expected invalid uuid to be handled")
			}
			return nil
		})
		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)
		}
	})

	t.Run("resolve existing foto attachment returns direct attachment id", func(t *testing.T) {
		attachmentID := uuidv7.MustBytes()
		fileManagerSvc := service.NewFileManagerService(nil, nil, &helicopterAttachmentRepoMock{
			getByIDFn: func(id []byte) (*filemanager.Attachment, error) {
				return &filemanager.Attachment{ID: id}, nil
			},
		}, service.FileManagerServiceDependencies{})
		h := NewHelicopterHandler(service.NewHelicopterService(newMemHelicopterRepo())).WithFileManagerService(fileManagerSvc)
		app := fiber.New()
		app.Get("/", func(c *fiber.Ctx) error {
			raw := mustUUIDStringHelicopter(attachmentID)
			got, handled := h.resolveExistingFotoAttachmentOrFileID(c, uuidv7.MustBytes(), &raw, "Update failed")
			if handled {
				t.Fatalf("expected existing attachment path to succeed")
			}
			if string(got) != string(attachmentID) {
				t.Fatalf("expected direct attachment id returned")
			}
			return c.SendStatus(fiber.StatusOK)
		})
		req, _ := http.NewRequest(http.MethodGet, "/", nil)
		if _, err := app.Test(req); err != nil {
			t.Fatalf("app.Test: %v", err)
		}
	})

	t.Run("resolve existing foto attachment surfaces repository error", func(t *testing.T) {
		fileManagerSvc := service.NewFileManagerService(nil, nil, &helicopterAttachmentRepoMock{
			getByIDFn: func([]byte) (*filemanager.Attachment, error) {
				return nil, errors.New("attachment lookup failed")
			},
		}, service.FileManagerServiceDependencies{})
		h := NewHelicopterHandler(service.NewHelicopterService(newMemHelicopterRepo())).WithFileManagerService(fileManagerSvc)
		app := fiber.New()
		app.Get("/", func(c *fiber.Ctx) error {
			raw := mustUUIDStringHelicopter(uuidv7.MustBytes())
			_, handled := h.resolveExistingFotoAttachmentOrFileID(c, uuidv7.MustBytes(), &raw, "Update failed")
			if !handled {
				t.Fatalf("expected repository error to be handled")
			}
			return nil
		})
		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)
		}
	})

	t.Run("require AOG pin verification branches", func(t *testing.T) {
		h := NewHelicopterHandler(service.NewHelicopterService(newMemHelicopterRepo()))
		app := fiber.New()
		app.Get("/missing-config", func(c *fiber.Ctx) error {
			if !h.requireAOGPINVerification(c) {
				t.Fatalf("expected missing pin verifier to block request")
			}
			return nil
		})
		req, _ := http.NewRequest(http.MethodGet, "/missing-config", nil)
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusUnauthorized {
			t.Fatalf("expected 401, got %d", resp.StatusCode)
		}

		h.pinVerifier = &helicopterPINVerifierMock{}
		app.Get("/missing-user", func(c *fiber.Ctx) error {
			if !h.requireAOGPINVerification(c) {
				t.Fatalf("expected missing actor to block request")
			}
			return nil
		})
		req, _ = http.NewRequest(http.MethodGet, "/missing-user", nil)
		resp, _ = app.Test(req)
		if resp.StatusCode != http.StatusUnauthorized {
			t.Fatalf("expected 401, got %d", resp.StatusCode)
		}

		app.Use(func(c *fiber.Ctx) error {
			c.Locals("user_id", uuidv7.MustBytes())
			return c.Next()
		})
		h.pinVerifier = &helicopterPINVerifierMock{
			consumeFn: func(context.Context, []byte, string, string) error { return errors.New("missing pin") },
		}
		app.Get("/pin-required", func(c *fiber.Ctx) error {
			if !h.requireAOGPINVerification(c) {
				t.Fatalf("expected invalid token to require pin")
			}
			return nil
		})
		req, _ = http.NewRequest(http.MethodGet, "/pin-required", nil)
		resp, _ = app.Test(req)
		if resp.StatusCode != http.StatusAccepted {
			t.Fatalf("expected 202, got %d", resp.StatusCode)
		}

		h.pinVerifier = &helicopterPINVerifierMock{
			consumeFn: func(context.Context, []byte, string, string) error { return nil },
		}
		app.Get("/pin-ok", func(c *fiber.Ctx) error {
			if h.requireAOGPINVerification(c) {
				t.Fatalf("expected valid token to pass")
			}
			return c.SendStatus(fiber.StatusOK)
		})
		req, _ = http.NewRequest(http.MethodGet, "/pin-ok", nil)
		req.Header.Set(pinVerificationHeader, "token")
		resp, _ = app.Test(req)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d", resp.StatusCode)
		}
	})
}

func mustUUIDStringHelicopter(id []byte) string {
	v, err := uuidv7.BytesToString(id)
	if err != nil {
		panic(err)
	}
	return v
}
