package handlers

import (
	"bytes"
	"context"
	"encoding/hex"
	"errors"
	"io"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"

	"github.com/gofiber/fiber/v2"
	"gorm.io/gorm"

	"wucher/internal/domain/contact"
	filemanager "wucher/internal/domain/file_manager"
	"wucher/internal/shared/pkg/uuidv7"
	"wucher/internal/transport/http/dto"
)

type stubContactService struct {
	listItems  []contact.ContactListItem
	listTotal  int64
	listErr    error
	lastFilter contact.ListFilter

	getItem *contact.ContactListItem
	getErr  error

	resolveRoleCode     string
	resolveRoleCodeByID map[string]string
	resolveRoleErr      error
	lastResolveID       []byte

	createUserID []byte
	createErr    error
	lastCreate   contact.CreateInput

	updateErr  error
	lastUpdate contact.UpdateInput

	deleteErr    error
	lastDeleteID []byte

	updateStatusErr    error
	lastUpdateStatusID []byte
	lastIsActive       bool

	updateRoleErr    error
	lastUpdateRoleID []byte
	lastRoleCode     string

	updatePilotCategoryErr error
	lastUpdatePilotID      []byte
	lastPilotCategory      string

	updateLicenseErr    error
	lastUpdateLicenseID []byte
	lastLicenseNo       *string
	updateChiefFlagsErr        error
	lastUpdateChiefFlagsID     []byte
	lastChiefFlags             contact.ProfilePatch
	updateResponsibleErr       error
	lastUpdateResponsibleID    []byte
	lastResponsibleComplaints  bool
	bulkUpdateErr              error
	bulkUpdateCount            int64
	lastBulkIDs                [][]byte
	lastBulkIsActive           bool
	checkSensitiveActionCalled bool
	isActorAdmin               bool
	isActorAdminErr            error
}

func (s *stubContactService) ListContacts(_ context.Context, filter contact.ListFilter) ([]contact.ContactListItem, int64, error) {
	s.lastFilter = filter
	if s.listErr != nil {
		return nil, 0, s.listErr
	}
	return s.listItems, s.listTotal, nil
}

func (s *stubContactService) GetContactByID(_ context.Context, userID []byte) (*contact.ContactListItem, error) {
	if s.getErr != nil {
		return nil, s.getErr
	}
	if s.getItem != nil && len(s.getItem.UserID) == 0 {
		copyItem := *s.getItem
		copyItem.UserID = append([]byte(nil), userID...)
		return &copyItem, nil
	}
	return s.getItem, nil
}

func (s *stubContactService) IsActorAdmin(_ context.Context) (bool, error) {
	if s.isActorAdminErr != nil {
		return false, s.isActorAdminErr
	}
	return s.isActorAdmin, nil
}

func (s *stubContactService) ResolveRoleCodeByID(_ context.Context, roleID []byte) (string, error) {
	s.lastResolveID = append([]byte(nil), roleID...)
	if s.resolveRoleErr != nil {
		return "", s.resolveRoleErr
	}
	if s.resolveRoleCodeByID != nil {
		if code, ok := s.resolveRoleCodeByID[string(roleID)]; ok {
			return code, nil
		}
	}
	return s.resolveRoleCode, nil
}

func (s *stubContactService) CreateContact(_ context.Context, in contact.CreateInput) ([]byte, error) {
	s.lastCreate = in
	if s.createErr != nil {
		return nil, s.createErr
	}
	if len(s.createUserID) == 0 {
		s.createUserID = uuidv7.MustBytes()
	}
	return s.createUserID, nil
}

func (s *stubContactService) UpdateContact(_ context.Context, in contact.UpdateInput) error {
	s.lastUpdate = in
	return s.updateErr
}

func (s *stubContactService) DeleteContact(_ context.Context, userID []byte) error {
	s.lastDeleteID = append([]byte(nil), userID...)
	return s.deleteErr
}

func (s *stubContactService) UpdateContactStatus(_ context.Context, userID []byte, isActive bool) error {
	s.lastUpdateStatusID = append([]byte(nil), userID...)
	s.lastIsActive = isActive
	return s.updateStatusErr
}

func (s *stubContactService) UpdateContactRole(_ context.Context, userID []byte, roleCode string) error {
	s.lastUpdateRoleID = append([]byte(nil), userID...)
	s.lastRoleCode = roleCode
	return s.updateRoleErr
}

func (s *stubContactService) UpdatePilotCategory(_ context.Context, userID []byte, category string) error {
	s.lastUpdatePilotID = append([]byte(nil), userID...)
	s.lastPilotCategory = category
	return s.updatePilotCategoryErr
}

func (s *stubContactService) UpdateLicense(_ context.Context, userID []byte, licenseNo *string) error {
	s.lastUpdateLicenseID = append([]byte(nil), userID...)
	s.lastLicenseNo = licenseNo
	return s.updateLicenseErr
}

func (s *stubContactService) UpdateChiefFlags(_ context.Context, userID []byte, patch contact.ProfilePatch) error {
	s.lastUpdateChiefFlagsID = append([]byte(nil), userID...)
	s.lastChiefFlags = patch
	return s.updateChiefFlagsErr
}

func (s *stubContactService) UpdateResponsibleComplaints(_ context.Context, userID []byte, value bool) error {
	s.lastUpdateResponsibleID = append([]byte(nil), userID...)
	s.lastResponsibleComplaints = value
	return s.updateResponsibleErr
}

func (s *stubContactService) BulkUpdateStatus(_ context.Context, userIDs [][]byte, isActive bool) (int64, error) {
	s.lastBulkIDs = append([][]byte(nil), userIDs...)
	s.lastBulkIsActive = isActive
	if s.bulkUpdateErr != nil {
		return 0, s.bulkUpdateErr
	}
	return s.bulkUpdateCount, nil
}

func (s *stubContactService) CheckSensitiveAction(_ context.Context, _ []byte, _ string, _ string) error {
	s.checkSensitiveActionCalled = true
	return nil
}

type stubContactInviter struct {
	err        error
	lastUserID []byte
}

func (s *stubContactInviter) SendInviteSetPasswordForUser(_ context.Context, userID []byte) error {
	s.lastUserID = append([]byte(nil), userID...)
	return s.err
}

func newContactTestApp(h *ContactHandler) *fiber.App {
	app := fiber.New()
	app.Post("/contacts/create", h.Create)
	app.Get("/contacts", h.List)
	app.Get("/contacts/dt", h.ListDatatable)
	app.Get("/contacts/:user_id", h.Get)
	app.Patch("/contacts/:user_id", h.Update)
	app.Delete("/contacts/:user_id", h.Delete)
	app.Patch("/contacts/:user_id/status", h.UpdateContactStatus)
	app.Patch("/contacts/:user_id/role", h.UpdateContactRole)
	app.Patch("/contacts/:user_id/pilot-category", h.UpdatePilotCategory)
	app.Patch("/contacts/:user_id/license", h.UpdateLicense)
	app.Patch("/contacts/:user_id/chief-flags", h.UpdateChiefFlags)
	app.Post("/contacts/bulk-update", h.BulkUpdate)
	return app
}

