package mysql

import (
	"context"
	"errors"
	"fmt"
	"strings"
	"testing"
	"time"

	"gorm.io/gorm"

	"wucher/internal/domain/auth"
	"wucher/internal/domain/contact"
	"wucher/internal/shared/pkg/appctx"
	"wucher/internal/shared/pkg/uuidv7"
)

func TestContactDetailSelectSQL_IncludesProfileAttachmentID(t *testing.T) {
	sql := contactDetailSelectSQL()
	if !strings.Contains(sql, "users.profile_attachment_id AS profile_attachment_id") {
		t.Fatalf("expected detail select to include users.profile_attachment_id alias")
	}
}

func TestContactListSelectSQL_IncludesProfileAttachmentID(t *testing.T) {
	sql := contactListSelectSQL()
	if !strings.Contains(sql, "users.profile_attachment_id AS profile_attachment_id") {
		t.Fatalf("expected list select to include users.profile_attachment_id alias")
	}
}

func TestContactRepositoryUpdateContactClearSSOEmailUnlinksMicrosoftIdentity(t *testing.T) {
	db := openDBTestSQLite(t)
	if err := AutoMigrate(db); err != nil {
		t.Fatalf("auto migrate: %v", err)
	}
	repo := NewContactRepository(db)

	adminRoleID := uuidv7.MustBytes()
	pilotRoleID := uuidv7.MustBytes()
	if err := db.Create(&auth.Role{ID: adminRoleID, Code: "admin", Name: "Admin"}).Error; err != nil {
		t.Fatalf("create admin role: %v", err)
	}
	if err := db.Create(&auth.Role{ID: pilotRoleID, Code: auth.RoleCodePilot, Name: "Pilot"}).Error; err != nil {
		t.Fatalf("create pilot role: %v", err)
	}

	adminUser := &auth.User{
		ID:        uuidv7.MustBytes(),
		Email:     "admin3@example.com",
		FirstName: "Admin",
		LastName:  "User",
		Timezone:  "UTC",
		RoleID:    adminRoleID,
		IsActive:  true,
	}
	if err := db.Create(adminUser).Error; err != nil {
		t.Fatalf("create admin user: %v", err)
	}
	ctx := appctx.WithUserID(context.Background(), adminUser.ID)

	sso := "a@microsoft.com"
	target := &auth.User{
		ID:        uuidv7.MustBytes(),
		Email:     "target@example.com",
		SSOEmail:  &sso,
		FirstName: "Target",
		LastName:  "User",
		Timezone:  "UTC",
		RoleID:    pilotRoleID,
		IsActive:  true,
	}
	if err := db.Create(target).Error; err != nil {
		t.Fatalf("create target user: %v", err)
	}
	if err := db.Create(&auth.UserIdentity{
		ID:              uuidv7.MustBytes(),
		UserID:          target.ID,
		Provider:        "microsoft",
		ProviderSubject: "subject-a",
		Email:           "a@microsoft.com",
	}).Error; err != nil {
		t.Fatalf("create identity: %v", err)
	}

	empty := "   "
	if err := repo.UpdateContact(ctx, contact.UpdateInput{
		UserID: target.ID,
		User: contact.UserPatch{
			SSOEmail: &empty,
		},
	}); err != nil {
		t.Fatalf("update contact clear sso email: %v", err)
	}

	var refreshed auth.User
	if err := db.Where("id = ?", target.ID).Take(&refreshed).Error; err != nil {
		t.Fatalf("reload user: %v", err)
	}
	if refreshed.SSOEmail != nil {
		t.Fatalf("expected user sso_email NULL, got %v", *refreshed.SSOEmail)
	}

	var count int64
	if err := db.Table("user_identities").
		Where("user_id = ? AND provider = ?", target.ID, "microsoft").
		Count(&count).Error; err != nil {
		t.Fatalf("count identities: %v", err)
	}
	if count != 0 {
		t.Fatalf("expected microsoft identity unlinked, got %d rows", count)
	}
}