func doJSONRequestContact(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 mustUUIDStringContact(t *testing.T, id []byte) string {
	t.Helper()
	s, err := uuidv7.BytesToString(id)
	if err != nil {
		t.Fatalf("uuid to string: %v", err)
	}
	return s
}

func mustRoleRawContact(t *testing.T, roleID []byte, roleName, roleCode string) string {
	t.Helper()
	return strings.ToLower(hex.EncodeToString(roleID)) + "|" +
		strings.ToLower(hex.EncodeToString([]byte(roleName))) + "|" + roleCode
}

func strPtrContact(v string) *string { return &v }
func boolPtrContact(v bool) *bool    { return &v }
func intPtrContact(v int) *int       { return &v }
func floatPtrContact(v float64) *float64 {
	return &v
}

func TestNewContactHandler(t *testing.T) {
	h := NewContactHandler(&stubContactService{}, &stubContactInviter{})
	if h == nil || h.validate == nil {
		t.Fatalf("expected handler and validator initialized")
	}
	h = h.WithFileManagerService(nil).WithFileStorage(nil)
	if h == nil {
		t.Fatalf("expected chain methods return handler")
	}
}

func TestContactHandlerCreate(t *testing.T) {
	t.Run("invalid json", func(t *testing.T) {
		app := newContactTestApp(NewContactHandler(&stubContactService{}, nil))
		resp, _ := doJSONRequestContact(t, app, http.MethodPost, "/contacts/create", []byte(`{"data":`))
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})

	t.Run("validation error", func(t *testing.T) {
		app := newContactTestApp(NewContactHandler(&stubContactService{}, nil))
		resp, _ := doJSONRequestContact(t, app, http.MethodPost, "/contacts/create", []byte(`{}`))
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})

	t.Run("invalid role id", func(t *testing.T) {
		app := newContactTestApp(NewContactHandler(&stubContactService{}, nil))
		body := []byte(`{"data":{"type":"contact_create","attributes":{"role_id":"bad","email":"user@example.com"}}}`)
		resp, _ := doJSONRequestContact(t, app, http.MethodPost, "/contacts/create", body)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})

	t.Run("invalid create type", func(t *testing.T) {
		svc := &stubContactService{resolveRoleCode: "doctor"}
		app := newContactTestApp(NewContactHandler(svc, nil))
		roleID := mustUUIDStringContact(t, uuidv7.MustBytes())
		body := []byte(`{"data":{"type":"contact_pilot_create","attributes":{"role_id":"` + roleID + `","email":"user@example.com"}}}`)
		resp, _ := doJSONRequestContact(t, app, http.MethodPost, "/contacts/create", body)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})

	t.Run("mismatched profile payload is rejected", func(t *testing.T) {
		svc := &stubContactService{resolveRoleCode: "pilot"}
		app := newContactTestApp(NewContactHandler(svc, nil))
		roleID := mustUUIDStringContact(t, uuidv7.MustBytes())
		body := []byte(`{"data":{"type":"contact_create","attributes":{"role_id":"` + roleID + `","email":"user@example.com","doctor":{"short_name":"Dr"}}}}`)
		resp, _ := doJSONRequestContact(t, app, http.MethodPost, "/contacts/create", body)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})

	t.Run("create error", func(t *testing.T) {
		svc := &stubContactService{resolveRoleCode: "pilot", createErr: errors.New("create failed")}
		app := newContactTestApp(NewContactHandler(svc, nil))
		roleID := mustUUIDStringContact(t, uuidv7.MustBytes())
		body := []byte(`{"data":{"type":"contact_create","attributes":{"role_id":"` + roleID + `","email":"user@example.com","username":"john","pilot":{"pilot_category":"regular"}}}}`)
		resp, _ := doJSONRequestContact(t, app, http.MethodPost, "/contacts/create", body)
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})

	t.Run("success with invite and fetched item", func(t *testing.T) {
		roleID := mustUUIDStringContact(t, uuidv7.MustBytes())
		userID := uuidv7.MustBytes()
		svc := &stubContactService{
			resolveRoleCode: "pilot",
			createUserID:    userID,
			getItem: &contact.ContactListItem{
				RoleCode:       "pilot",
				RoleName:       "Pilot",
				PilotCategory:  "regular",
				Username:       " pilot.user ",
				Email:          "pilot@example.com",
				FirstName:      "John",
				LastName:       "Doe",
				IsActive:       true,
				PilotShortName: strPtrContact("PJ"),
			},
		}
		inviter := &stubContactInviter{}
		app := newContactTestApp(NewContactHandler(svc, inviter))
		body := []byte(`{"data":{"type":"contact_create","attributes":{"role_id":"` + roleID + `","email":"pilot@example.com","username":"pilot.user","first_name":" John ","last_name":" Doe ","mobile_phone":" 081 ","timezone":" Asia/Jakarta ","is_active":true,"pilot":{"pilot_category":"regular","short_name":" PJ ","license_no":" LIC-1 "}}}}`)
		resp, _ := doJSONRequestContact(t, app, http.MethodPost, "/contacts/create", body)
		if resp.StatusCode != http.StatusCreated {
			t.Fatalf("expected 201, got %d", resp.StatusCode)
		}
		if svc.lastCreate.User.Email == nil || *svc.lastCreate.User.Email != "pilot@example.com" {
			t.Fatalf("expected trimmed email, got %#v", svc.lastCreate.User.Email)
		}
		if svc.lastCreate.Profile.PilotCategory == nil || *svc.lastCreate.Profile.PilotCategory != "regular" {
			t.Fatalf("expected pilot category patched")
		}
		if string(inviter.lastUserID) != string(userID) {
			t.Fatalf("expected inviter called with created user id")
		}
	})

	t.Run("success when invite fails and refetch nil", func(t *testing.T) {
		roleID := mustUUIDStringContact(t, uuidv7.MustBytes())
		userID := uuidv7.MustBytes()
		svc := &stubContactService{resolveRoleCode: "staff", createUserID: userID}
		inviter := &stubContactInviter{err: errors.New("ses down")}
		app := newContactTestApp(NewContactHandler(svc, inviter))
		body := []byte(`{"data":{"type":"contact_create","attributes":{"role_id":"` + roleID + `","email":"user@example.com","staff":{"short_name":"Dina"}}}}`)
		resp, _ := doJSONRequestContact(t, app, http.MethodPost, "/contacts/create", body)
		if resp.StatusCode != http.StatusCreated {
			t.Fatalf("expected 201, got %d", resp.StatusCode)
		}
	})

	t.Run("get after create error", func(t *testing.T) {
		roleID := mustUUIDStringContact(t, uuidv7.MustBytes())
		svc := &stubContactService{resolveRoleCode: "staff", createUserID: uuidv7.MustBytes(), getErr: errors.New("lookup failed")}
		app := newContactTestApp(NewContactHandler(svc, nil))
		body := []byte(`{"data":{"type":"contact_create","attributes":{"role_id":"` + roleID + `","email":"user@example.com","staff":{"short_name":"Dina"}}}}`)
		resp, _ := doJSONRequestContact(t, app, http.MethodPost, "/contacts/create", body)
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})
}

func TestContactHelperFunctions(t *testing.T) {
	t.Run("contactBaseRoles and contactDULBases", func(t *testing.T) {
		baseID := uuidv7.MustBytes()
		baseHex := strings.ToLower(hex.EncodeToString(baseID))
		nameHex := strings.ToLower(hex.EncodeToString([]byte("Halim Base")))
		catHex := strings.ToLower(hex.EncodeToString([]byte("hems")))
		raw := baseHex + "|" + nameHex + "|dul|" + catHex + "," + baseHex + "|" + nameHex + "|pilot|" + catHex
		roles := contactBaseRoles(&raw)
		if len(roles) != 2 {
			t.Fatalf("expected 2 base roles, got %d", len(roles))
		}
		dulBases := contactDULBases(&raw)
		if len(dulBases) != 1 {
			t.Fatalf("expected only dul role base, got %d", len(dulBases))
		}
		if dulBases[0]["category"] != "hems" {
			t.Fatalf("expected dul base category hems")
		}
		invalid := "invalid"
		if got := contactBaseRoles(&invalid); got != nil {
			t.Fatalf("invalid raw should return nil")
		}
		if got := contactDULBases(&invalid); got != nil {
			t.Fatalf("invalid raw should return nil dul bases")
		}
	})

	t.Run("contactRoles", func(t *testing.T) {
		roleID := uuidv7.MustBytes()
		roleRaw := strings.ToLower(hex.EncodeToString(roleID)) + "|" +
			strings.ToLower(hex.EncodeToString([]byte("Pilot"))) + "|pilot"
		out := contactRoles(&roleRaw)
		if len(out) != 1 || out[0]["role_code"] != "pilot" {
			t.Fatalf("expected pilot role parsed")
		}
		blank := " "
		if got := contactRoles(&blank); len(got) != 0 {
			t.Fatalf("blank raw should return empty slice")
		}
		bad := "abc"
		if got := contactRoles(&bad); len(got) != 0 {
			t.Fatalf("bad raw should return empty slice")
		}
	})

	t.Run("applyContactFoto no attachment id", func(t *testing.T) {
		h := NewContactHandler(&stubContactService{}, nil)
		attrs := map[string]any{}
		h.applyContactFoto(context.Background(), &contact.ContactListItem{}, attrs)
		if _, ok := attrs["foto"]; ok {
			t.Fatalf("foto should not be set without attachment id")
		}
	})

	t.Run("resolveContactProfileAttachmentID and applyContactFoto success path", func(t *testing.T) {
		raw := mustUUIDStringContact(t, uuidv7.MustBytes())
		profileID := uuidv7.MustBytes()
		thumb := "thumb-key"
		h := NewContactHandler(&stubContactService{}, nil).
			WithFileManagerService(&fileManagerAttachmentHandlerServiceMock{
				createAttachmentFn: func(context.Context, filemanager.CreateAttachmentParams) (*filemanager.Attachment, error) {
					return &filemanager.Attachment{ID: profileID}, nil
				},
				getAttachmentFn: func(context.Context, []byte) (*filemanager.Attachment, error) {
					return &filemanager.Attachment{
						ID: profileID,
						File: &filemanager.File{
							ObjectKey:          "photo-key",
							SizeBytes:          100,
							ThumbnailObjectKey: &thumb,
							ThumbnailSizeBytes: 10,
						},
					}, nil
				},
			}).
			WithFileStorage(&basePresignStorageMock{url: "https://example.com/signed"})

		app := fiber.New()
		app.Get("/x", func(c *fiber.Ctx) error {
			c.Locals("user_id", uuidv7.MustBytes())
			id, handled := h.resolveContactProfileAttachmentID(c, uuidv7.MustBytes(), &raw)
			if handled || len(id) != 16 {
				t.Fatalf("expected resolved attachment id")
			}
			return nil
		})
		resp, err := app.Test(httptest.NewRequest(http.MethodGet, "/x", nil))
		if err != nil || resp.StatusCode != http.StatusOK {
			t.Fatalf("unexpected resolveContactProfileAttachmentID result")
		}

		attrs := map[string]any{}
		h.applyContactFoto(context.Background(), &contact.ContactListItem{ProfileAttachmentID: profileID}, attrs)
		foto, ok := attrs["foto"].(map[string]any)
		if !ok || strings.TrimSpace(foto["download_url"].(string)) == "" || strings.TrimSpace(foto["thumbnail_download_url"].(string)) == "" {
			t.Fatalf("expected foto urls to be set")
		}
	})

	t.Run("resolveContactProfileAttachmentID invalid uuid", func(t *testing.T) {
		raw := "bad"
		h := NewContactHandler(&stubContactService{}, nil).WithFileManagerService(&fileManagerAttachmentHandlerServiceMock{})
		app := fiber.New()
		app.Get("/x", func(c *fiber.Ctx) error {
			_, handled := h.resolveContactProfileAttachmentID(c, uuidv7.MustBytes(), &raw)
			if !handled {
				t.Fatalf("expected handled invalid uuid")
			}
			return nil
		})
		resp, err := app.Test(httptest.NewRequest(http.MethodGet, "/x", nil))
		if err != nil || resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422 for invalid uuid")
		}
	})

	t.Run("ensureContactProfileAttachmentSet error when missing profile id", func(t *testing.T) {
		svc := &stubContactService{getItem: &contact.ContactListItem{UserID: uuidv7.MustBytes()}}
		h := NewContactHandler(svc, nil)
		if err := h.ensureContactProfileAttachmentSet(context.Background(), uuidv7.MustBytes()); err == nil {
			t.Fatalf("expected error when profile attachment id is missing")
		}
	})

	t.Run("inferContactCreateRoleCodes", func(t *testing.T) {
		codes := inferContactCreateRoleCodes(dto.ContactCreateAttributes{
			Pilot: &dto.PilotProfileAttributes{},
			Staff: &dto.StaffProfileAttributes{},
		})
		if len(codes) != 2 {
			t.Fatalf("expected inferred role codes from payloads, got %d", len(codes))
		}
	})

	t.Run("validatePilotDULBasePayload", func(t *testing.T) {
		errs := validatePilotDULBasePayload(&dto.PilotProfileAttributes{
			DULBaseIDs: []string{"bad-uuid"},
		})
		if len(errs) == 0 {
			t.Fatalf("expected validation error for invalid dul_base_ids")
		}

		errs = validatePilotDULBasePayload(&dto.PilotProfileAttributes{
			DULBase: []dto.DULBase{{ID: "bad-uuid"}},
		})
		if len(errs) == 0 {
			t.Fatalf("expected validation error for invalid dul_base")
		}

		errs = validatePilotDULBasePayload(&dto.PilotProfileAttributes{
			DULBaseIDs: []string{mustUUIDStringContact(t, uuidv7.MustBytes())},
			DULBase:    []dto.DULBase{{ID: mustUUIDStringContact(t, uuidv7.MustBytes())}},
		})
		if len(errs) == 0 {
			t.Fatalf("expected validation error when both dul_base_ids and dul_base are provided")
		}
	})
}

func TestContactHandlerList(t *testing.T) {
	t.Run("success with normalized pagination", func(t *testing.T) {
		userID := uuidv7.MustBytes()
		svc := &stubContactService{
			listItems: []contact.ContactListItem{{
				UserID:    userID,
				RoleCode:  "staff",
				RoleName:  "Staff",
				Email:     "staff@example.com",
				FirstName: "Jane",
				LastName:  "Doe",
				IsActive:  true,
			}},
			listTotal: 1,
		}
		app := newContactTestApp(NewContactHandler(svc, nil))
		resp, _ := doJSONRequestContact(t, app, http.MethodGet, "/contacts?role=staff&pilot_category=regular&search=jane&sort=-sortkey&limit=500&offset=-4", nil)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d", resp.StatusCode)
		}
		if svc.lastFilter.Limit != 0 || svc.lastFilter.Offset != 0 {
			t.Fatalf("expected normalized pagination, got %#v", svc.lastFilter)
		}
		if svc.lastFilter.Sort != "-sortkey" {
			t.Fatalf("expected sort forwarded, got %#v", svc.lastFilter)
		}
	})

	t.Run("supports role alias and special flag filters", func(t *testing.T) {
		svc := &stubContactService{listTotal: 0}
		app := newContactTestApp(NewContactHandler(svc, nil))
		resp, _ := doJSONRequestContact(
			t,
			app,
			http.MethodGet,
			"/contacts?role=technicians&chief_doctor=true&chief_technician=1&responsible_complaint=yes&chief_pilot=true&ifr_qualified=true",
			nil,
		)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d", resp.StatusCode)
		}
		if svc.lastFilter.Role != "technician" {
			t.Fatalf("expected role alias normalized, got %q", svc.lastFilter.Role)
		}
		if svc.lastFilter.ChiefDoctor == nil || !*svc.lastFilter.ChiefDoctor {
			t.Fatalf("expected chief_doctor=true")
		}
		if svc.lastFilter.ChiefTechnician == nil || !*svc.lastFilter.ChiefTechnician {
			t.Fatalf("expected chief_technician=true")
		}
		if svc.lastFilter.ResponsibleComplaint == nil || !*svc.lastFilter.ResponsibleComplaint {
			t.Fatalf("expected responsible_complaint=true")
		}
		if svc.lastFilter.ChiefPilot == nil || !*svc.lastFilter.ChiefPilot {
			t.Fatalf("expected chief_pilot=true")
		}
		if svc.lastFilter.IFRQualified == nil || !*svc.lastFilter.IFRQualified {
			t.Fatalf("expected ifr_qualified=true")
		}
	})

	t.Run("list error", func(t *testing.T) {
		svc := &stubContactService{listErr: errors.New("list failed")}
		app := newContactTestApp(NewContactHandler(svc, nil))
		resp, _ := doJSONRequestContact(t, app, http.MethodGet, "/contacts", nil)
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})
}

func TestContactHandlerListDatatable(t *testing.T) {
	t.Run("success with page size fallback", func(t *testing.T) {
		svc := &stubContactService{listTotal: 2}
		app := newContactTestApp(NewContactHandler(svc, nil))
		resp, _ := doJSONRequestContact(t, app, http.MethodGet, "/contacts/dt?page=0&size=0&draw=9&role=pilot&sort=note", nil)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d", resp.StatusCode)
		}
		if svc.lastFilter.Limit != 20 || svc.lastFilter.Offset != 0 || svc.lastFilter.Role != "pilot" {
			t.Fatalf("unexpected filter %#v", svc.lastFilter)
		}
		if svc.lastFilter.Sort != "note" {
			t.Fatalf("expected sort forwarded, got %#v", svc.lastFilter)
		}
	})

	t.Run("supports special flag filters", func(t *testing.T) {
		svc := &stubContactService{listTotal: 0}
		app := newContactTestApp(NewContactHandler(svc, nil))
		resp, _ := doJSONRequestContact(
			t,
			app,
			http.MethodGet,
			"/contacts/dt?chief_doctor=false&chief_technician=0&responsible_complaint=no&chief_pilot=false&ifr_qualified=false",
			nil,
		)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d", resp.StatusCode)
		}
		if svc.lastFilter.ChiefDoctor == nil || *svc.lastFilter.ChiefDoctor {
			t.Fatalf("expected chief_doctor=false")
		}
		if svc.lastFilter.ChiefTechnician == nil || *svc.lastFilter.ChiefTechnician {
			t.Fatalf("expected chief_technician=false")
		}
		if svc.lastFilter.ResponsibleComplaint == nil || *svc.lastFilter.ResponsibleComplaint {
			t.Fatalf("expected responsible_complaint=false")
		}
		if svc.lastFilter.ChiefPilot == nil || *svc.lastFilter.ChiefPilot {
			t.Fatalf("expected chief_pilot=false")
		}
		if svc.lastFilter.IFRQualified == nil || *svc.lastFilter.IFRQualified {
			t.Fatalf("expected ifr_qualified=false")
		}
	})

	t.Run("list error", func(t *testing.T) {
		svc := &stubContactService{listErr: errors.New("datatable failed")}
		app := newContactTestApp(NewContactHandler(svc, nil))
		resp, _ := doJSONRequestContact(t, app, http.MethodGet, "/contacts/dt", nil)
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})
}

func TestContactHandlerGetAndWriteByID(t *testing.T) {
	t.Run("invalid uuid", func(t *testing.T) {
		app := newContactTestApp(NewContactHandler(&stubContactService{}, nil))
		resp, _ := doJSONRequestContact(t, app, http.MethodGet, "/contacts/not-uuid", nil)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})

	t.Run("service error", func(t *testing.T) {
		id := mustUUIDStringContact(t, uuidv7.MustBytes())
		svc := &stubContactService{getErr: errors.New("get failed")}
		app := newContactTestApp(NewContactHandler(svc, nil))
		resp, _ := doJSONRequestContact(t, app, http.MethodGet, "/contacts/"+id, nil)
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})

	t.Run("not found", func(t *testing.T) {
		id := mustUUIDStringContact(t, uuidv7.MustBytes())
		app := newContactTestApp(NewContactHandler(&stubContactService{}, nil))
		resp, _ := doJSONRequestContact(t, app, http.MethodGet, "/contacts/"+id, nil)
		if resp.StatusCode != http.StatusNotFound {
			t.Fatalf("expected 404, got %d", resp.StatusCode)
		}
	})

	t.Run("success", func(t *testing.T) {
		id := mustUUIDStringContact(t, uuidv7.MustBytes())
		svc := &stubContactService{getItem: &contact.ContactListItem{
			RoleCode:  "doctor",
			RoleName:  "Doctor",
			Email:     "doctor@example.com",
			FirstName: "Doc",
			LastName:  "Tor",
			IsActive:  true,
		}}
		app := newContactTestApp(NewContactHandler(svc, nil))
		resp, _ := doJSONRequestContact(t, app, http.MethodGet, "/contacts/"+id, nil)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d", resp.StatusCode)
		}
	})
}