func TestContactRepositoryDeleteContactRemovesUserAndProfiles(t *testing.T) {
	db := openDBTestSQLite(t)
	if err := AutoMigrate(db); err != nil {
		t.Fatalf("auto migrate: %v", err)
	}

	repo := NewContactRepository(db)
	roleID := uuidv7.MustBytes()
	if err := db.Create(&auth.Role{ID: roleID, Code: auth.RoleCodePilot, Name: "Pilot"}).Error; err != nil {
		t.Fatalf("create role: %v", err)
	}

	username := "pilot.delete"
	user := &auth.User{
		ID:        uuidv7.MustBytes(),
		Email:     "pilot-delete@example.com",
		Username:  &username,
		FirstName: "Pilot",
		LastName:  "Delete",
		Timezone:  "Asia/Jakarta",
		RoleID:    roleID,
		IsActive:  true,
		CreatedAt: time.Now().UTC(),
		UpdatedAt: time.Now().UTC(),
	}
	if err := db.Create(user).Error; err != nil {
		t.Fatalf("create user: %v", err)
	}
	if err := db.Create(&auth.PilotProfile{
		UserID:        user.ID,
		PilotCategory: auth.PilotCategoryRegular,
		ShortName:     "PD",
	}).Error; err != nil {
		t.Fatalf("create pilot profile: %v", err)
	}
	if err := db.Create(&auth.DoctorProfile{
		UserID:    user.ID,
		ShortName: "Dr PD",
		CreatedAt: time.Now().UTC(),
		UpdatedAt: time.Now().UTC(),
	}).Error; err != nil {
		t.Fatalf("create extra profile: %v", err)
	}

	if err := repo.DeleteContact(context.Background(), user.ID); err != nil {
		t.Fatalf("delete contact: %v", err)
	}

	var userCount int64
	if err := db.Table("users").Where("id = ?", user.ID).Count(&userCount).Error; err != nil {
		t.Fatalf("count users: %v", err)
	}
	if userCount != 0 {
		t.Fatalf("expected user deleted, got %d rows", userCount)
	}

	for _, table := range contactProfileTables {
		var count int64
		if err := db.Table(table).Where("user_id = ?", user.ID).Count(&count).Error; err != nil {
			t.Fatalf("count %s: %v", table, err)
		}
		if count != 0 {
			t.Fatalf("expected %s deleted, got %d rows", table, count)
		}
	}
}

func TestContactRepositoryDeleteContactBlockedWhenUsedByBase(t *testing.T) {
	db := openDBTestSQLite(t)
	if err := AutoMigrate(db); err != nil {
		t.Fatalf("auto migrate: %v", err)
	}
	roleID := uuidv7.MustBytes()
	if err := db.Create(&auth.Role{ID: roleID, Code: auth.RoleCodePilot, Name: "Pilot"}).Error; err != nil {
		t.Fatalf("create role: %v", err)
	}

	user := &auth.User{
		ID:        uuidv7.MustBytes(),
		Email:     "pilot-base-ref@example.com",
		FirstName: "Pilot",
		LastName:  "Ref",
		Timezone:  "UTC",
		RoleID:    roleID,
		IsActive:  true,
	}
	if err := db.Create(user).Error; err != nil {
		t.Fatalf("create user: %v", err)
	}

	if err := db.Exec(`CREATE TABLE IF NOT EXISTS base_contact_roles (
		id BLOB PRIMARY KEY,
		base_id BLOB NOT NULL,
		contact_id BLOB NOT NULL,
		role_code TEXT NOT NULL
	)`).Error; err != nil {
		t.Fatalf("create base_contact_roles: %v", err)
	}
	repo := NewContactRepository(db)
	if err := db.Table("base_contact_roles").Create(map[string]any{
		"id":         uuidv7.MustBytes(),
		"base_id":    uuidv7.MustBytes(),
		"contact_id": user.ID,
		"role_code":  "responsible_flight_rescuer",
	}).Error; err != nil {
		t.Fatalf("seed base_contact_roles: %v", err)
	}

	err := repo.DeleteContact(context.Background(), user.ID)
	if err == nil {
		t.Fatalf("expected delete to be blocked")
	}
	if !strings.Contains(strings.ToLower(err.Error()), "bases") {
		t.Fatalf("expected bases dependency in error, got: %v", err)
	}
}

func TestContactRepositoryUpdateContactSameValuesDoesNotReturnNotFound(t *testing.T) {
	db := openDBTestSQLite(t)
	if err := AutoMigrate(db); err != nil {
		t.Fatalf("auto migrate: %v", err)
	}

	repo := NewContactRepository(db)
	roleID := uuidv7.MustBytes()
	if err := db.Create(&auth.Role{ID: roleID, Code: auth.RoleCodePilot, Name: "Pilot"}).Error; err != nil {
		t.Fatalf("create role: %v", err)
	}

	issuedAt := time.Date(2004, 10, 3, 0, 0, 0, 0, time.UTC)
	expiredAt := time.Date(2029, 3, 27, 0, 0, 0, 0, time.UTC)
	weight := 0.0
	username := "pilot_test_2"
	user := &auth.User{
		ID:          uuidv7.MustBytes(),
		Email:       "pilot2@test.com",
		Username:    &username,
		FirstName:   "a",
		LastName:    "a",
		MobilePhone: "2",
		Timezone:    "Asia/Jakarta",
		RoleID:      roleID,
		IsActive:    true,
		CreatedAt:   time.Now().UTC(),
		UpdatedAt:   time.Now().UTC(),
	}
	if err := db.Create(user).Error; err != nil {
		t.Fatalf("create user: %v", err)
	}
	if err := db.Create(&auth.PilotProfile{
		UserID:                  user.ID,
		PilotCategory:           auth.PilotCategoryRegular,
		ShortName:               "a",
		LicenseNo:               "a",
		LicenseIssuedAt:         &issuedAt,
		LicenseExpiredAt:        &expiredAt,
		TechnicianLicenseNo:     "",
		HasIFRQualification:     true,
		IsChiefPilot:            false,
		IsDUL:                   true,
		TotalFlightMinutes:      2,
		ResponsiblePilotMinutes: 2,
		SecondPilotMinutes:      2,
		DoubleCommandMinutes:    2,
		FlightInstructorMinutes: 0,
		NightFlightMinutes:      2,
		IFRFlightMinutes:        2,
		Location:                "string",
		Postcode:                "2",
		StreetLine:              "2",
		WeightKG:                &weight,
		PhotoFileID:             "a",
		CreatedAt:               time.Now().UTC(),
		UpdatedAt:               time.Now().UTC(),
	}).Error; err != nil {
		t.Fatalf("create pilot profile: %v", err)
	}

	email := "pilot2@test.com"
	firstName := "a"
	lastName := "a"
	mobilePhone := "2"
	timezone := "Asia/Jakarta"
	isActive := true
	pilotCategory := auth.PilotCategoryRegular
	shortName := "a"
	licenseNo := "a"
	technicianLicenseNo := ""
	hasIFRQualification := true
	isChiefPilot := false
	isDUL := true
	totalFlightMinutes := 2
	responsiblePilotMinutes := 2
	secondPilotMinutes := 2
	doubleCommandMinutes := 2
	flightInstructorMinutes := 0
	nightFlightMinutes := 2
	ifrFlightMinutes := 2
	location := "string"
	postcode := "2"
	streetLine := "2"
	photoFileID := "a"

	err := repo.UpdateContact(context.Background(), contact.UpdateInput{
		UserID: user.ID,
		User: contact.UserPatch{
			Email:       &email,
			Username:    &username,
			FirstName:   &firstName,
			LastName:    &lastName,
			MobilePhone: &mobilePhone,
			Timezone:    &timezone,
			IsActive:    &isActive,
		},
		Profile: contact.ProfilePatch{
			PilotCategory:           &pilotCategory,
			ShortName:               &shortName,
			LicenseNo:               &licenseNo,
			LicenseIssuedAt:         &issuedAt,
			LicenseExpiredAt:        &expiredAt,
			TechnicianLicenseNo:     &technicianLicenseNo,
			HasIFRQualification:     &hasIFRQualification,
			IsChiefPilot:            &isChiefPilot,
			IsDUL:                   &isDUL,
			TotalFlightMinutes:      &totalFlightMinutes,
			ResponsiblePilotMinutes: &responsiblePilotMinutes,
			SecondPilotMinutes:      &secondPilotMinutes,
			DoubleCommandMinutes:    &doubleCommandMinutes,
			FlightInstructorMinutes: &flightInstructorMinutes,
			NightFlightMinutes:      &nightFlightMinutes,
			IFRFlightMinutes:        &ifrFlightMinutes,
			Location:                &location,
			Postcode:                &postcode,
			StreetLine:              &streetLine,
			WeightKG:                &weight,
			PhotoFileID:             &photoFileID,
		},
	})
	if err != nil {
		t.Fatalf("update contact with same values: %v", err)
	}

	var pilotProfileCount int64
	if err := db.Table("pilot_profiles").Where("user_id = ?", user.ID).Count(&pilotProfileCount).Error; err != nil {
		t.Fatalf("count pilot profiles: %v", err)
	}
	if pilotProfileCount != 1 {
		t.Fatalf("expected single pilot profile row after update, got %d", pilotProfileCount)
	}

	got, err := repo.GetContactByID(context.Background(), user.ID)
	if err != nil {
		t.Fatalf("get contact after update: %v", err)
	}
	if got == nil || got.RoleCode != auth.RoleCodePilot {
		t.Fatalf("expected pilot contact after update, got %#v", got)
	}
}