func TestContactHandlerUpdate(t *testing.T) {
	userID := uuidv7.MustBytes()
	id := mustUUIDStringContact(t, userID)

	t.Run("invalid uuid", func(t *testing.T) {
		app := newContactTestApp(NewContactHandler(&stubContactService{}, nil))
		resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/not-uuid", []byte(`{}`))
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})

	t.Run("invalid json", func(t *testing.T) {
		app := newContactTestApp(NewContactHandler(&stubContactService{}, nil))
		resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/"+id, []byte(`{"data":`))
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})

	t.Run("validation error", func(t *testing.T) {
		app := newContactTestApp(NewContactHandler(&stubContactService{}, nil))
		resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/"+id, []byte(`{}`))
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})

	t.Run("not found", func(t *testing.T) {
		svc := &stubContactService{}
		app := newContactTestApp(NewContactHandler(svc, nil))
		body := []byte(`{"data":{"type":"contact_update","attributes":{"first_name":"Jane"}}}`)
		resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/"+id, body)
		if resp.StatusCode != http.StatusNotFound {
			t.Fatalf("expected 404, got %d", resp.StatusCode)
		}
	})

	t.Run("type must be general contact_update", func(t *testing.T) {
		svc := &stubContactService{getItem: &contact.ContactListItem{RoleCode: "staff", RoleName: "Staff"}}
		app := newContactTestApp(NewContactHandler(svc, nil))
		body := []byte(`{"data":{"type":"contact_staff_update","attributes":{"first_name":"Jane"}}}`)
		resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/"+id, body)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})

	t.Run("mismatched profile payload is rejected", func(t *testing.T) {
		svc := &stubContactService{getItem: &contact.ContactListItem{RoleCode: "staff", RoleName: "Staff"}}
		app := newContactTestApp(NewContactHandler(svc, nil))
		body := []byte(`{"data":{"type":"contact_update","attributes":{"pilot":{"short_name":"PJ"}}}}`)
		resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/"+id, body)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})

	t.Run("success with multiple role specific payloads for assigned roles", func(t *testing.T) {
		rolePilotID := uuidv7.MustBytes()
		roleAirRescuerID := uuidv7.MustBytes()
		rolesRaw := mustRoleRawContact(t, rolePilotID, "Pilot", "pilot") + "," +
			mustRoleRawContact(t, roleAirRescuerID, "Air Rescuer", "air_rescuer")
		svc := &stubContactService{
			getItem: &contact.ContactListItem{
				RoleCode: "pilot",
				RoleName: "Pilot",
				RolesRaw: &rolesRaw,
			},
			resolveRoleCodeByID: map[string]string{
				string(rolePilotID):      "pilot",
				string(roleAirRescuerID): "air_rescuer",
			},
		}
		app := newContactTestApp(NewContactHandler(svc, nil))
		body := []byte(`{"data":{"type":"contact_update","attributes":{"role_id":"` +
			mustUUIDStringContact(t, rolePilotID) + `","role_ids":["` + mustUUIDStringContact(t, rolePilotID) + `","` +
			mustUUIDStringContact(t, roleAirRescuerID) + `"],"pilot":{"short_name":"P"},"air_rescuer":{"short_name":"AR"}}}}`)
		resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/"+id, body)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d", resp.StatusCode)
		}
		if len(svc.lastUpdate.Profiles) != 2 {
			t.Fatalf("expected 2 profile patches, got %d", len(svc.lastUpdate.Profiles))
		}
	})

	t.Run("success", func(t *testing.T) {
		svc := &stubContactService{getItem: &contact.ContactListItem{
			RoleCode:  "staff",
			RoleName:  "Staff",
			Email:     "updated@example.com",
			FirstName: "Jane",
			LastName:  "Doe",
		}}
		app := newContactTestApp(NewContactHandler(svc, nil))
		body := []byte(`{"data":{"type":"contact_update","attributes":{"first_name":"Jane","staff":{"short_name":"PJ"}}}}`)
		resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/"+id, body)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d", resp.StatusCode)
		}
		if svc.lastUpdate.Profile.ShortName == nil || *svc.lastUpdate.Profile.ShortName != "PJ" {
			t.Fatalf("expected profile patch on update")
		}
	})

	t.Run("empty strings in update payload clear role profile fields", func(t *testing.T) {
		svc := &stubContactService{getItem: &contact.ContactListItem{
			RoleCode: "air_rescuer",
			RoleName: "Air Rescuer",
		}}
		app := newContactTestApp(NewContactHandler(svc, nil))
		body := []byte(`{"data":{"type":"contact_update","attributes":{"air_rescuer":{"short_name":"","location":" ","note":"","postcode":"","street_line":""}}}}`)
		resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/"+id, body)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d", resp.StatusCode)
		}
		if svc.lastUpdate.Profile.ShortName == nil || *svc.lastUpdate.Profile.ShortName != "" {
			t.Fatalf("expected short_name to be explicitly cleared, got %#v", svc.lastUpdate.Profile.ShortName)
		}
		if svc.lastUpdate.Profile.Location == nil || *svc.lastUpdate.Profile.Location != "" {
			t.Fatalf("expected location to be explicitly cleared, got %#v", svc.lastUpdate.Profile.Location)
		}
		if svc.lastUpdate.Profile.Note == nil || *svc.lastUpdate.Profile.Note != "" {
			t.Fatalf("expected note to be explicitly cleared, got %#v", svc.lastUpdate.Profile.Note)
		}
		if svc.lastUpdate.Profile.Postcode == nil || *svc.lastUpdate.Profile.Postcode != "" {
			t.Fatalf("expected postcode to be explicitly cleared, got %#v", svc.lastUpdate.Profile.Postcode)
		}
		if svc.lastUpdate.Profile.StreetLine == nil || *svc.lastUpdate.Profile.StreetLine != "" {
			t.Fatalf("expected street_line to be explicitly cleared, got %#v", svc.lastUpdate.Profile.StreetLine)
		}
	})

	t.Run("success update role_ids and profile for assigned role", func(t *testing.T) {
		roleStaffID := uuidv7.MustBytes()
		roleAirRescuerID := uuidv7.MustBytes()
		rolesRaw := mustRoleRawContact(t, roleStaffID, "Staff", "staff") + "," +
			mustRoleRawContact(t, roleAirRescuerID, "Air Rescuer", "air_rescuer")
		svc := &stubContactService{
			getItem: &contact.ContactListItem{
				RoleCode:  "staff",
				RoleName:  "Staff",
				Email:     "updated@example.com",
				FirstName: "Jane",
				LastName:  "Doe",
				RolesRaw:  &rolesRaw,
			},
			resolveRoleCodeByID: map[string]string{
				string(roleStaffID):      "staff",
				string(roleAirRescuerID): "air_rescuer",
			},
		}
		app := newContactTestApp(NewContactHandler(svc, nil))
		body := []byte(`{"data":{"type":"contact_update","attributes":{"role_id":"` +
			mustUUIDStringContact(t, roleAirRescuerID) +
			`","role_ids":["` + mustUUIDStringContact(t, roleAirRescuerID) + `","` + mustUUIDStringContact(t, roleStaffID) +
			`"],"air_rescuer":{"short_name":"AR"},"staff":{"short_name":"S"}}}}`)
		resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/"+id, body)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d", resp.StatusCode)
		}
		if !svc.lastUpdate.UpdateRoles {
			t.Fatalf("expected role updates enabled")
		}
		if len(svc.lastUpdate.RoleID) != 16 || string(svc.lastUpdate.RoleID) != string(roleAirRescuerID) {
			t.Fatalf("expected primary role updated to air_rescuer")
		}
		if len(svc.lastUpdate.RoleIDs) != 2 {
			t.Fatalf("expected 2 role ids, got %d", len(svc.lastUpdate.RoleIDs))
		}
		if len(svc.lastUpdate.Profiles) != 2 {
			t.Fatalf("expected two role profile patches")
		}
		foundAirRescuer := false
		foundStaff := false
		for i := range svc.lastUpdate.Profiles {
			switch svc.lastUpdate.Profiles[i].RoleCode {
			case "air_rescuer":
				foundAirRescuer = svc.lastUpdate.Profiles[i].Patch.ShortName != nil && *svc.lastUpdate.Profiles[i].Patch.ShortName == "AR"
			case "staff":
				foundStaff = svc.lastUpdate.Profiles[i].Patch.ShortName != nil && *svc.lastUpdate.Profiles[i].Patch.ShortName == "S"
			}
		}
		if !foundAirRescuer || !foundStaff {
			t.Fatalf("expected air_rescuer and staff patches")
		}
	})

	t.Run("success update role_ids only promotes first role_id as primary", func(t *testing.T) {
		rolePilotID := uuidv7.MustBytes()
		roleAirRescuerID := uuidv7.MustBytes()
		rolesRaw := mustRoleRawContact(t, rolePilotID, "Pilot", "pilot") + "," +
			mustRoleRawContact(t, roleAirRescuerID, "Air Rescuer", "air_rescuer")
		svc := &stubContactService{
			getItem: &contact.ContactListItem{
				RoleCode:  "pilot",
				RoleName:  "Pilot",
				Email:     "updated@example.com",
				FirstName: "Jane",
				LastName:  "Doe",
				RolesRaw:  &rolesRaw,
			},
			resolveRoleCodeByID: map[string]string{
				string(rolePilotID):      "pilot",
				string(roleAirRescuerID): "air_rescuer",
			},
		}
		app := newContactTestApp(NewContactHandler(svc, nil))
		body := []byte(`{"data":{"type":"contact_update","attributes":{"role_ids":["` +
			mustUUIDStringContact(t, roleAirRescuerID) + `","` + mustUUIDStringContact(t, rolePilotID) +
			`"],"air_rescuer":{"short_name":"AR"},"pilot":{"short_name":"P"}}}}`)
		resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/"+id, body)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d", resp.StatusCode)
		}
		if !svc.lastUpdate.UpdateRoles {
			t.Fatalf("expected role updates enabled")
		}
		if len(svc.lastUpdate.RoleID) != 16 || string(svc.lastUpdate.RoleID) != string(roleAirRescuerID) {
			t.Fatalf("expected primary role updated to first role_ids item (air_rescuer)")
		}
		if len(svc.lastUpdate.RoleIDs) != 2 {
			t.Fatalf("expected 2 role ids, got %d", len(svc.lastUpdate.RoleIDs))
		}
	})

	t.Run("role_ids update requires matching profile payloads", func(t *testing.T) {
		roleStaffID := uuidv7.MustBytes()
		roleAirRescuerID := uuidv7.MustBytes()
		rolesRaw := mustRoleRawContact(t, roleStaffID, "Staff", "staff") + "," +
			mustRoleRawContact(t, roleAirRescuerID, "Air Rescuer", "air_rescuer")
		svc := &stubContactService{
			getItem: &contact.ContactListItem{
				RoleCode: "staff",
				RoleName: "Staff",
				RolesRaw: &rolesRaw,
			},
			resolveRoleCodeByID: map[string]string{
				string(roleStaffID):      "staff",
				string(roleAirRescuerID): "air_rescuer",
			},
		}
		app := newContactTestApp(NewContactHandler(svc, nil))
		body := []byte(`{"data":{"type":"contact_update","attributes":{"role_ids":["` +
			mustUUIDStringContact(t, roleAirRescuerID) + `","` + mustUUIDStringContact(t, roleStaffID) +
			`"],"air_rescuer":{"short_name":"AR"}}}}`)
		resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/"+id, body)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})

	t.Run("invalid role_id format", func(t *testing.T) {
		svc := &stubContactService{
			getItem: &contact.ContactListItem{RoleCode: "staff", RoleName: "Staff"},
		}
		app := newContactTestApp(NewContactHandler(svc, nil))
		body := []byte(`{"data":{"type":"contact_update","attributes":{"role_id":"bad"}}}`)
		resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/"+id, body)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})

	t.Run("invalid role_ids format", func(t *testing.T) {
		svc := &stubContactService{
			getItem: &contact.ContactListItem{RoleCode: "staff", RoleName: "Staff"},
		}
		app := newContactTestApp(NewContactHandler(svc, nil))
		body := []byte(`{"data":{"type":"contact_update","attributes":{"role_ids":["bad"]}}}`)
		resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/"+id, body)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})

	t.Run("success clear all roles with empty role_ids", func(t *testing.T) {
		roleStaffID := uuidv7.MustBytes()
		rolesRaw := mustRoleRawContact(t, roleStaffID, "Staff", "staff")
		svc := &stubContactService{
			getItem: &contact.ContactListItem{
				RoleCode: "staff",
				RoleName: "Staff",
				RolesRaw: &rolesRaw,
			},
		}
		app := newContactTestApp(NewContactHandler(svc, nil))
		body := []byte(`{"data":{"type":"contact_update","attributes":{"role_ids":[]}}}`)
		resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/"+id, body)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d", resp.StatusCode)
		}
		if !svc.lastUpdate.UpdateRoles {
			t.Fatalf("expected role updates enabled")
		}
		if len(svc.lastUpdate.RoleID) != 0 {
			t.Fatalf("expected empty primary role id")
		}
		if len(svc.lastUpdate.RoleIDs) != 0 {
			t.Fatalf("expected empty role ids")
		}
	})

	t.Run("success clear all roles with empty role_id string", func(t *testing.T) {
		roleStaffID := uuidv7.MustBytes()
		rolesRaw := mustRoleRawContact(t, roleStaffID, "Staff", "staff")
		svc := &stubContactService{
			getItem: &contact.ContactListItem{
				RoleCode: "staff",
				RoleName: "Staff",
				RolesRaw: &rolesRaw,
			},
		}
		app := newContactTestApp(NewContactHandler(svc, nil))
		body := []byte(`{"data":{"type":"contact_update","attributes":{"role_id":"","role_ids":[]}}}`)
		resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/"+id, body)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d", resp.StatusCode)
		}
		if !svc.lastUpdate.UpdateRoles {
			t.Fatalf("expected role updates enabled")
		}
		if len(svc.lastUpdate.RoleID) != 0 {
			t.Fatalf("expected empty primary role id")
		}
		if len(svc.lastUpdate.RoleIDs) != 0 {
			t.Fatalf("expected empty role ids")
		}
	})

	t.Run("success clear all roles with only empty role_id string", func(t *testing.T) {
		roleStaffID := uuidv7.MustBytes()
		rolesRaw := mustRoleRawContact(t, roleStaffID, "Staff", "staff")
		svc := &stubContactService{
			getItem: &contact.ContactListItem{
				RoleCode: "staff",
				RoleName: "Staff",
				RolesRaw: &rolesRaw,
			},
		}
		app := newContactTestApp(NewContactHandler(svc, nil))
		body := []byte(`{"data":{"type":"contact_update","attributes":{"role_id":""}}}`)
		resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/"+id, body)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d", resp.StatusCode)
		}
		if !svc.lastUpdate.UpdateRoles || len(svc.lastUpdate.RoleID) != 0 || len(svc.lastUpdate.RoleIDs) != 0 {
			t.Fatalf("expected clear roles request")
		}
	})

	t.Run("success clear all roles with role_id null string", func(t *testing.T) {
		roleStaffID := uuidv7.MustBytes()
		rolesRaw := mustRoleRawContact(t, roleStaffID, "Staff", "staff")
		svc := &stubContactService{
			getItem: &contact.ContactListItem{
				RoleCode: "staff",
				RoleName: "Staff",
				RolesRaw: &rolesRaw,
			},
		}
		app := newContactTestApp(NewContactHandler(svc, nil))
		body := []byte(`{"data":{"type":"contact_update","attributes":{"role_id":"null"}}}`)
		resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/"+id, body)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d", resp.StatusCode)
		}
		if !svc.lastUpdate.UpdateRoles || len(svc.lastUpdate.RoleID) != 0 || len(svc.lastUpdate.RoleIDs) != 0 {
			t.Fatalf("expected clear roles request")
		}
	})
}