func TestContactRepositoryUpdateContactClearsAirRescuerProfileStringFields(t *testing.T) {
	db := openDBTestSQLite(t)
	if err := AutoMigrate(db); err != nil {
		t.Fatalf("auto migrate: %v", err)
	}

	repo := NewContactRepository(db)
	roleID := uuidv7.MustBytes()
	if err := db.Create(&auth.Role{ID: roleID, Code: auth.RoleCodeAirRescuer, Name: "Air Rescuer"}).Error; err != nil {
		t.Fatalf("create role: %v", err)
	}

	user := &auth.User{
		ID:        uuidv7.MustBytes(),
		Email:     "air-rescuer@test.com",
		FirstName: "Air",
		LastName:  "Rescuer",
		Timezone:  "UTC",
		RoleID:    roleID,
		IsActive:  true,
	}
	if err := db.Create(user).Error; err != nil {
		t.Fatalf("create user: %v", err)
	}
	if err := db.Create(&auth.AirRescuerProfile{
		UserID:                   user.ID,
		ShortName:                "CINDY",
		Location:                 "12",
		Postcode:                 "12",
		StreetLine:               "12",
		Note:                     "12",
		ResponsibleFlightRescuer: false,
		CreatedAt:                time.Now().UTC(),
		UpdatedAt:                time.Now().UTC(),
	}).Error; err != nil {
		t.Fatalf("create air rescuer profile: %v", err)
	}

	empty := ""
	if err := repo.UpdateContact(context.Background(), contact.UpdateInput{
		UserID: user.ID,
		Profile: contact.ProfilePatch{
			ShortName:  &empty,
			Location:   &empty,
			Postcode:   &empty,
			StreetLine: &empty,
			Note:       &empty,
		},
	}); err != nil {
		t.Fatalf("update contact clear profile strings: %v", err)
	}

	var profile auth.AirRescuerProfile
	if err := db.Where("user_id = ?", user.ID).Take(&profile).Error; err != nil {
		t.Fatalf("reload air rescuer profile: %v", err)
	}
	if profile.ShortName != "" || profile.Location != "" || profile.Postcode != "" || profile.StreetLine != "" || profile.Note != "" {
		t.Fatalf("expected cleared string fields, got %#v", profile)
	}
}

func TestContactRepositoryCreateContactRejectsDuplicateUsername(t *testing.T) {
	db := openDBTestSQLite(t)
	if err := AutoMigrate(db); err != nil {
		t.Fatalf("auto migrate: %v", err)
	}

	repo := NewContactRepository(db)
	roleID := uuidv7.MustBytes()
	if err := db.Create(&auth.Role{ID: roleID, Code: auth.RoleCodePilot, Name: "Pilot"}).Error; err != nil {
		t.Fatalf("create role: %v", err)
	}

	username := "pilot.duplicate"
	first, err := repo.CreateContact(context.Background(), contact.CreateInput{
		RoleID: roleID,
		User: contact.UserPatch{
			Email:     strPtrContactRepo("first@example.com"),
			Username:  &username,
			FirstName: strPtrContactRepo("First"),
			LastName:  strPtrContactRepo("User"),
		},
	})
	if err != nil {
		t.Fatalf("create first contact: %v", err)
	}
	if len(first) != 16 {
		t.Fatalf("expected first user id, got len=%d", len(first))
	}

	secondUsername := "pilot.duplicate"
	_, err = repo.CreateContact(context.Background(), contact.CreateInput{
		RoleID: roleID,
		User: contact.UserPatch{
			Email:     strPtrContactRepo("second@example.com"),
			Username:  &secondUsername,
			FirstName: strPtrContactRepo("Second"),
			LastName:  strPtrContactRepo("User"),
		},
	})
	if !errors.Is(err, contact.ErrUsernameAlreadyExists) {
		t.Fatalf("expected duplicate username error, got %v", err)
	}
}