func TestContactHandlerDelete(t *testing.T) {
	t.Run("invalid uuid", func(t *testing.T) {
		app := newContactTestApp(NewContactHandler(&stubContactService{}, nil))
		resp, _ := doJSONRequestContact(t, app, http.MethodDelete, "/contacts/not-uuid", nil)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})

	t.Run("not found", func(t *testing.T) {
		id := mustUUIDStringContact(t, uuidv7.MustBytes())
		svc := &stubContactService{deleteErr: gorm.ErrRecordNotFound}
		app := newContactTestApp(NewContactHandler(svc, nil))
		resp, _ := doJSONRequestContact(t, app, http.MethodDelete, "/contacts/"+id, nil)
		if resp.StatusCode != http.StatusNotFound {
			t.Fatalf("expected 404, got %d", resp.StatusCode)
		}
	})

	t.Run("success", func(t *testing.T) {
		id := mustUUIDStringContact(t, uuidv7.MustBytes())
		app := newContactTestApp(NewContactHandler(&stubContactService{}, nil))
		resp, _ := doJSONRequestContact(t, app, http.MethodDelete, "/contacts/"+id, nil)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d", resp.StatusCode)
		}
	})
}

func TestContactHandlerSimpleMutations(t *testing.T) {
	userID := mustUUIDStringContact(t, uuidv7.MustBytes())
	getItem := &contact.ContactListItem{
		RoleCode:  "technician",
		RoleName:  "Technician",
		Email:     "tech@example.com",
		FirstName: "Tech",
		LastName:  "Nician",
		IsActive:  true,
	}

	tests := []struct {
		name   string
		method string
		path   string
		body   []byte
		assert func(t *testing.T, svc *stubContactService)
	}{
		{
			name:   "status success",
			method: http.MethodPatch,
			path:   "/contacts/" + userID + "/status",
			body:   []byte(`{"data":{"type":"contact_status_update","attributes":{"is_active":true}}}`),
			assert: func(t *testing.T, svc *stubContactService) {
				if !svc.lastIsActive {
					t.Fatalf("expected active flag true")
				}
			},
		},
		{
			name:   "role success",
			method: http.MethodPatch,
			path:   "/contacts/" + userID + "/role",
			body:   []byte(`{"data":{"type":"contact_role_update","attributes":{"role_code":"pilot"}}}`),
			assert: func(t *testing.T, svc *stubContactService) {
				if svc.lastRoleCode != "pilot" {
					t.Fatalf("expected role code recorded")
				}
			},
		},
		{
			name:   "pilot category success",
			method: http.MethodPatch,
			path:   "/contacts/" + userID + "/pilot-category",
			body:   []byte(`{"data":{"type":"contact_pilot_category_update","attributes":{"pilot_category":"freelance"}}}`),
			assert: func(t *testing.T, svc *stubContactService) {
				if svc.lastPilotCategory != "freelance" {
					t.Fatalf("expected pilot category recorded")
				}
			},
		},
		{
			name:   "license success",
			method: http.MethodPatch,
			path:   "/contacts/" + userID + "/license",
			body:   []byte(`{"data":{"type":"contact_license_update","attributes":{"license_no":"LIC"}}}`),
			assert: func(t *testing.T, svc *stubContactService) {
				if svc.lastLicenseNo == nil || *svc.lastLicenseNo != "LIC" {
					t.Fatalf("expected license fields recorded")
				}
			},
		},
		{
			name:   "chief flags success",
			method: http.MethodPatch,
			path:   "/contacts/" + userID + "/chief-flags",
			body:   []byte(`{"data":{"type":"contact_chief_flags_update","attributes":{"is_chief_pilot":true,"is_chief_technician":false}}}`),
			assert: func(t *testing.T, svc *stubContactService) {
				if svc.lastChiefFlags.IsChiefPilot == nil || !*svc.lastChiefFlags.IsChiefPilot {
					t.Fatalf("expected chief pilot flag recorded")
				}
			},
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			svc := &stubContactService{getItem: getItem}
			app := newContactTestApp(NewContactHandler(svc, nil))
			resp, _ := doJSONRequestContact(t, app, tc.method, tc.path, tc.body)
			if resp.StatusCode != http.StatusOK {
				t.Fatalf("expected 200, got %d", resp.StatusCode)
			}
			tc.assert(t, svc)
		})
	}
}