func TestContactRepositoryUpdateContactRejectsDuplicateUsername(t *testing.T) {
	db := openDBTestSQLite(t)
	if err := AutoMigrate(db); err != nil {
		t.Fatalf("auto migrate: %v", err)
	}

	repo := NewContactRepository(db)
	roleID := uuidv7.MustBytes()
	if err := db.Create(&auth.Role{ID: roleID, Code: auth.RoleCodePilot, Name: "Pilot"}).Error; err != nil {
		t.Fatalf("create role: %v", err)
	}

	firstUsername := "pilot.update.1"
	firstID, err := repo.CreateContact(context.Background(), contact.CreateInput{
		RoleID: roleID,
		User: contact.UserPatch{
			Email:     strPtrContactRepo("first-update@example.com"),
			Username:  &firstUsername,
			FirstName: strPtrContactRepo("First"),
			LastName:  strPtrContactRepo("User"),
		},
	})
	if err != nil {
		t.Fatalf("create first contact: %v", err)
	}

	secondUsername := "pilot.update.2"
	secondID, err := repo.CreateContact(context.Background(), contact.CreateInput{
		RoleID: roleID,
		User: contact.UserPatch{
			Email:     strPtrContactRepo("second-update@example.com"),
			Username:  &secondUsername,
			FirstName: strPtrContactRepo("Second"),
			LastName:  strPtrContactRepo("User"),
		},
	})
	if err != nil {
		t.Fatalf("create second contact: %v", err)
	}

	newUsername := firstUsername
	err = repo.UpdateContact(context.Background(), contact.UpdateInput{
		UserID: secondID,
		User: contact.UserPatch{
			Username: &newUsername,
		},
	})
	if !errors.Is(err, contact.ErrUsernameAlreadyExists) {
		t.Fatalf("expected duplicate username error on update, got %v", err)
	}

	got, err := repo.GetContactByID(context.Background(), firstID)
	if err != nil {
		t.Fatalf("get first contact: %v", err)
	}
	if got == nil || got.Username != firstUsername {
		t.Fatalf("unexpected first contact after duplicate update: %#v", got)
	}
}

func TestContactRepositoryListContactsDefaultSortByActiveAndSortKey(t *testing.T) {
	db := openDBTestSQLite(t)
	if err := AutoMigrate(db); err != nil {
		t.Fatalf("auto migrate: %v", err)
	}

	repo := NewContactRepository(db)
	roleID := uuidv7.MustBytes()
	if err := db.Create(&auth.Role{ID: roleID, Code: auth.RoleCodeStaff, Name: "Staff"}).Error; err != nil {
		t.Fatalf("create role: %v", err)
	}

	seedContactStaffUser(t, db, roleID, "Gamma", nil, true, "g-note")
	seedContactStaffUser(t, db, roleID, "Beta", intPtrContactRepo(0), true, "b-note")
	seedContactStaffUser(t, db, roleID, "Charlie", intPtrContactRepo(2), true, "c-note")
	seedContactStaffUser(t, db, roleID, "Alpha", intPtrContactRepo(1), true, "a-note")
	seedContactStaffUser(t, db, roleID, "Zulu", nil, false, "z-note")
	seedContactStaffUser(t, db, roleID, "Bravo", intPtrContactRepo(9), false, "br-note")

	rows, total, err := repo.ListContacts(context.Background(), contact.ListFilter{})
	if err != nil {
		t.Fatalf("list contacts: %v", err)
	}
	if total != 6 || len(rows) != 6 {
		t.Fatalf("unexpected total/len total=%d len=%d", total, len(rows))
	}

	got := []string{rows[0].FirstName, rows[1].FirstName, rows[2].FirstName, rows[3].FirstName, rows[4].FirstName, rows[5].FirstName}
	want := []string{"Beta", "Alpha", "Charlie", "Gamma", "Bravo", "Zulu"}
	for i := range want {
		if got[i] != want[i] {
			t.Fatalf("unexpected order got=%v want=%v", got, want)
		}
	}
}

func TestContactRepositoryListContactsSearchByNote(t *testing.T) {
	db := openDBTestSQLite(t)
	if err := AutoMigrate(db); err != nil {
		t.Fatalf("auto migrate: %v", err)
	}

	repo := NewContactRepository(db)
	roleID := uuidv7.MustBytes()
	if err := db.Create(&auth.Role{ID: roleID, Code: auth.RoleCodeStaff, Name: "Staff"}).Error; err != nil {
		t.Fatalf("create role: %v", err)
	}

	seedContactStaffUser(t, db, roleID, "Alpha", nil, true, "dispatcher")
	seedContactStaffUser(t, db, roleID, "Bravo", nil, true, "maintenance")

	rows, total, err := repo.ListContacts(context.Background(), contact.ListFilter{Search: "dispatch"})
	if err != nil {
		t.Fatalf("list contacts: %v", err)
	}
	if total != 1 || len(rows) != 1 {
		t.Fatalf("unexpected total/len total=%d len=%d", total, len(rows))
	}
	if rows[0].FirstName != "Alpha" || rows[0].Note != "dispatcher" {
		t.Fatalf("unexpected row %+v", rows[0])
	}
}

func TestContactRepositoryListContactsIncludesPilotProfileFields(t *testing.T) {
	db := openDBTestSQLite(t)
	if err := AutoMigrate(db); err != nil {
		t.Fatalf("auto migrate: %v", err)
	}

	repo := NewContactRepository(db)
	roleID := uuidv7.MustBytes()
	if err := db.Create(&auth.Role{ID: roleID, Code: auth.RoleCodePilot, Name: "Pilot"}).Error; err != nil {
		t.Fatalf("create role: %v", err)
	}

	userID := uuidv7.MustBytes()
	username := "pilot_list_detail"
	issuedAt := time.Date(2026, 4, 20, 0, 0, 0, 0, time.UTC)
	expiredAt := time.Date(2029, 4, 20, 0, 0, 0, 0, time.UTC)
	user := &auth.User{
		ID:        userID,
		Email:     "pilot.list@example.com",
		Username:  &username,
		FirstName: "Pilot",
		LastName:  "List",
		Timezone:  "UTC",
		RoleID:    roleID,
		IsActive:  true,
		CreatedAt: time.Now().UTC(),
		UpdatedAt: time.Now().UTC(),
	}
	if err := db.Create(user).Error; err != nil {
		t.Fatalf("create user: %v", err)
	}
	if err := db.Create(&auth.PilotProfile{
		UserID:           userID,
		PilotCategory:    auth.PilotCategoryFreelance,
		LicenseIssuedAt:  &issuedAt,
		LicenseExpiredAt: &expiredAt,
		CreatedAt:        time.Now().UTC(),
		UpdatedAt:        time.Now().UTC(),
	}).Error; err != nil {
		t.Fatalf("create pilot profile: %v", err)
	}

	rows, total, err := repo.ListContacts(context.Background(), contact.ListFilter{Role: auth.RoleCodePilot})
	if err != nil {
		t.Fatalf("list contacts: %v", err)
	}
	if total != 1 || len(rows) != 1 {
		t.Fatalf("unexpected total/len total=%d len=%d", total, len(rows))
	}
	if rows[0].PilotLicenseIssuedAt == nil || !rows[0].PilotLicenseIssuedAt.Equal(issuedAt) {
		t.Fatalf("expected pilot license_issued_at returned, got %#v", rows[0].PilotLicenseIssuedAt)
	}
	if rows[0].PilotLicenseExpiredAt == nil || !rows[0].PilotLicenseExpiredAt.Equal(expiredAt) {
		t.Fatalf("expected pilot license_expired_at returned, got %#v", rows[0].PilotLicenseExpiredAt)
	}
	if rows[0].PilotCategory != auth.PilotCategoryFreelance {
		t.Fatalf("expected pilot category returned, got %q", rows[0].PilotCategory)
	}
}

func TestContactRepositoryUpdateContactRoleAssignsRoleWhenCurrentRoleIsEmpty(t *testing.T) {
	db := openDBTestSQLite(t)
	if err := AutoMigrate(db); err != nil {
		t.Fatalf("auto migrate: %v", err)
	}

	repo := NewContactRepository(db)
	staffRoleID := uuidv7.MustBytes()
	if err := db.Create(&auth.Role{ID: staffRoleID, Code: auth.RoleCodeStaff, Name: "Staff"}).Error; err != nil {
		t.Fatalf("create role: %v", err)
	}

	userID := uuidv7.MustBytes()
	user := &auth.User{
		ID:        userID,
		Email:     "no-role@example.com",
		FirstName: "No",
		LastName:  "Role",
		Timezone:  "UTC",
		IsActive:  true,
	}
	if err := db.Create(user).Error; err != nil {
		t.Fatalf("create user: %v", err)
	}

	if err := repo.UpdateContactRole(context.Background(), userID, auth.RoleCodeStaff); err != nil {
		t.Fatalf("update contact role: %v", err)
	}

	var refreshed auth.User
	if err := db.Where("id = ?", userID).Take(&refreshed).Error; err != nil {
		t.Fatalf("reload user: %v", err)
	}
	if len(refreshed.RoleID) != 16 || string(refreshed.RoleID) != string(staffRoleID) {
		t.Fatalf("expected role_id updated to staff, got %x", refreshed.RoleID)
	}

	roleIDs, err := listUserRoleIDsTx(db, userID)
	if err != nil {
		t.Fatalf("list user roles: %v", err)
	}
	if len(roleIDs) != 1 || string(roleIDs[0]) != string(staffRoleID) {
		t.Fatalf("expected user_roles to contain only staff role, got %d rows", len(roleIDs))
	}
}