func TestContactHandlerSimpleMutationsErrors(t *testing.T) {
	t.Run("status invalid uuid", func(t *testing.T) {
		app := newContactTestApp(NewContactHandler(&stubContactService{}, nil))
		resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/not-uuid/status", []byte(`{}`))
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})

	t.Run("status service error", func(t *testing.T) {
		id := mustUUIDStringContact(t, uuidv7.MustBytes())
		svc := &stubContactService{updateStatusErr: errors.New("status failed")}
		app := newContactTestApp(NewContactHandler(svc, nil))
		body := []byte(`{"data":{"type":"contact_status_update","attributes":{"is_active":false}}}`)
		resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/"+id+"/status", body)
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})

	t.Run("role invalid json", func(t *testing.T) {
		id := mustUUIDStringContact(t, uuidv7.MustBytes())
		app := newContactTestApp(NewContactHandler(&stubContactService{}, nil))
		resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/"+id+"/role", []byte(`{"data":`))
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})

	t.Run("role validation error", func(t *testing.T) {
		id := mustUUIDStringContact(t, uuidv7.MustBytes())
		app := newContactTestApp(NewContactHandler(&stubContactService{}, nil))
		body := []byte(`{"data":{"type":"contact_role_update","attributes":{"role_code":"wrong"}}}`)
		resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/"+id+"/role", body)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})

	t.Run("role service error", func(t *testing.T) {
		id := mustUUIDStringContact(t, uuidv7.MustBytes())
		svc := &stubContactService{updateRoleErr: errors.New("role failed")}
		app := newContactTestApp(NewContactHandler(svc, nil))
		body := []byte(`{"data":{"type":"contact_role_update","attributes":{"role_code":"pilot"}}}`)
		resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/"+id+"/role", body)
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})

	t.Run("pilot category invalid json", func(t *testing.T) {
		id := mustUUIDStringContact(t, uuidv7.MustBytes())
		app := newContactTestApp(NewContactHandler(&stubContactService{}, nil))
		resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/"+id+"/pilot-category", []byte(`{"data":`))
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})

	t.Run("pilot category validation error", func(t *testing.T) {
		id := mustUUIDStringContact(t, uuidv7.MustBytes())
		app := newContactTestApp(NewContactHandler(&stubContactService{}, nil))
		body := []byte(`{"data":{"type":"contact_pilot_category_update","attributes":{"pilot_category":"wrong"}}}`)
		resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/"+id+"/pilot-category", body)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})

	t.Run("pilot category service error", func(t *testing.T) {
		id := mustUUIDStringContact(t, uuidv7.MustBytes())
		svc := &stubContactService{updatePilotCategoryErr: errors.New("pilot category failed")}
		app := newContactTestApp(NewContactHandler(svc, nil))
		body := []byte(`{"data":{"type":"contact_pilot_category_update","attributes":{"pilot_category":"regular"}}}`)
		resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/"+id+"/pilot-category", body)
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})

	t.Run("license invalid json", func(t *testing.T) {
		id := mustUUIDStringContact(t, uuidv7.MustBytes())
		app := newContactTestApp(NewContactHandler(&stubContactService{}, nil))
		resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/"+id+"/license", []byte(`{"data":`))
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})

	t.Run("license service error", func(t *testing.T) {
		id := mustUUIDStringContact(t, uuidv7.MustBytes())
		svc := &stubContactService{updateLicenseErr: errors.New("license failed")}
		app := newContactTestApp(NewContactHandler(svc, nil))
		body := []byte(`{"data":{"type":"contact_license_update","attributes":{"license_no":"LIC"}}}`)
		resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/"+id+"/license", body)
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})

	t.Run("chief flags invalid json", func(t *testing.T) {
		id := mustUUIDStringContact(t, uuidv7.MustBytes())
		app := newContactTestApp(NewContactHandler(&stubContactService{}, nil))
		resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/"+id+"/chief-flags", []byte(`{"data":`))
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})

	t.Run("chief flags service error", func(t *testing.T) {
		id := mustUUIDStringContact(t, uuidv7.MustBytes())
		svc := &stubContactService{updateChiefFlagsErr: errors.New("chief failed")}
		app := newContactTestApp(NewContactHandler(svc, nil))
		body := []byte(`{"data":{"type":"contact_chief_flags_update","attributes":{"is_chief_physician":true}}}`)
		resp, _ := doJSONRequestContact(t, app, http.MethodPatch, "/contacts/"+id+"/chief-flags", body)
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})

}

func TestContactHandlerBulkUpdate(t *testing.T) {
	t.Run("invalid json", func(t *testing.T) {
		app := newContactTestApp(NewContactHandler(&stubContactService{}, nil))
		resp, _ := doJSONRequestContact(t, app, http.MethodPost, "/contacts/bulk-update", []byte(`{"data":`))
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})

	t.Run("validation error", func(t *testing.T) {
		app := newContactTestApp(NewContactHandler(&stubContactService{}, nil))
		resp, _ := doJSONRequestContact(t, app, http.MethodPost, "/contacts/bulk-update", []byte(`{}`))
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})

	t.Run("invalid uuid", func(t *testing.T) {
		app := newContactTestApp(NewContactHandler(&stubContactService{}, nil))
		body := []byte(`{"data":{"type":"contact_bulk_update","attributes":{"user_ids":["bad"],"is_active":true}}}`)
		resp, _ := doJSONRequestContact(t, app, http.MethodPost, "/contacts/bulk-update", body)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})

	t.Run("service error", func(t *testing.T) {
		id := mustUUIDStringContact(t, uuidv7.MustBytes())
		svc := &stubContactService{bulkUpdateErr: errors.New("bulk failed")}
		app := newContactTestApp(NewContactHandler(svc, nil))
		body := []byte(`{"data":{"type":"contact_bulk_update","attributes":{"user_ids":["` + id + `"],"is_active":false}}}`)
		resp, _ := doJSONRequestContact(t, app, http.MethodPost, "/contacts/bulk-update", body)
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})

	t.Run("success", func(t *testing.T) {
		id1 := mustUUIDStringContact(t, uuidv7.MustBytes())
		id2 := mustUUIDStringContact(t, uuidv7.MustBytes())
		svc := &stubContactService{bulkUpdateCount: 2}
		app := newContactTestApp(NewContactHandler(svc, nil))
		body := []byte(`{"data":{"type":"contact_bulk_update","attributes":{"user_ids":["` + id1 + `","` + id2 + `"],"is_active":true}}}`)
		resp, _ := doJSONRequestContact(t, app, http.MethodPost, "/contacts/bulk-update", body)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d", resp.StatusCode)
		}
		if len(svc.lastBulkIDs) != 2 || !svc.lastBulkIsActive {
			t.Fatalf("expected bulk update recorded")
		}
	})
}

func TestContactHandlerProfilePatchHelpers(t *testing.T) {
	now := time.Now().UTC()
	later := now.Add(24 * time.Hour)

	createPatch := profilePatchFromCreate("pilot", dto.ContactCreateAttributes{
		Pilot: &dto.PilotProfileAttributes{
			PilotCategory:           "regular",
			ShortName:               "P",
			LicenseNo:               "L",
			LicenseIssuedAt:         &now,
			LicenseExpiredAt:        &later,
			TechnicianLicenseNo:     "T",
			HasIFRQualification:     boolPtrContact(true),
			IsChiefPilot:            boolPtrContact(true),
			DULBaseIDs:              []string{},
			TotalFlightMinutes:      intPtrContact(1),
			ResponsiblePilotMinutes: intPtrContact(2),
			SecondPilotMinutes:      intPtrContact(3),
			DoubleCommandMinutes:    intPtrContact(4),
			FlightInstructorMinutes: intPtrContact(5),
			NightFlightMinutes:      intPtrContact(6),
			IFRFlightMinutes:        intPtrContact(7),
			Location:                "Loc",
			Postcode:                "123",
			StreetLine:              "Street",
			WeightKG:                floatPtrContact(70.5),
		},
		Doctor: &dto.DoctorProfileAttributes{
			ShortName:         "D",
			Specialization:    "Spec",
			SubSpecialization: "Sub",
			IsChiefPhysician:  boolPtrContact(true),
			Location:          "DocLoc",
			Postcode:          "456",
			StreetLine:        "DocStreet",
		},
		AirRescuer: &dto.AirRescuerProfileAttributes{
			ShortName:                "A",
			ResponsibleFlightRescuer: boolPtrContact(true),
			Location:                 "AirLoc",
			Postcode:                 "789",
			StreetLine:               "AirStreet",
		},
		Technician: &dto.TechnicianProfileAttributes{
			ShortName:               "T",
			LicenseNo:               "TEC",
			IsChiefTechnician:       boolPtrContact(true),
			IsResponsibleComplaints: boolPtrContact(true),
			Location:                "TechLoc",
			Postcode:                "101",
			StreetLine:              "TechStreet",
		},
		FlightAssistant: &dto.FlightAssistantProfileAttributes{
			ShortName:  "F",
			Location:   "FlightLoc",
			Postcode:   "202",
			StreetLine: "FlightStreet",
		},
		Staff: &dto.StaffProfileAttributes{
			ShortName:  "S",
			RoleLabel:  "Admin",
			Location:   "StaffLoc",
			Postcode:   "303",
			StreetLine: "StaffStreet",
		},
	})
	if createPatch.WeightKG == nil || *createPatch.WeightKG != 70.5 {
		t.Fatalf("expected pilot weight copied")
	}
	if createPatch.ResponsibleFlightRescuer != nil {
		t.Fatalf("expected pilot responsible flight rescuer ignored on create")
	}
	if createPatch.RoleLabel != nil {
		t.Fatalf("expected non-pilot fields to be ignored on create")
	}

	airRescuerPatch := profilePatchFromCreate("air_rescuer", dto.ContactCreateAttributes{
		AirRescuer: &dto.AirRescuerProfileAttributes{
			ShortName:                "Air",
			ResponsibleFlightRescuer: boolPtrContact(true),
		},
	})
	if airRescuerPatch.ResponsibleFlightRescuer == nil || !*airRescuerPatch.ResponsibleFlightRescuer {
		t.Fatalf("expected air rescuer responsible flight rescuer copied on create")
	}

	updatePatch := profilePatchFromUpdate("staff", dto.ContactUpdateAttributes{
		Pilot: &dto.PilotProfileUpdateAttributes{
			PilotCategory: strPtrContact("dry_lease"),
			ShortName:     strPtrContact("Pilot"),
		},
		Doctor: &dto.DoctorProfileUpdateAttributes{},
		AirRescuer: &dto.AirRescuerProfileUpdateAttributes{
			ResponsibleFlightRescuer: boolPtrContact(true),
		},
		Technician: &dto.TechnicianProfileUpdateAttributes{
			IsResponsibleComplaints: boolPtrContact(true),
		},
		FlightAssistant: &dto.FlightAssistantProfileUpdateAttributes{ShortName: strPtrContact("FA")},
		Staff: &dto.StaffProfileUpdateAttributes{
			ShortName: strPtrContact("Staff"),
			RoleLabel: strPtrContact("Coordinator"),
		},
	})
	if updatePatch.ShortName == nil || *updatePatch.ShortName != "Staff" {
		t.Fatalf("expected update profile patch")
	}
	if updatePatch.RoleLabel == nil || *updatePatch.RoleLabel != "Coordinator" {
		t.Fatalf("expected staff fields copied on update")
	}
	if updatePatch.PilotCategory != nil || updatePatch.ResponsibleFlightRescuer != nil || updatePatch.IsResponsibleComplaints != nil {
		t.Fatalf("expected non-staff fields to be ignored on update")
	}

	pilotPatch := contact.ProfilePatch{}
	dulBaseID := mustUUIDStringContact(t, uuidv7.MustBytes())
	fillPatchFromPilot(&pilotPatch, &dto.PilotProfileAttributes{
		DULBase: []dto.DULBase{{ID: dulBaseID, Name: "Base A", Category: "hems"}},
	})
	if !pilotPatch.DULBaseIDsSet || len(pilotPatch.DULBaseIDs) != 1 {
		t.Fatalf("expected dul_base object payload mapped to DUL base ids")
	}

	fillPatchFromPilot(nil, nil)
	fillPatchFromDoctor(nil, nil)
	fillPatchFromAirRescuer(nil, nil)
	fillPatchFromTechnician(nil, nil)
	fillPatchFromFlightAssistant(nil, nil)
	fillPatchFromStaff(nil, nil)
}

func TestContactHandlerHelpers(t *testing.T) {
	if optionalStr("   ") != nil {
		t.Fatalf("expected nil optional string")
	}
	if got := optionalStr("  abc "); got == nil || *got != "abc" {
		t.Fatalf("expected trimmed optional string")
	}
	if got := strPtr("x"); got == nil || *got != "x" {
		t.Fatalf("expected string pointer")
	}
	if got := contactUUIDString([]byte("bad")); got != "" {
		t.Fatalf("expected empty uuid string on invalid bytes")
	}
	if attrs := contactAttributes(nil, false); len(attrs) != 0 {
		t.Fatalf("expected empty attrs for nil item")
	}
	if roles := contactRoles(nil); roles == nil || len(roles) != 0 {
		t.Fatalf("expected empty roles for nil raw")
	}
	roleID := uuidv7.MustBytes()
	roleIDHex := strings.ToLower(hex.EncodeToString(roleID))
	roleNameHex := strings.ToLower(hex.EncodeToString([]byte("Pilot")))
	raw := roleIDHex + "|" + roleNameHex + "|pilot"
	roles := contactRoles(&raw)
	if len(roles) != 1 {
		t.Fatalf("expected one role, got %d", len(roles))
	}
	if roles[0]["role_code"] != "pilot" || roles[0]["role_name"] != "Pilot" {
		t.Fatalf("unexpected role payload: %#v", roles[0])
	}
	if intStatus(http.StatusBadRequest) != "400" || intStatus(http.StatusNotFound) != "404" {
		t.Fatalf("unexpected int status mapping")
	}
}

func TestContactAttributesByRole(t *testing.T) {
	now := time.Now().UTC()
	creator := uuidv7.MustBytes()
	str := "value"
	yes := true
	n := 10
	kg := 80.5

	cases := []contact.ContactListItem{
		{
			CreatedBy:                    creator,
			RoleCode:                     "pilot",
			RoleName:                     "Pilot",
			Email:                        "p@example.com",
			FirstName:                    "P",
			LastName:                     "One",
			PilotCategory:                "regular",
			PilotShortName:               &str,
			PilotLicenseNo:               &str,
			PilotLicenseIssuedAt:         &now,
			PilotLicenseExpiredAt:        &now,
			PilotTechnicianLicenseNo:     &str,
			PilotHasIFRQualification:     &yes,
			PilotIsChiefPilot:            &yes,
			PilotIsDUL:                   &yes,
			PilotTotalFlightMinutes:      &n,
			PilotResponsiblePilotMinutes: &n,
			PilotSecondPilotMinutes:      &n,
			PilotDoubleCommandMinutes:    &n,
			PilotFlightInstructorMinutes: &n,
			PilotNightFlightMinutes:      &n,
			PilotIFRFlightMinutes:        &n,
			PilotLocation:                &str,
			PilotPostcode:                &str,
			PilotStreetLine:              &str,
			PilotWeightKG:                &kg,
			PilotPhotoFileID:             &str,
		},
		{
			CreatedBy:               creator,
			RoleCode:                "doctor",
			RoleName:                "Doctor",
			Email:                   "d@example.com",
			FirstName:               "D",
			LastName:                "One",
			DoctorShortName:         &str,
			DoctorSpecialization:    &str,
			DoctorSubSpecialization: &str,
			DoctorIsChiefPhysician:  &yes,
			DoctorLocation:          &str,
			DoctorPostcode:          &str,
			DoctorStreetLine:        &str,
			DoctorPhotoFileID:       &str,
		},
		{
			CreatedBy:                          creator,
			RoleCode:                           "air_rescuer",
			RoleName:                           "Air Rescuer",
			Email:                              "a@example.com",
			FirstName:                          "A",
			LastName:                           "One",
			AirRescuerShortName:                &str,
			AirRescuerResponsibleFlightRescuer: &yes,
			AirRescuerLocation:                 &str,
			AirRescuerPostcode:                 &str,
			AirRescuerStreetLine:               &str,
			AirRescuerPhotoFileID:              &str,
		},
		{
			CreatedBy:                         creator,
			RoleCode:                          "technician",
			RoleName:                          "Technician",
			Email:                             "t@example.com",
			FirstName:                         "T",
			LastName:                          "One",
			TechnicianShortName:               &str,
			TechnicianLicenseNo:               &str,
			TechnicianIsChiefTechnician:       &yes,
			TechnicianIsResponsibleComplaints: &yes,
			TechnicianLocation:                &str,
			TechnicianPostcode:                &str,
			TechnicianStreetLine:              &str,
			TechnicianPhotoFileID:             &str,
		},
		{
			CreatedBy:                  creator,
			RoleCode:                   "flight_assistant",
			RoleName:                   "Flight Assistant",
			Email:                      "f@example.com",
			FirstName:                  "F",
			LastName:                   "One",
			FlightAssistantShortName:   &str,
			FlightAssistantLocation:    &str,
			FlightAssistantPostcode:    &str,
			FlightAssistantStreetLine:  &str,
			FlightAssistantPhotoFileID: &str,
		},
		{
			CreatedBy:        creator,
			RoleCode:         "staff",
			RoleName:         "Staff",
			Email:            "s@example.com",
			FirstName:        "S",
			LastName:         "One",
			StaffShortName:   &str,
			StaffRoleLabel:   &str,
			StaffLocation:    &str,
			StaffPostcode:    &str,
			StaffStreetLine:  &str,
			StaffPhotoFileID: &str,
		},
	}

	for _, item := range cases {
		attrs := contactAttributes(&item, false)
		if len(attrs) == 0 {
			t.Fatalf("expected attrs for role %s", item.RoleCode)
		}
		if attrs["email"] == nil {
			t.Fatalf("expected base attrs for role %s", item.RoleCode)
		}
		if _, ok := attrs["set_password"].(bool); !ok {
			t.Fatalf("expected set_password bool for role %s", item.RoleCode)
		}
		if attrs["created_by"] == "" {
			t.Fatalf("expected created_by for role %s", item.RoleCode)
		}
		if _, ok := attrs["updated_by"].(string); !ok {
			t.Fatalf("expected updated_by string for role %s", item.RoleCode)
		}
		if _, ok := attrs["created_at"].(string); !ok {
			t.Fatalf("expected created_at string for role %s", item.RoleCode)
		}
		if _, ok := attrs["updated_at"].(string); !ok {
			t.Fatalf("expected updated_at string for role %s", item.RoleCode)
		}
		if item.RoleCode == "pilot" {
			if profile, ok := attrs["pilot"].(map[string]any); ok && profile["responsible_flight_rescuer"] != nil {
				t.Fatalf("expected pilot responsible_flight_rescuer removed from response")
			}
			if _, ok := attrs["license_no"]; ok {
				t.Fatalf("expected top-level license_no removed for pilot")
			}
			if _, ok := attrs["pilot_category"]; ok {
				t.Fatalf("expected top-level pilot_category removed for pilot")
			}
		}
		if item.RoleCode == "doctor" {
			if _, ok := attrs["medical_license"]; ok {
				t.Fatalf("expected top-level medical_license removed for doctor")
			}
			if _, ok := attrs["license_no"]; ok {
				t.Fatalf("expected top-level license_no removed for doctor")
			}
			if _, ok := attrs["pilot_category"]; ok {
				t.Fatalf("expected top-level pilot_category removed for doctor")
			}
		}
		if item.RoleCode == "air_rescuer" {
			profile, ok := attrs["air_rescuer"].(map[string]any)
			if !ok || profile["responsible_flight_rescuer"] == nil {
				t.Fatalf("expected air rescuer responsible_flight_rescuer in response")
			}
			if _, ok := attrs["license_no"]; ok {
				t.Fatalf("expected top-level license_no removed for air_rescuer")
			}
			if _, ok := attrs["medical_license"]; ok {
				t.Fatalf("expected top-level medical_license removed for air_rescuer")
			}
			if _, ok := attrs["pilot_category"]; ok {
				t.Fatalf("expected top-level pilot_category removed for air_rescuer")
			}
		}
		if item.RoleCode == "flight_assistant" {
			if _, ok := attrs["license_no"]; ok {
				t.Fatalf("expected top-level license_no removed for flight_assistant")
			}
			if _, ok := attrs["medical_license"]; ok {
				t.Fatalf("expected top-level medical_license removed for flight_assistant")
			}
			if _, ok := attrs["pilot_category"]; ok {
				t.Fatalf("expected top-level pilot_category removed for flight_assistant")
			}
		}
		if item.RoleCode == "technician" {
			if _, ok := attrs["medical_license"]; ok {
				t.Fatalf("expected top-level medical_license removed for technician")
			}
			if _, ok := attrs["pilot_category"]; ok {
				t.Fatalf("expected top-level pilot_category removed for technician")
			}
		}
		if item.RoleCode == "staff" {
			if _, ok := attrs["license_no"]; ok {
				t.Fatalf("expected top-level license_no removed for staff")
			}
			if _, ok := attrs["medical_license"]; ok {
				t.Fatalf("expected top-level medical_license removed for staff")
			}
			if _, ok := attrs["pilot_category"]; ok {
				t.Fatalf("expected top-level pilot_category removed for staff")
			}
		}
	}

	baseOnly := contactAttributes(&contact.ContactListItem{
		RoleCode:  "pilot",
		RoleName:  "Pilot",
		Email:     "base@example.com",
		FirstName: "Base",
		LastName:  "Only",
		SortKey:   intPtrContactHandler(2),
		Note:      "base-note",
	}, false)
	if baseOnly["sortkey"] == nil || baseOnly["note"] != "base-note" {
		t.Fatalf("expected sortkey and note in base attrs")
	}
	if baseOnly["set_password"] != true {
		t.Fatalf("expected set_password=true when password is empty")
	}
	if _, ok := baseOnly["pilot"]; ok {
		t.Fatalf("expected pilot branch skipped when profile fields nil")
	}

	categoryOnly := contactAttributes(&contact.ContactListItem{
		RoleCode:      "pilot",
		RoleName:      "Pilot",
		Email:         "cat@example.com",
		FirstName:     "Cat",
		LastName:      "Only",
		PilotCategory: "freelance",
	}, false)
	pilotProfile, ok := categoryOnly["pilot"].(map[string]any)
	if !ok {
		t.Fatalf("expected pilot branch included when pilot_category is present")
	}
	if pilotProfile["pilot_category"] != "freelance" {
		t.Fatalf("expected pilot_category propagated, got %#v", pilotProfile["pilot_category"])
	}

	pilotRoleID := uuidv7.MustBytes()
	staffRoleID := uuidv7.MustBytes()
	rolesRaw := strings.ToLower(hex.EncodeToString(pilotRoleID)) + "|" + strings.ToLower(hex.EncodeToString([]byte("Pilot"))) + "|pilot," +
		strings.ToLower(hex.EncodeToString(staffRoleID)) + "|" + strings.ToLower(hex.EncodeToString([]byte("Staff"))) + "|staff"
	multiRole := contactAttributes(&contact.ContactListItem{
		RoleCode:        "staff",
		RoleName:        "staff",
		RolesRaw:        &rolesRaw,
		Email:           "multi@example.com",
		FirstName:       "Multi",
		LastName:        "Role",
		PilotCategory:   "regular",
		PilotShortName:  strPtrContact("Pilot"),
		StaffShortName:  strPtrContact("Staff"),
		StaffRoleLabel:  strPtrContact("Ops"),
		StaffLocation:   strPtrContact("Jakarta"),
		StaffPostcode:   strPtrContact("12950"),
		StaffStreetLine: strPtrContact("Jl"),
	}, false)
	if _, ok := multiRole["pilot"]; !ok {
		t.Fatalf("expected pilot profile in multi-role payload")
	}
	if _, ok := multiRole["staff"]; !ok {
		t.Fatalf("expected staff profile in multi-role payload")
	}

	pilotTechRolesRaw := strings.ToLower(hex.EncodeToString(pilotRoleID)) + "|" + strings.ToLower(hex.EncodeToString([]byte("Pilot"))) + "|pilot," +
		strings.ToLower(hex.EncodeToString(staffRoleID)) + "|" + strings.ToLower(hex.EncodeToString([]byte("Technician"))) + "|technician"
	pilotTechnician := contactAttributes(&contact.ContactListItem{
		RoleCode:                          "pilot",
		RoleName:                          "pilot",
		RolesRaw:                          &pilotTechRolesRaw,
		Email:                             "pilot-tech@example.com",
		FirstName:                         "Pilot",
		LastName:                          "Tech",
		PilotCategory:                     "regular",
		PilotShortName:                    strPtrContact("Pilot"),
		TechnicianShortName:               strPtrContact("Tech"),
		TechnicianLicenseNo:               strPtrContact("TECH-1"),
		TechnicianIsChiefTechnician:       boolPtrContact(true),
		TechnicianIsResponsibleComplaints: boolPtrContact(true),
	}, false)
	if _, ok := pilotTechnician["pilot"]; !ok {
		t.Fatalf("expected pilot profile in pilot+technician payload")
	}
	if _, ok := pilotTechnician["technician"]; !ok {
		t.Fatalf("expected technician profile in pilot+technician payload")
	}
}

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