func TestContactRepositoryUpdateContact_IsAdminTruePersistsAdminRoleWhenUpdatingRoles(t *testing.T) {
	db := openDBTestSQLite(t)
	if err := AutoMigrate(db); err != nil {
		t.Fatalf("auto migrate: %v", err)
	}

	repo := NewContactRepository(db)
	adminRoleID := uuidv7.MustBytes()
	pilotRoleID := uuidv7.MustBytes()
	if err := db.Create(&auth.Role{ID: adminRoleID, Code: "admin", Name: "Admin"}).Error; err != nil {
		t.Fatalf("create admin role: %v", err)
	}
	if err := db.Create(&auth.Role{ID: pilotRoleID, Code: auth.RoleCodePilot, Name: "Pilot"}).Error; err != nil {
		t.Fatalf("create pilot role: %v", err)
	}

	user := &auth.User{
		ID:        uuidv7.MustBytes(),
		Email:     "admin-sync@example.com",
		FirstName: "Admin",
		LastName:  "Sync",
		Timezone:  "UTC",
		RoleID:    pilotRoleID,
		IsActive:  true,
	}
	if err := db.Create(user).Error; err != nil {
		t.Fatalf("create user: %v", err)
	}
	if err := replaceUserRolesTx(context.Background(), db, user.ID, pilotRoleID, [][]byte{pilotRoleID}); err != nil {
		t.Fatalf("seed user roles: %v", err)
	}

	trueVal := true
	if err := repo.UpdateContact(context.Background(), contact.UpdateInput{
		UserID:      user.ID,
		UpdateRoles: true,
		RoleID:      pilotRoleID,
		RoleIDs:     [][]byte{pilotRoleID},
		User: contact.UserPatch{
			IsAdmin: &trueVal,
		},
	}); err != nil {
		t.Fatalf("update contact: %v", err)
	}

	roleIDs, err := listUserRoleIDsTx(db, user.ID)
	if err != nil {
		t.Fatalf("list user roles: %v", err)
	}
	if len(roleIDs) != 2 {
		t.Fatalf("expected 2 roles (pilot + admin), got %d", len(roleIDs))
	}
	foundAdmin := false
	for i := range roleIDs {
		if string(roleIDs[i]) == string(adminRoleID) {
			foundAdmin = true
			break
		}
	}
	if !foundAdmin {
		t.Fatalf("expected admin role assigned")
	}
}

func TestContactRepositoryUpdateContact_StaffCanBeAdmin(t *testing.T) {
	db := openDBTestSQLite(t)
	if err := AutoMigrate(db); err != nil {
		t.Fatalf("auto migrate: %v", err)
	}

	repo := NewContactRepository(db)
	adminRoleID := uuidv7.MustBytes()
	staffRoleID := uuidv7.MustBytes()
	if err := db.Create(&auth.Role{ID: adminRoleID, Code: "admin", Name: "Admin"}).Error; err != nil {
		t.Fatalf("create admin role: %v", err)
	}
	if err := db.Create(&auth.Role{ID: staffRoleID, Code: auth.RoleCodeStaff, Name: "Staff"}).Error; err != nil {
		t.Fatalf("create staff role: %v", err)
	}

	user := &auth.User{
		ID:        uuidv7.MustBytes(),
		Email:     "staff-admin@example.com",
		FirstName: "Staff",
		LastName:  "Admin",
		Timezone:  "UTC",
		RoleID:    staffRoleID,
		IsActive:  true,
	}
	if err := db.Create(user).Error; err != nil {
		t.Fatalf("create user: %v", err)
	}
	if err := replaceUserRolesTx(context.Background(), db, user.ID, staffRoleID, [][]byte{staffRoleID}); err != nil {
		t.Fatalf("seed user roles: %v", err)
	}

	trueVal := true
	if err := repo.UpdateContact(context.Background(), contact.UpdateInput{
		UserID: user.ID,
		User: contact.UserPatch{
			IsAdmin: &trueVal,
		},
	}); err != nil {
		t.Fatalf("update contact: %v", err)
	}

	roleIDs, err := listUserRoleIDsTx(db, user.ID)
	if err != nil {
		t.Fatalf("list user roles: %v", err)
	}
	if len(roleIDs) != 2 {
		t.Fatalf("expected 2 roles (staff + admin), got %d", len(roleIDs))
	}
	foundAdmin := false
	for i := range roleIDs {
		if string(roleIDs[i]) == string(adminRoleID) {
			foundAdmin = true
			break
		}
	}
	if !foundAdmin {
		t.Fatalf("expected admin role assigned")
	}
}

func TestContactRepositoryUpdateContactNilSortKeyDoesNotClearWithoutSetFlag(t *testing.T) {
	db := openDBTestSQLite(t)
	if err := AutoMigrate(db); err != nil {
		t.Fatalf("auto migrate: %v", err)
	}

	repo := NewContactRepository(db)
	roleID := uuidv7.MustBytes()
	if err := db.Create(&auth.Role{ID: roleID, Code: auth.RoleCodeStaff, Name: "Staff"}).Error; err != nil {
		t.Fatalf("create role: %v", err)
	}

	sortKey := 7
	user := &auth.User{
		ID:        uuidv7.MustBytes(),
		Email:     "sortkey-clear@example.com",
		FirstName: "Sort",
		LastName:  "Key",
		Timezone:  "UTC",
		RoleID:    roleID,
		SortKey:   &sortKey,
		IsActive:  true,
	}
	if err := db.Create(user).Error; err != nil {
		t.Fatalf("create user: %v", err)
	}

	firstName := "Sorted"
	if err := repo.UpdateContact(context.Background(), contact.UpdateInput{
		UserID: user.ID,
		User: contact.UserPatch{
			FirstName:  &firstName,
			SortKey:    nil,
			SortKeySet: false,
		},
	}); err != nil {
		t.Fatalf("update contact: %v", err)
	}

	var refreshed auth.User
	if err := db.Where("id = ?", user.ID).Take(&refreshed).Error; err != nil {
		t.Fatalf("reload user: %v", err)
	}
	if refreshed.SortKey == nil || *refreshed.SortKey != 7 {
		t.Fatalf("expected sortkey to remain 7, got %#v", refreshed.SortKey)
	}
}

func TestContactRepositoryUpdateContactNullSortKeyClearsValue(t *testing.T) {
	db := openDBTestSQLite(t)
	if err := AutoMigrate(db); err != nil {
		t.Fatalf("auto migrate: %v", err)
	}

	repo := NewContactRepository(db)
	roleID := uuidv7.MustBytes()
	if err := db.Create(&auth.Role{ID: roleID, Code: auth.RoleCodeStaff, Name: "Staff"}).Error; err != nil {
		t.Fatalf("create role: %v", err)
	}

	sortKey := 7
	user := &auth.User{
		ID:        uuidv7.MustBytes(),
		Email:     "sortkey-null@example.com",
		FirstName: "Sort",
		LastName:  "Key",
		Timezone:  "UTC",
		RoleID:    roleID,
		SortKey:   &sortKey,
		IsActive:  true,
	}
	if err := db.Create(user).Error; err != nil {
		t.Fatalf("create user: %v", err)
	}

	if err := repo.UpdateContact(context.Background(), contact.UpdateInput{
		UserID: user.ID,
		User: contact.UserPatch{
			SortKey:    nil,
			SortKeySet: true,
		},
	}); err != nil {
		t.Fatalf("update contact: %v", err)
	}

	var refreshed auth.User
	if err := db.Where("id = ?", user.ID).Take(&refreshed).Error; err != nil {
		t.Fatalf("reload user: %v", err)
	}
	if refreshed.SortKey != nil {
		t.Fatalf("expected sortkey NULL, got %d", *refreshed.SortKey)
	}
}

func seedContactStaffUser(t *testing.T, db *gorm.DB, roleID []byte, firstName string, sortKey *int, isActive bool, note string) []byte {
	t.Helper()

	userID := uuidv7.MustBytes()
	username := fmt.Sprintf("%s_user_%d", firstName, time.Now().UnixNano())
	user := &auth.User{
		ID:        userID,
		Email:     fmt.Sprintf("%s@example.com", username),
		Username:  &username,
		FirstName: firstName,
		LastName:  "Contact",
		Timezone:  "UTC",
		RoleID:    roleID,
		SortKey:   sortKey,
		IsActive:  isActive,
		CreatedAt: time.Now().UTC(),
		UpdatedAt: time.Now().UTC(),
	}
	if err := db.Create(user).Error; err != nil {
		t.Fatalf("create user: %v", err)
	}
	if err := db.Model(&auth.User{}).Where("id = ?", userID).UpdateColumn("is_active", isActive).Error; err != nil {
		t.Fatalf("sync user is_active: %v", err)
	}
	if err := db.Create(&auth.StaffProfile{
		UserID:    userID,
		ShortName: firstName,
		Note:      note,
		CreatedAt: time.Now().UTC(),
		UpdatedAt: time.Now().UTC(),
	}).Error; err != nil {
		t.Fatalf("create staff profile: %v", err)
	}

	return userID
}

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

func strPtrContactRepo(v string) *string { return &v }
