package handlers

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

	"github.com/gofiber/fiber/v2"

	filemanager "wucher/internal/domain/file_manager"
	mastersettings "wucher/internal/domain/master_settings"
	"wucher/internal/service"
	"wucher/internal/shared/pkg/uuidv7"
)

type masterSettingsServiceMock struct {
	createErr error
	updateErr error
	deleteErr error
	getErr    error
	listErr   error

	lastCreate   *mastersettings.MasterSettings
	lastUpdate   *mastersettings.MasterSettings
	lastDeleteID []byte
	lastDeleteBy []byte
	lastGetID    []byte

	getByIDCall int
	getByIDFn   func(id []byte, call int) (*mastersettings.MasterSettings, error)
	getRow      *mastersettings.MasterSettings

	lastListFilter string
	lastListSort   string
	lastListLimit  int
	lastListOffset int
	listRows       []mastersettings.MasterSettings
	listTotal      int64

	upsertMicrosoftFn  func(input mastersettings.MicrosoftEntraConfigInput, actor []byte) (*mastersettings.MicrosoftEntraConfigView, error)
	getMicrosoftFn     func() (*mastersettings.MicrosoftEntraConfigView, error)
	lastMicrosoftIn    mastersettings.MicrosoftEntraConfigInput
	lastMicrosoftActor []byte
}

func (m *masterSettingsServiceMock) Create(_ context.Context, row *mastersettings.MasterSettings) error {
	if len(row.ID) == 0 {
		row.ID = uuidv7.MustBytes()
	}
	m.lastCreate = row
	return m.createErr
}

func (m *masterSettingsServiceMock) Update(_ context.Context, row *mastersettings.MasterSettings) error {
	m.lastUpdate = row
	return m.updateErr
}

func (m *masterSettingsServiceMock) Delete(_ context.Context, id []byte, deletedBy []byte) error {
	m.lastDeleteID = append([]byte(nil), id...)
	m.lastDeleteBy = append([]byte(nil), deletedBy...)
	return m.deleteErr
}

func (m *masterSettingsServiceMock) GetByID(_ context.Context, id []byte) (*mastersettings.MasterSettings, error) {
	m.lastGetID = append([]byte(nil), id...)
	m.getByIDCall++
	if m.getByIDFn != nil {
		return m.getByIDFn(id, m.getByIDCall)
	}
	return m.getRow, m.getErr
}

func (m *masterSettingsServiceMock) List(_ context.Context, filter, sort string, limit, offset int) ([]mastersettings.MasterSettings, int64, error) {
	m.lastListFilter = filter
	m.lastListSort = sort
	m.lastListLimit = limit
	m.lastListOffset = offset
	return m.listRows, m.listTotal, m.listErr
}

func (m *masterSettingsServiceMock) UpsertMicrosoftEntraConfig(_ context.Context, input mastersettings.MicrosoftEntraConfigInput, actor []byte) (*mastersettings.MicrosoftEntraConfigView, error) {
	m.lastMicrosoftIn = input
	m.lastMicrosoftActor = append([]byte(nil), actor...)
	if m.upsertMicrosoftFn != nil {
		return m.upsertMicrosoftFn(input, actor)
	}
	return nil, nil
}

func (m *masterSettingsServiceMock) GetMicrosoftEntraConfig(_ context.Context) (*mastersettings.MicrosoftEntraConfigView, error) {
	if m.getMicrosoftFn != nil {
		return m.getMicrosoftFn()
	}
	return nil, nil
}

type masterSettingsFileRepoMock struct {
	getByIDErr error
	getByIDRow *filemanager.File
	getByIDFn  func(id []byte) (*filemanager.File, error)
	lastGetID  []byte
}

func (m *masterSettingsFileRepoMock) Create(context.Context, *filemanager.File) error { return nil }
func (m *masterSettingsFileRepoMock) Update(context.Context, *filemanager.File) error { return nil }
func (m *masterSettingsFileRepoMock) TransitionStatus(context.Context, filemanager.FileStatusTransitionParams) error {
	return nil
}
func (m *masterSettingsFileRepoMock) Finalize(context.Context, filemanager.FileFinalizeParams) error {
	return nil
}
func (m *masterSettingsFileRepoMock) Delete(context.Context, []byte, []byte, *time.Time) error {
	return nil
}
func (m *masterSettingsFileRepoMock) Restore(context.Context, []byte, []byte) error { return nil }
func (m *masterSettingsFileRepoMock) Purge(context.Context, []byte) error           { return nil }
func (m *masterSettingsFileRepoMock) SearchByName(context.Context, string, string, int, int) ([]filemanager.File, int64, error) {
	return nil, 0, nil
}
func (m *masterSettingsFileRepoMock) GetByID(_ context.Context, id []byte) (*filemanager.File, error) {
	m.lastGetID = append([]byte(nil), id...)
	if m.getByIDFn != nil {
		return m.getByIDFn(id)
	}
	return m.getByIDRow, m.getByIDErr
}
func (m *masterSettingsFileRepoMock) GetByIDAny(context.Context, []byte) (*filemanager.File, error) {
	return nil, nil
}
func (m *masterSettingsFileRepoMock) GetByObjectKey(context.Context, string) (*filemanager.File, error) {
	return nil, nil
}
func (m *masterSettingsFileRepoMock) GetByFolderAndName(context.Context, []byte, string) (*filemanager.File, error) {
	return nil, nil
}
func (m *masterSettingsFileRepoMock) GetByTemplateAndTakeover(context.Context, []byte, []byte) (*filemanager.File, error) {
	return nil, nil
}
func (m *masterSettingsFileRepoMock) ListByFolder(context.Context, []byte, string, int, int) ([]filemanager.File, int64, error) {
	return nil, 0, nil
}
func (m *masterSettingsFileRepoMock) ListTrashByFolder(context.Context, []byte, string, int, int) ([]filemanager.File, int64, error) {
	return nil, 0, nil
}

type masterSettingsStorageMock struct {
	presignURL string
	presignErr error
	lastKey    string
}

type masterSettingsLifecycleServiceMock struct {
	filemanager.Service
	markErr      error
	reconcileErr error
}

func (m *masterSettingsLifecycleServiceMock) MarkFileActive(context.Context, []byte, []byte) error {
	return m.markErr
}
func (m *masterSettingsLifecycleServiceMock) ReconcileFileLifecycle(context.Context, []byte, []byte) error {
	return m.reconcileErr
}
func (m *masterSettingsLifecycleServiceMock) CleanupOrphanPendingFiles(context.Context, time.Time, time.Duration, int, bool) (filemanager.FileCleanupStats, error) {
	return filemanager.FileCleanupStats{}, nil
}

func (m *masterSettingsStorageMock) EnsureBucket(context.Context) error { return nil }
func (m *masterSettingsStorageMock) PutObject(context.Context, filemanager.PutObjectInput) (filemanager.ObjectMetadata, error) {
	return filemanager.ObjectMetadata{}, nil
}
func (m *masterSettingsStorageMock) DeleteObject(context.Context, string) error { return nil }
func (m *masterSettingsStorageMock) HeadObject(context.Context, string) (filemanager.ObjectMetadata, error) {
	return filemanager.ObjectMetadata{}, nil
}
func (m *masterSettingsStorageMock) PresignPutObject(context.Context, filemanager.PresignPutObjectInput) (filemanager.PresignedPutObject, error) {
	return filemanager.PresignedPutObject{}, nil
}
func (m *masterSettingsStorageMock) PresignGetObject(_ context.Context, objectKey string, _ time.Duration) (string, error) {
	m.lastKey = objectKey
	return m.presignURL, m.presignErr
}

func newMasterSettingsFileService(fileRepo *masterSettingsFileRepoMock) filemanager.Service {
	return service.NewFileManagerService(nil, fileRepo, nil, service.FileManagerServiceDependencies{})
}

func newMasterSettingsTestApp(h *MasterSettingsHandler, actor []byte) *fiber.App {
	app := fiber.New()
	app.Use(func(c *fiber.Ctx) error {
		if len(actor) > 0 {
			c.Locals("user_id", actor)
		}
		return c.Next()
	})
	app.Post("/api/v1/master-settings/create", h.Create)
	app.Get("/api/v1/master-settings/get", h.Get)
	app.Get("/api/v1/master-settings/sso/microsoft", h.GetMicrosoftEntraSSO)
	app.Put("/api/v1/master-settings/sso/microsoft", h.UpsertMicrosoftEntraSSO)
	app.Patch("/api/v1/master-settings/update/:uuid", h.Update)
	app.Delete("/api/v1/master-settings/delete/:uuid", h.Delete)
	return app
}

func doJSONRequestMasterSettings(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 mustUUIDStringMasterSettings(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 strPtrMasterSettings(v string) *string { return &v }

func TestNewMasterSettingsHandler(t *testing.T) {
	h := NewMasterSettingsHandler(&masterSettingsServiceMock{}, nil, nil)
	if h == nil || h.validate == nil {
		t.Fatalf("expected handler and validator initialized")
	}
}

func TestMasterSettingsHandlerCreate(t *testing.T) {
	t.Run("invalid json", func(t *testing.T) {
		svc := &masterSettingsServiceMock{}
		h := NewMasterSettingsHandler(svc, nil, nil)
		app := newMasterSettingsTestApp(h, nil)

		resp, _ := doJSONRequestMasterSettings(t, app, http.MethodPost, "/api/v1/master-settings/create", []byte(`{"data":`))
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})

	t.Run("validation error", func(t *testing.T) {
		svc := &masterSettingsServiceMock{}
		h := NewMasterSettingsHandler(svc, nil, nil)
		app := newMasterSettingsTestApp(h, nil)

		resp, _ := doJSONRequestMasterSettings(t, app, http.MethodPost, "/api/v1/master-settings/create", []byte(`{}`))
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})

	t.Run("company name empty after trim", func(t *testing.T) {
		svc := &masterSettingsServiceMock{}
		h := NewMasterSettingsHandler(svc, nil, nil)
		app := newMasterSettingsTestApp(h, nil)

		body := []byte(`{"data":{"type":"master_settings_create","attributes":{"company_name":"   "}}}`)
		resp, _ := doJSONRequestMasterSettings(t, app, http.MethodPost, "/api/v1/master-settings/create", body)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})

	t.Run("logo file uuid invalid", func(t *testing.T) {
		svc := &masterSettingsServiceMock{}
		h := NewMasterSettingsHandler(svc, nil, nil)
		app := newMasterSettingsTestApp(h, nil)

		body := []byte(`{"data":{"type":"master_settings_create","attributes":{"company_name":"Wucher","logo_file_uuid":"not-uuid"}}}`)
		resp, _ := doJSONRequestMasterSettings(t, app, http.MethodPost, "/api/v1/master-settings/create", body)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})

	t.Run("cover file uuid invalid", func(t *testing.T) {
		svc := &masterSettingsServiceMock{}
		h := NewMasterSettingsHandler(svc, nil, nil)
		app := newMasterSettingsTestApp(h, nil)

		body := []byte(`{"data":{"type":"master_settings_create","attributes":{"company_name":"Wucher","cover_file_uuid":"not-uuid"}}}`)
		resp, _ := doJSONRequestMasterSettings(t, app, http.MethodPost, "/api/v1/master-settings/create", body)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})

	t.Run("file manager unavailable", func(t *testing.T) {
		svc := &masterSettingsServiceMock{}
		h := NewMasterSettingsHandler(svc, nil, nil)
		app := newMasterSettingsTestApp(h, nil)
		id := mustUUIDStringMasterSettings(t, uuidv7.MustBytes())

		body := []byte(`{"data":{"type":"master_settings_create","attributes":{"company_name":"Wucher","logo_file_uuid":"` + id + `"}}}`)
		resp, _ := doJSONRequestMasterSettings(t, app, http.MethodPost, "/api/v1/master-settings/create", body)
		if resp.StatusCode != http.StatusServiceUnavailable {
			t.Fatalf("expected 503, got %d", resp.StatusCode)
		}
	})

	t.Run("file lookup error", func(t *testing.T) {
		svc := &masterSettingsServiceMock{}
		fileRepo := &masterSettingsFileRepoMock{getByIDErr: errors.New("lookup failed")}
		h := NewMasterSettingsHandler(svc, newMasterSettingsFileService(fileRepo), nil)
		app := newMasterSettingsTestApp(h, nil)
		id := mustUUIDStringMasterSettings(t, uuidv7.MustBytes())

		body := []byte(`{"data":{"type":"master_settings_create","attributes":{"company_name":"Wucher","logo_file_uuid":"` + id + `"}}}`)
		resp, _ := doJSONRequestMasterSettings(t, app, http.MethodPost, "/api/v1/master-settings/create", body)
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})

	t.Run("file not found", func(t *testing.T) {
		svc := &masterSettingsServiceMock{}
		fileRepo := &masterSettingsFileRepoMock{getByIDRow: nil}
		h := NewMasterSettingsHandler(svc, newMasterSettingsFileService(fileRepo), nil)
		app := newMasterSettingsTestApp(h, nil)
		id := mustUUIDStringMasterSettings(t, uuidv7.MustBytes())

		body := []byte(`{"data":{"type":"master_settings_create","attributes":{"company_name":"Wucher","logo_file_uuid":"` + id + `"}}}`)
		resp, _ := doJSONRequestMasterSettings(t, app, http.MethodPost, "/api/v1/master-settings/create", body)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})

	t.Run("file not ready", func(t *testing.T) {
		svc := &masterSettingsServiceMock{}
		fileRepo := &masterSettingsFileRepoMock{
			getByIDRow: &filemanager.File{
				ID:     uuidv7.MustBytes(),
				Status: filemanager.FileStatusUploaded,
			},
		}
		h := NewMasterSettingsHandler(svc, newMasterSettingsFileService(fileRepo), nil)
		app := newMasterSettingsTestApp(h, nil)
		id := mustUUIDStringMasterSettings(t, uuidv7.MustBytes())

		body := []byte(`{"data":{"type":"master_settings_create","attributes":{"company_name":"Wucher","logo_file_uuid":"` + id + `"}}}`)
		resp, _ := doJSONRequestMasterSettings(t, app, http.MethodPost, "/api/v1/master-settings/create", body)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})

	t.Run("create error", func(t *testing.T) {
		svc := &masterSettingsServiceMock{createErr: errors.New("create failed")}
		h := NewMasterSettingsHandler(svc, nil, nil)
		app := newMasterSettingsTestApp(h, nil)

		body := []byte(`{"data":{"type":"master_settings_create","attributes":{"company_name":"Wucher"}}}`)
		resp, _ := doJSONRequestMasterSettings(t, app, http.MethodPost, "/api/v1/master-settings/create", body)
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})

	t.Run("success with refetch", func(t *testing.T) {
		actor := uuidv7.MustBytes()
		logoID := uuidv7.MustBytes()
		coverID := uuidv7.MustBytes()
		fileRepo := &masterSettingsFileRepoMock{
			getByIDFn: func(id []byte) (*filemanager.File, error) {
				return &filemanager.File{
					ID:        id,
					Status:    filemanager.FileStatusReady,
					ObjectKey: "obj-" + mustUUIDStringMasterSettings(t, id),
				}, nil
			},
		}
		svc := &masterSettingsServiceMock{
			getByIDFn: func(id []byte, _ int) (*mastersettings.MasterSettings, error) {
				return &mastersettings.MasterSettings{
					ID:          id,
					CompanyName: "Wucher",
					Title:       "Welcome",
					Subtitle:    "Ops",
					LogoFileID:  logoID,
					CoverFileID: coverID,
					CreatedAt:   time.Now().UTC(),
					UpdatedAt:   time.Now().UTC(),
				}, nil
			},
		}
		h := NewMasterSettingsHandler(svc, newMasterSettingsFileService(fileRepo), nil)
		app := newMasterSettingsTestApp(h, actor)
		logoIDStr := mustUUIDStringMasterSettings(t, logoID)
		coverIDStr := mustUUIDStringMasterSettings(t, coverID)
		body := []byte(`{"data":{"type":"master_settings_create","attributes":{"company_name":" Wucher ","title":" Welcome ","subtitle":" Ops ","logo_file_uuid":"` + logoIDStr + `","cover_file_uuid":"` + coverIDStr + `"}}}`)

		resp, _ := doJSONRequestMasterSettings(t, app, http.MethodPost, "/api/v1/master-settings/create", body)
		if resp.StatusCode != http.StatusCreated {
			t.Fatalf("expected 201, got %d", resp.StatusCode)
		}
		if svc.lastCreate == nil {
			t.Fatalf("expected row created")
		}
		if svc.lastCreate.CompanyName != "Wucher" || svc.lastCreate.Title != "Welcome" || svc.lastCreate.Subtitle != "Ops" {
			t.Fatalf("expected trimmed values saved")
		}
		if string(svc.lastCreate.CreatedBy) != string(actor) || string(svc.lastCreate.UpdatedBy) != string(actor) {
			t.Fatalf("expected created_by and updated_by from actor")
		}
	})

	t.Run("success fallback when refetch fails", func(t *testing.T) {
		svc := &masterSettingsServiceMock{
			getByIDFn: func(_ []byte, _ int) (*mastersettings.MasterSettings, error) {
				return nil, errors.New("lookup failed")
			},
		}
		h := NewMasterSettingsHandler(svc, nil, nil)
		app := newMasterSettingsTestApp(h, nil)

		body := []byte(`{"data":{"type":"master_settings_create","attributes":{"company_name":"Wucher"}}}`)
		resp, _ := doJSONRequestMasterSettings(t, app, http.MethodPost, "/api/v1/master-settings/create", body)
		if resp.StatusCode != http.StatusCreated {
			t.Fatalf("expected 201, got %d", resp.StatusCode)
		}
	})
}

func TestMasterSettingsHandlerGet(t *testing.T) {
	t.Run("list error", func(t *testing.T) {
		svc := &masterSettingsServiceMock{listErr: errors.New("list failed")}
		h := NewMasterSettingsHandler(svc, nil, nil)
		app := newMasterSettingsTestApp(h, nil)

		resp, _ := doJSONRequestMasterSettings(t, app, http.MethodGet, "/api/v1/master-settings/get", nil)
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})

	t.Run("not found", func(t *testing.T) {
		svc := &masterSettingsServiceMock{listRows: nil, listTotal: 0}
		h := NewMasterSettingsHandler(svc, nil, nil)
		app := newMasterSettingsTestApp(h, nil)

		resp, body := doJSONRequestMasterSettings(t, app, http.MethodGet, "/api/v1/master-settings/get", nil)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d", resp.StatusCode)
		}
		if !bytes.Contains(body, []byte(`"company_name":""`)) {
			t.Fatalf("expected empty company_name in response, body=%s", string(body))
		}
	})

	t.Run("success with nil logo and cover url when file resolver unavailable", func(t *testing.T) {
		logoID := uuidv7.MustBytes()
		coverID := uuidv7.MustBytes()
		row := mastersettings.MasterSettings{
			ID:          uuidv7.MustBytes(),
			CompanyName: "Wucher",
			Title:       "Welcome",
			Subtitle:    "Ops",
			LogoFileID:  logoID,
			CoverFileID: coverID,
			CreatedAt:   time.Now().UTC(),
			UpdatedAt:   time.Now().UTC(),
		}
		svc := &masterSettingsServiceMock{listRows: []mastersettings.MasterSettings{row}, listTotal: 1}
		h := NewMasterSettingsHandler(svc, nil, nil)
		app := newMasterSettingsTestApp(h, nil)

		resp, body := doJSONRequestMasterSettings(t, app, http.MethodGet, "/api/v1/master-settings/get", nil)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d", resp.StatusCode)
		}
		if bytes.Contains(body, []byte(`"logo_url":"`)) {
			t.Fatalf("expected logo_url omitted when resolver unavailable, body=%s", string(body))
		}
		if bytes.Contains(body, []byte(`"cover_url":"`)) {
			t.Fatalf("expected cover_url omitted when resolver unavailable, body=%s", string(body))
		}
		if svc.lastListSort != "-updated_at" || svc.lastListLimit != 1 || svc.lastListOffset != 0 {
			t.Fatalf("expected list called with singleton params")
		}
	})
}

func TestMasterSettingsHandlerUpdate(t *testing.T) {
	t.Run("invalid uuid", func(t *testing.T) {
		svc := &masterSettingsServiceMock{}
		h := NewMasterSettingsHandler(svc, nil, nil)
		app := newMasterSettingsTestApp(h, nil)

		resp, _ := doJSONRequestMasterSettings(t, app, http.MethodPatch, "/api/v1/master-settings/update/not-uuid", []byte(`{}`))
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})

	t.Run("invalid json", func(t *testing.T) {
		svc := &masterSettingsServiceMock{}
		h := NewMasterSettingsHandler(svc, nil, nil)
		app := newMasterSettingsTestApp(h, nil)
		id := mustUUIDStringMasterSettings(t, uuidv7.MustBytes())

		resp, _ := doJSONRequestMasterSettings(t, app, http.MethodPatch, "/api/v1/master-settings/update/"+id, []byte(`{"data":`))
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})

	t.Run("validation error", func(t *testing.T) {
		svc := &masterSettingsServiceMock{}
		h := NewMasterSettingsHandler(svc, nil, nil)
		app := newMasterSettingsTestApp(h, nil)
		id := mustUUIDStringMasterSettings(t, uuidv7.MustBytes())

		resp, _ := doJSONRequestMasterSettings(t, app, http.MethodPatch, "/api/v1/master-settings/update/"+id, []byte(`{}`))
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})

	t.Run("id required", func(t *testing.T) {
		svc := &masterSettingsServiceMock{}
		h := NewMasterSettingsHandler(svc, nil, nil)
		app := newMasterSettingsTestApp(h, nil)
		id := mustUUIDStringMasterSettings(t, uuidv7.MustBytes())

		body := []byte(`{"data":{"type":"master_settings_update","attributes":{"title":"Welcome"}}}`)
		resp, _ := doJSONRequestMasterSettings(t, app, http.MethodPatch, "/api/v1/master-settings/update/"+id, body)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})

	t.Run("id mismatch", func(t *testing.T) {
		svc := &masterSettingsServiceMock{}
		h := NewMasterSettingsHandler(svc, nil, nil)
		app := newMasterSettingsTestApp(h, nil)
		pathID := mustUUIDStringMasterSettings(t, uuidv7.MustBytes())
		otherID := mustUUIDStringMasterSettings(t, uuidv7.MustBytes())

		body := []byte(`{"data":{"type":"master_settings_update","id":"` + otherID + `","attributes":{"title":"Welcome"}}}`)
		resp, _ := doJSONRequestMasterSettings(t, app, http.MethodPatch, "/api/v1/master-settings/update/"+pathID, body)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})

	t.Run("no attributes provided", func(t *testing.T) {
		svc := &masterSettingsServiceMock{}
		h := NewMasterSettingsHandler(svc, nil, nil)
		app := newMasterSettingsTestApp(h, nil)
		id := mustUUIDStringMasterSettings(t, uuidv7.MustBytes())

		body := []byte(`{"data":{"type":"master_settings_update","id":"` + id + `","attributes":{}}}`)
		resp, _ := doJSONRequestMasterSettings(t, app, http.MethodPatch, "/api/v1/master-settings/update/"+id, body)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})

	t.Run("not found when get fails", func(t *testing.T) {
		svc := &masterSettingsServiceMock{getErr: errors.New("db error")}
		h := NewMasterSettingsHandler(svc, nil, nil)
		app := newMasterSettingsTestApp(h, nil)
		id := mustUUIDStringMasterSettings(t, uuidv7.MustBytes())

		body := []byte(`{"data":{"type":"master_settings_update","id":"` + id + `","attributes":{"title":"Welcome"}}}`)
		resp, _ := doJSONRequestMasterSettings(t, app, http.MethodPatch, "/api/v1/master-settings/update/"+id, body)
		if resp.StatusCode != http.StatusNotFound {
			t.Fatalf("expected 404, got %d", resp.StatusCode)
		}
	})

	t.Run("not found when get nil", func(t *testing.T) {
		svc := &masterSettingsServiceMock{getRow: nil}
		h := NewMasterSettingsHandler(svc, nil, nil)
		app := newMasterSettingsTestApp(h, nil)
		id := mustUUIDStringMasterSettings(t, uuidv7.MustBytes())

		body := []byte(`{"data":{"type":"master_settings_update","id":"` + id + `","attributes":{"title":"Welcome"}}}`)
		resp, _ := doJSONRequestMasterSettings(t, app, http.MethodPatch, "/api/v1/master-settings/update/"+id, body)
		if resp.StatusCode != http.StatusNotFound {
			t.Fatalf("expected 404, got %d", resp.StatusCode)
		}
	})

	t.Run("company name empty", func(t *testing.T) {
		idBytes := uuidv7.MustBytes()
		id := mustUUIDStringMasterSettings(t, idBytes)
		svc := &masterSettingsServiceMock{
			getRow: &mastersettings.MasterSettings{ID: idBytes, CompanyName: "Wucher"},
		}
		h := NewMasterSettingsHandler(svc, nil, nil)
		app := newMasterSettingsTestApp(h, nil)

		body := []byte(`{"data":{"type":"master_settings_update","id":"` + id + `","attributes":{"company_name":"   "}}}`)
		resp, _ := doJSONRequestMasterSettings(t, app, http.MethodPatch, "/api/v1/master-settings/update/"+id, body)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})

	t.Run("logo file uuid invalid", func(t *testing.T) {
		idBytes := uuidv7.MustBytes()
		id := mustUUIDStringMasterSettings(t, idBytes)
		svc := &masterSettingsServiceMock{
			getRow: &mastersettings.MasterSettings{ID: idBytes, CompanyName: "Wucher"},
		}
		h := NewMasterSettingsHandler(svc, nil, nil)
		app := newMasterSettingsTestApp(h, nil)

		body := []byte(`{"data":{"type":"master_settings_update","id":"` + id + `","attributes":{"logo_file_uuid":"bad"}}}`)
		resp, _ := doJSONRequestMasterSettings(t, app, http.MethodPatch, "/api/v1/master-settings/update/"+id, body)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})

	t.Run("cover file uuid invalid", func(t *testing.T) {
		idBytes := uuidv7.MustBytes()
		id := mustUUIDStringMasterSettings(t, idBytes)
		svc := &masterSettingsServiceMock{
			getRow: &mastersettings.MasterSettings{ID: idBytes, CompanyName: "Wucher"},
		}
		h := NewMasterSettingsHandler(svc, nil, nil)
		app := newMasterSettingsTestApp(h, nil)

		body := []byte(`{"data":{"type":"master_settings_update","id":"` + id + `","attributes":{"cover_file_uuid":"bad"}}}`)
		resp, _ := doJSONRequestMasterSettings(t, app, http.MethodPatch, "/api/v1/master-settings/update/"+id, body)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})

	t.Run("update error", func(t *testing.T) {
		idBytes := uuidv7.MustBytes()
		id := mustUUIDStringMasterSettings(t, idBytes)
		svc := &masterSettingsServiceMock{
			getRow:    &mastersettings.MasterSettings{ID: idBytes, CompanyName: "Wucher"},
			updateErr: errors.New("update failed"),
		}
		h := NewMasterSettingsHandler(svc, nil, nil)
		app := newMasterSettingsTestApp(h, nil)

		body := []byte(`{"data":{"type":"master_settings_update","id":"` + id + `","attributes":{"title":"Welcome"}}}`)
		resp, _ := doJSONRequestMasterSettings(t, app, http.MethodPatch, "/api/v1/master-settings/update/"+id, body)
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})

	t.Run("success update with cover file uuid", func(t *testing.T) {
		idBytes := uuidv7.MustBytes()
		id := mustUUIDStringMasterSettings(t, idBytes)
		coverID := uuidv7.MustBytes()
		coverIDStr := mustUUIDStringMasterSettings(t, coverID)
		fileRepo := &masterSettingsFileRepoMock{
			getByIDFn: func(fid []byte) (*filemanager.File, error) {
				return &filemanager.File{
					ID:        fid,
					Status:    filemanager.FileStatusReady,
					ObjectKey: "cover.png",
				}, nil
			},
		}
		svc := &masterSettingsServiceMock{
			getByIDFn: func(gotID []byte, call int) (*mastersettings.MasterSettings, error) {
				if call == 1 {
					return &mastersettings.MasterSettings{ID: gotID, CompanyName: "Wucher"}, nil
				}
				return &mastersettings.MasterSettings{
					ID:          gotID,
					CompanyName: "Wucher",
					CoverFileID: coverID,
					CreatedAt:   time.Now().UTC(),
					UpdatedAt:   time.Now().UTC(),
				}, nil
			},
		}
		h := NewMasterSettingsHandler(svc, newMasterSettingsFileService(fileRepo), nil)
		app := newMasterSettingsTestApp(h, nil)

		body := []byte(`{"data":{"type":"master_settings_update","id":"` + id + `","attributes":{"cover_file_uuid":"` + coverIDStr + `"}}}`)
		resp, _ := doJSONRequestMasterSettings(t, app, http.MethodPatch, "/api/v1/master-settings/update/"+id, body)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d", resp.StatusCode)
		}
		if svc.lastUpdate == nil || string(svc.lastUpdate.CoverFileID) != string(coverID) {
			t.Fatalf("expected cover_file_id updated")
		}
	})

	t.Run("empty logo_file_uuid clears existing file", func(t *testing.T) {
		idBytes := uuidv7.MustBytes()
		id := mustUUIDStringMasterSettings(t, idBytes)
		logoID := uuidv7.MustBytes()
		svc := &masterSettingsServiceMock{
			getByIDFn: func(gotID []byte, call int) (*mastersettings.MasterSettings, error) {
				if call == 1 {
					return &mastersettings.MasterSettings{
						ID:          gotID,
						CompanyName: "Wucher",
						LogoFileID:  append([]byte(nil), logoID...),
					}, nil
				}
				return &mastersettings.MasterSettings{
					ID:          gotID,
					CompanyName: "Wucher",
					LogoFileID:  append([]byte(nil), logoID...),
					UpdatedAt:   time.Now().UTC(),
				}, nil
			},
		}
		h := NewMasterSettingsHandler(svc, nil, nil)
		app := newMasterSettingsTestApp(h, nil)

		body := []byte(`{"data":{"type":"master_settings_update","id":"` + id + `","attributes":{"company_name":"Wucher","logo_file_uuid":""}}}`)
		resp, _ := doJSONRequestMasterSettings(t, app, http.MethodPatch, "/api/v1/master-settings/update/"+id, body)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d", resp.StatusCode)
		}
		if svc.lastUpdate == nil || svc.lastUpdate.LogoFileID != nil {
			t.Fatalf("expected logo_file_id to be cleared")
		}
	})

	t.Run("success with refetch", func(t *testing.T) {
		idBytes := uuidv7.MustBytes()
		id := mustUUIDStringMasterSettings(t, idBytes)
		logoID := uuidv7.MustBytes()
		logoIDStr := mustUUIDStringMasterSettings(t, logoID)
		actor := uuidv7.MustBytes()
		fileRepo := &masterSettingsFileRepoMock{
			getByIDFn: func(fid []byte) (*filemanager.File, error) {
				return &filemanager.File{
					ID:        fid,
					Status:    filemanager.FileStatusReady,
					ObjectKey: "obj",
				}, nil
			},
		}
		svc := &masterSettingsServiceMock{
			getByIDFn: func(gotID []byte, call int) (*mastersettings.MasterSettings, error) {
				if call == 1 {
					legacy := "https://legacy/logo.png"
					return &mastersettings.MasterSettings{
						ID:          gotID,
						CompanyName: "Old",
						Title:       "Old",
						Subtitle:    "Old",
						LogoURL:     &legacy,
					}, nil
				}
				return &mastersettings.MasterSettings{
					ID:          gotID,
					CompanyName: "Wucher",
					Title:       "Welcome",
					Subtitle:    "Ops",
					LogoFileID:  logoID,
					CreatedAt:   time.Now().UTC(),
					UpdatedAt:   time.Now().UTC(),
				}, nil
			},
		}
		h := NewMasterSettingsHandler(svc, newMasterSettingsFileService(fileRepo), nil)
		app := newMasterSettingsTestApp(h, actor)

		body := []byte(`{"data":{"type":"master_settings_update","id":"` + id + `","attributes":{"company_name":" Wucher ","title":" Welcome ","subtitle":" Ops ","logo_file_uuid":"` + logoIDStr + `"}}}`)
		resp, _ := doJSONRequestMasterSettings(t, app, http.MethodPatch, "/api/v1/master-settings/update/"+id, body)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d", resp.StatusCode)
		}
		if svc.lastUpdate == nil {
			t.Fatalf("expected update called")
		}
		if svc.lastUpdate.CompanyName != "Wucher" || svc.lastUpdate.Title != "Welcome" || svc.lastUpdate.Subtitle != "Ops" {
			t.Fatalf("expected trimmed update values")
		}
		if svc.lastUpdate.LogoURL != nil {
			t.Fatalf("expected legacy logo_url cleared when logo_file_uuid provided")
		}
		if string(svc.lastUpdate.UpdatedBy) != string(actor) {
			t.Fatalf("expected updated_by from actor")
		}
	})

	t.Run("success fallback when refetch fails", func(t *testing.T) {
		idBytes := uuidv7.MustBytes()
		id := mustUUIDStringMasterSettings(t, idBytes)
		svc := &masterSettingsServiceMock{
			getByIDFn: func(gotID []byte, call int) (*mastersettings.MasterSettings, error) {
				if call == 1 {
					return &mastersettings.MasterSettings{ID: gotID, CompanyName: "Wucher"}, nil
				}
				return nil, errors.New("lookup failed")
			},
		}
		h := NewMasterSettingsHandler(svc, nil, nil)
		app := newMasterSettingsTestApp(h, nil)

		body := []byte(`{"data":{"type":"master_settings_update","id":"` + id + `","attributes":{"title":"Welcome"}}}`)
		resp, _ := doJSONRequestMasterSettings(t, app, http.MethodPatch, "/api/v1/master-settings/update/"+id, body)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d", resp.StatusCode)
		}
	})
}

func TestMasterSettingsHandlerDelete(t *testing.T) {
	t.Run("invalid uuid", func(t *testing.T) {
		svc := &masterSettingsServiceMock{}
		h := NewMasterSettingsHandler(svc, nil, nil)
		app := newMasterSettingsTestApp(h, nil)

		resp, _ := doJSONRequestMasterSettings(t, app, http.MethodDelete, "/api/v1/master-settings/delete/not-uuid", nil)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})

	t.Run("delete error", func(t *testing.T) {
		svc := &masterSettingsServiceMock{deleteErr: errors.New("delete failed")}
		h := NewMasterSettingsHandler(svc, nil, nil)
		app := newMasterSettingsTestApp(h, nil)
		id := mustUUIDStringMasterSettings(t, uuidv7.MustBytes())

		resp, _ := doJSONRequestMasterSettings(t, app, http.MethodDelete, "/api/v1/master-settings/delete/"+id, nil)
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})

	t.Run("success", func(t *testing.T) {
		actor := uuidv7.MustBytes()
		svc := &masterSettingsServiceMock{}
		h := NewMasterSettingsHandler(svc, nil, nil)
		app := newMasterSettingsTestApp(h, actor)
		idBytes := uuidv7.MustBytes()
		id := mustUUIDStringMasterSettings(t, idBytes)

		resp, _ := doJSONRequestMasterSettings(t, app, http.MethodDelete, "/api/v1/master-settings/delete/"+id, nil)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d", resp.StatusCode)
		}
		if string(svc.lastDeleteID) != string(idBytes) {
			t.Fatalf("expected delete called with id")
		}
		if string(svc.lastDeleteBy) != string(actor) {
			t.Fatalf("expected delete called with actor")
		}
	})

	t.Run("fails when sync previous logo lifecycle fails", func(t *testing.T) {
		idBytes := uuidv7.MustBytes()
		id := mustUUIDStringMasterSettings(t, idBytes)
		svc := &masterSettingsServiceMock{
			getRow: &mastersettings.MasterSettings{
				ID:         idBytes,
				LogoFileID: uuidv7.MustBytes(),
			},
		}
		fileSvc := &masterSettingsLifecycleServiceMock{reconcileErr: errors.New("reconcile failed")}
		h := NewMasterSettingsHandler(svc, fileSvc, nil)
		app := newMasterSettingsTestApp(h, uuidv7.MustBytes())

		resp, _ := doJSONRequestMasterSettings(t, app, http.MethodDelete, "/api/v1/master-settings/delete/"+id, nil)
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})

	t.Run("fails when sync previous cover lifecycle fails", func(t *testing.T) {
		idBytes := uuidv7.MustBytes()
		id := mustUUIDStringMasterSettings(t, idBytes)
		svc := &masterSettingsServiceMock{
			getRow: &mastersettings.MasterSettings{
				ID:          idBytes,
				CoverFileID: uuidv7.MustBytes(),
			},
		}
		fileSvc := &masterSettingsLifecycleServiceMock{reconcileErr: errors.New("reconcile failed")}
		h := NewMasterSettingsHandler(svc, fileSvc, nil)
		app := newMasterSettingsTestApp(h, uuidv7.MustBytes())

		resp, _ := doJSONRequestMasterSettings(t, app, http.MethodDelete, "/api/v1/master-settings/delete/"+id, nil)
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})
}

func TestMasterSettingsHandlerHelperFunctions(t *testing.T) {
	if got := optionalTrimmedString(nil); got != "" {
		t.Fatalf("expected empty string for nil, got %q", got)
	}
	if got := optionalTrimmedString(strPtrMasterSettings("  hi  ")); got != "hi" {
		t.Fatalf("expected trimmed string, got %q", got)
	}
	if got := masterSettingsDerefString(nil); got != "" {
		t.Fatalf("expected empty string for nil ptr")
	}
	if got := masterSettingsDerefString(strPtrMasterSettings("abc")); got != "abc" {
		t.Fatalf("unexpected deref value: %q", got)
	}
	if got := stringPointer("x"); got == nil || *got != "x" {
		t.Fatalf("expected pointer value")
	}
	if got := uuidStringPtrOrNil(nil); got != nil {
		t.Fatalf("expected nil for empty uuid bytes")
	}
	id := uuidv7.MustBytes()
	if got := uuidStringPtrOrNil(id); got == nil || strings.TrimSpace(*got) == "" {
		t.Fatalf("expected non-empty uuid string pointer")
	}
}

func TestMasterSettingsHandlerResolveManagedFileID(t *testing.T) {
	t.Run("raw nil", func(t *testing.T) {
		h := NewMasterSettingsHandler(&masterSettingsServiceMock{}, nil, nil)
		id, status, errObj := h.resolveManagedFileID(context.Background(), nil, "/x", "logo_file_uuid")
		if len(id) != 0 || status != 0 || errObj != nil {
			t.Fatalf("expected nil id without error")
		}
	})

	t.Run("raw empty", func(t *testing.T) {
		h := NewMasterSettingsHandler(&masterSettingsServiceMock{}, nil, nil)
		raw := "   "
		id, status, errObj := h.resolveManagedFileID(context.Background(), &raw, "/x", "logo_file_uuid")
		if len(id) != 0 || status != 0 || errObj != nil {
			t.Fatalf("expected nil id without error")
		}
	})

	t.Run("invalid uuid", func(t *testing.T) {
		h := NewMasterSettingsHandler(&masterSettingsServiceMock{}, nil, nil)
		raw := "bad"
		_, status, errObj := h.resolveManagedFileID(context.Background(), &raw, "/x", "logo_file_uuid")
		if status != http.StatusUnprocessableEntity || errObj == nil {
			t.Fatalf("expected 422 validation error")
		}
	})

	t.Run("file service unavailable", func(t *testing.T) {
		h := NewMasterSettingsHandler(&masterSettingsServiceMock{}, nil, nil)
		raw := mustUUIDStringMasterSettings(t, uuidv7.MustBytes())
		_, status, errObj := h.resolveManagedFileID(context.Background(), &raw, "/x", "logo_file_uuid")
		if status != http.StatusServiceUnavailable || errObj == nil {
			t.Fatalf("expected 503 when file service is nil")
		}
	})

	t.Run("lookup error", func(t *testing.T) {
		repo := &masterSettingsFileRepoMock{getByIDErr: errors.New("lookup failed")}
		h := NewMasterSettingsHandler(&masterSettingsServiceMock{}, newMasterSettingsFileService(repo), nil)
		raw := mustUUIDStringMasterSettings(t, uuidv7.MustBytes())
		_, status, errObj := h.resolveManagedFileID(context.Background(), &raw, "/x", "logo_file_uuid")
		if status != http.StatusBadRequest || errObj == nil {
			t.Fatalf("expected 400 lookup error")
		}
	})

	t.Run("file not found", func(t *testing.T) {
		repo := &masterSettingsFileRepoMock{getByIDRow: nil}
		h := NewMasterSettingsHandler(&masterSettingsServiceMock{}, newMasterSettingsFileService(repo), nil)
		raw := mustUUIDStringMasterSettings(t, uuidv7.MustBytes())
		_, status, errObj := h.resolveManagedFileID(context.Background(), &raw, "/x", "logo_file_uuid")
		if status != http.StatusUnprocessableEntity || errObj == nil {
			t.Fatalf("expected 422 not found")
		}
	})

	t.Run("file not ready", func(t *testing.T) {
		repo := &masterSettingsFileRepoMock{
			getByIDRow: &filemanager.File{
				ID:     uuidv7.MustBytes(),
				Status: filemanager.FileStatusUploaded,
			},
		}
		h := NewMasterSettingsHandler(&masterSettingsServiceMock{}, newMasterSettingsFileService(repo), nil)
		raw := mustUUIDStringMasterSettings(t, uuidv7.MustBytes())
		_, status, errObj := h.resolveManagedFileID(context.Background(), &raw, "/x", "logo_file_uuid")
		if status != http.StatusUnprocessableEntity || errObj == nil {
			t.Fatalf("expected 422 not ready")
		}
	})

	t.Run("success ready file", func(t *testing.T) {
		id := uuidv7.MustBytes()
		repo := &masterSettingsFileRepoMock{
			getByIDFn: func(gotID []byte) (*filemanager.File, error) {
				return &filemanager.File{ID: gotID, Status: filemanager.FileStatusReady}, nil
			},
		}
		h := NewMasterSettingsHandler(&masterSettingsServiceMock{}, newMasterSettingsFileService(repo), nil)
		raw := mustUUIDStringMasterSettings(t, id)
		got, status, errObj := h.resolveManagedFileID(context.Background(), &raw, "/x", "logo_file_uuid")
		if status != 0 || errObj != nil {
			t.Fatalf("expected success, got status=%d err=%v", status, errObj)
		}
		if string(got) != string(id) {
			t.Fatalf("expected parsed id returned")
		}
	})

	t.Run("resolve wrappers use field names", func(t *testing.T) {
		h := NewMasterSettingsHandler(&masterSettingsServiceMock{}, nil, nil)
		raw := "bad"

		_, _, logoErr := h.resolveLogoFileID(context.Background(), &raw, "/logo")
		if logoErr == nil || !strings.Contains(logoErr.Detail, "logo_file_uuid") {
			t.Fatalf("expected logo wrapper detail contains logo_file_uuid")
		}
		_, _, coverErr := h.resolveCoverFileID(context.Background(), &raw, "/cover")
		if coverErr == nil || !strings.Contains(coverErr.Detail, "cover_file_uuid") {
			t.Fatalf("expected cover wrapper detail contains cover_file_uuid")
		}
	})
}

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

	t.Run("nil file uuid returns nil", func(t *testing.T) {
		h := NewMasterSettingsHandler(&masterSettingsServiceMock{}, nil, nil)
		withFiberCtx(t, func(c *fiber.Ctx) {
			if got := h.resolveManagedFileURL(c, nil, nil); got != nil {
				t.Fatalf("expected nil url")
			}
		})
	})

	t.Run("nil when file service and storage unavailable", func(t *testing.T) {
		h := NewMasterSettingsHandler(&masterSettingsServiceMock{}, nil, nil)
		fileUUID := "abc"
		withFiberCtx(t, func(c *fiber.Ctx) {
			got := h.resolveManagedFileURL(c, nil, &fileUUID)
			if got != nil {
				t.Fatalf("expected nil, got %v", got)
			}
		})
	})

	t.Run("presign success", func(t *testing.T) {
		fileID := uuidv7.MustBytes()
		fileUUID := mustUUIDStringMasterSettings(t, fileID)
		fileRepo := &masterSettingsFileRepoMock{
			getByIDRow: &filemanager.File{
				ID:        fileID,
				Status:    filemanager.FileStatusReady,
				ObjectKey: "logos/logo.png",
			},
		}
		storage := &masterSettingsStorageMock{presignURL: "https://signed.example/logo.png"}
		h := NewMasterSettingsHandler(&masterSettingsServiceMock{}, newMasterSettingsFileService(fileRepo), storage)

		withFiberCtx(t, func(c *fiber.Ctx) {
			got := h.resolveManagedFileURL(c, fileID, &fileUUID)
			if got == nil || *got != "https://signed.example/logo.png" {
				t.Fatalf("expected presigned url")
			}
			if storage.lastKey != "logos/logo.png" {
				t.Fatalf("expected object key passed to presign")
			}
		})
	})

	t.Run("nil when file lookup error", func(t *testing.T) {
		fileID := uuidv7.MustBytes()
		fileUUID := mustUUIDStringMasterSettings(t, fileID)
		fileRepo := &masterSettingsFileRepoMock{getByIDErr: errors.New("lookup failed")}
		h := NewMasterSettingsHandler(&masterSettingsServiceMock{}, newMasterSettingsFileService(fileRepo), &masterSettingsStorageMock{})

		withFiberCtx(t, func(c *fiber.Ctx) {
			got := h.resolveManagedFileURL(c, fileID, &fileUUID)
			if got != nil {
				t.Fatalf("expected nil on lookup error")
			}
		})
	})

	t.Run("nil when file row missing object key", func(t *testing.T) {
		fileID := uuidv7.MustBytes()
		fileUUID := mustUUIDStringMasterSettings(t, fileID)
		fileRepo := &masterSettingsFileRepoMock{
			getByIDRow: &filemanager.File{
				ID:        fileID,
				Status:    filemanager.FileStatusReady,
				ObjectKey: "   ",
			},
		}
		h := NewMasterSettingsHandler(&masterSettingsServiceMock{}, newMasterSettingsFileService(fileRepo), &masterSettingsStorageMock{})

		withFiberCtx(t, func(c *fiber.Ctx) {
			got := h.resolveManagedFileURL(c, fileID, &fileUUID)
			if got != nil {
				t.Fatalf("expected nil when object key empty")
			}
		})
	})

	t.Run("nil when presign error", func(t *testing.T) {
		fileID := uuidv7.MustBytes()
		fileUUID := mustUUIDStringMasterSettings(t, fileID)
		fileRepo := &masterSettingsFileRepoMock{
			getByIDRow: &filemanager.File{
				ID:        fileID,
				Status:    filemanager.FileStatusReady,
				ObjectKey: "logos/logo.png",
			},
		}
		storage := &masterSettingsStorageMock{presignErr: errors.New("presign failed")}
		h := NewMasterSettingsHandler(&masterSettingsServiceMock{}, newMasterSettingsFileService(fileRepo), storage)

		withFiberCtx(t, func(c *fiber.Ctx) {
			got := h.resolveManagedFileURL(c, fileID, &fileUUID)
			if got != nil {
				t.Fatalf("expected nil on presign error")
			}
		})
	})

	t.Run("resolve logo url legacy and managed branches", func(t *testing.T) {
		legacy := "https://legacy.example/logo.png"
		row := &mastersettings.MasterSettings{LogoURL: &legacy}
		hLegacy := NewMasterSettingsHandler(&masterSettingsServiceMock{}, nil, nil)
		withFiberCtx(t, func(c *fiber.Ctx) {
			got := hLegacy.resolveLogoURL(c, row, nil)
			if got == nil || *got != legacy {
				t.Fatalf("expected legacy logo url")
			}
		})

		fileID := uuidv7.MustBytes()
		fileUUID := mustUUIDStringMasterSettings(t, fileID)
		fileRepo := &masterSettingsFileRepoMock{
			getByIDRow: &filemanager.File{
				ID:        fileID,
				Status:    filemanager.FileStatusReady,
				ObjectKey: "logos/logo.png",
			},
		}
		hManaged := NewMasterSettingsHandler(&masterSettingsServiceMock{}, newMasterSettingsFileService(fileRepo), &masterSettingsStorageMock{presignURL: "https://signed/logo.png"})
		managedRow := &mastersettings.MasterSettings{
			LogoFileID:  fileID,
			CoverFileID: fileID,
		}
		withFiberCtx(t, func(c *fiber.Ctx) {
			logo := hManaged.resolveLogoURL(c, managedRow, &fileUUID)
			cover := hManaged.resolveCoverURL(c, managedRow, &fileUUID)
			if logo == nil || *logo != "https://signed/logo.png" {
				t.Fatalf("expected managed logo url")
			}
			if cover == nil || *cover != "https://signed/logo.png" {
				t.Fatalf("expected managed cover url")
			}
		})
	})
}

func TestMasterSettingsHandlerMicrosoftEntra(t *testing.T) {
	t.Run("upsert success", func(t *testing.T) {
		actor := []byte("abcdefghijklmnop")
		svc := &masterSettingsServiceMock{
			upsertMicrosoftFn: func(_ mastersettings.MicrosoftEntraConfigInput, _ []byte) (*mastersettings.MicrosoftEntraConfigView, error) {
				return &mastersettings.MicrosoftEntraConfigView{
					TenantID:           "tenant",
					ClientID:           "client",
					ClientSecretMasked: "********",
					RedirectURL:        "https://example.com/callback",
					Authority:          "https://login.microsoftonline.com/",
					Scopes:             "openid,profile,email",
				}, nil
			},
		}
		h := NewMasterSettingsHandler(svc, nil, nil)
		app := newMasterSettingsTestApp(h, actor)
		body := []byte(`{"data":{"type":"master_settings_microsoft_entra_upsert","attributes":{"MS_ENTRA_TENANT_ID":"tenant","MS_ENTRA_CLIENT_ID":"client","MS_ENTRA_CLIENT_SECRET":"top-secret","MS_ENTRA_REDIRECT_URL":"https://example.com/callback","MS_ENTRA_AUTHORITY":"https://login.microsoftonline.com/","MS_ENTRA_SCOPES":"openid,profile,email"}}}`)
		resp, _ := doJSONRequestMasterSettings(t, app, http.MethodPut, "/api/v1/master-settings/sso/microsoft", body)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d", resp.StatusCode)
		}
		if svc.lastMicrosoftIn.ClientSecret != "top-secret" {
			t.Fatalf("expected secret forwarded")
		}
		if string(svc.lastMicrosoftActor) != string(actor) {
			t.Fatalf("expected actor forwarded")
		}
	})

	t.Run("upsert success with empty secret", func(t *testing.T) {
		svc := &masterSettingsServiceMock{
			upsertMicrosoftFn: func(_ mastersettings.MicrosoftEntraConfigInput, _ []byte) (*mastersettings.MicrosoftEntraConfigView, error) {
				return &mastersettings.MicrosoftEntraConfigView{
					TenantID:           "tenant",
					ClientID:           "client",
					ClientSecretMasked: "********",
					RedirectURL:        "https://example.com/callback",
					Authority:          "https://login.microsoftonline.com/",
					Scopes:             "openid,profile,email",
				}, nil
			},
		}
		h := NewMasterSettingsHandler(svc, nil, nil)
		app := newMasterSettingsTestApp(h, nil)
		body := []byte(`{"data":{"type":"master_settings_microsoft_entra_upsert","attributes":{"MS_ENTRA_TENANT_ID":"tenant","MS_ENTRA_CLIENT_ID":"client","MS_ENTRA_CLIENT_SECRET":"","MS_ENTRA_REDIRECT_URL":"https://example.com/callback","MS_ENTRA_AUTHORITY":"https://login.microsoftonline.com/","MS_ENTRA_SCOPES":"openid,profile,email"}}}`)
		resp, _ := doJSONRequestMasterSettings(t, app, http.MethodPut, "/api/v1/master-settings/sso/microsoft", body)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d", resp.StatusCode)
		}
		if svc.lastMicrosoftIn.ClientSecret != "" {
			t.Fatalf("expected empty secret forwarded")
		}
	})

	t.Run("upsert validation error", func(t *testing.T) {
		svc := &masterSettingsServiceMock{}
		h := NewMasterSettingsHandler(svc, nil, nil)
		app := newMasterSettingsTestApp(h, nil)
		body := []byte(`{"data":{"type":"master_settings_microsoft_entra_upsert","attributes":{"MS_ENTRA_TENANT_ID":"","MS_ENTRA_CLIENT_ID":"client","MS_ENTRA_CLIENT_SECRET":"secret","MS_ENTRA_REDIRECT_URL":"https://example.com/callback","MS_ENTRA_AUTHORITY":"https://login.microsoftonline.com/","MS_ENTRA_SCOPES":"openid"}}}`)
		resp, _ := doJSONRequestMasterSettings(t, app, http.MethodPut, "/api/v1/master-settings/sso/microsoft", body)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})

	t.Run("get success", func(t *testing.T) {
		svc := &masterSettingsServiceMock{
			getMicrosoftFn: func() (*mastersettings.MicrosoftEntraConfigView, error) {
				return &mastersettings.MicrosoftEntraConfigView{
					TenantID:           "tenant",
					ClientID:           "client",
					ClientSecretMasked: "********",
					RedirectURL:        "https://example.com/callback",
					Authority:          "https://login.microsoftonline.com/",
					Scopes:             "openid,profile,email",
				}, nil
			},
		}
		h := NewMasterSettingsHandler(svc, nil, nil)
		app := newMasterSettingsTestApp(h, nil)
		resp, _ := doJSONRequestMasterSettings(t, app, http.MethodGet, "/api/v1/master-settings/sso/microsoft", nil)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d", resp.StatusCode)
		}
	})

	t.Run("get not found", func(t *testing.T) {
		svc := &masterSettingsServiceMock{
			getMicrosoftFn: func() (*mastersettings.MicrosoftEntraConfigView, error) {
				return nil, nil
			},
		}
		h := NewMasterSettingsHandler(svc, nil, nil)
		app := newMasterSettingsTestApp(h, nil)
		resp, _ := doJSONRequestMasterSettings(t, app, http.MethodGet, "/api/v1/master-settings/sso/microsoft", nil)
		if resp.StatusCode != http.StatusNotFound {
			t.Fatalf("expected 404, got %d", resp.StatusCode)
		}
	})

	t.Run("upsert invalid json", func(t *testing.T) {
		h := NewMasterSettingsHandler(&masterSettingsServiceMock{}, nil, nil)
		app := newMasterSettingsTestApp(h, nil)
		resp, _ := doJSONRequestMasterSettings(t, app, http.MethodPut, "/api/v1/master-settings/sso/microsoft", []byte(`{"data":`))
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})

	t.Run("upsert service validation error is treated as 422", func(t *testing.T) {
		svc := &masterSettingsServiceMock{
			upsertMicrosoftFn: func(_ mastersettings.MicrosoftEntraConfigInput, _ []byte) (*mastersettings.MicrosoftEntraConfigView, error) {
				return nil, errors.New("MS_ENTRA_REDIRECT_URL must be valid absolute URL")
			},
		}
		h := NewMasterSettingsHandler(svc, nil, nil)
		app := newMasterSettingsTestApp(h, nil)
		body := []byte(`{"data":{"type":"master_settings_microsoft_entra_upsert","attributes":{"MS_ENTRA_TENANT_ID":"tenant","MS_ENTRA_CLIENT_ID":"client","MS_ENTRA_CLIENT_SECRET":"secret","MS_ENTRA_REDIRECT_URL":"https://example.com/callback","MS_ENTRA_AUTHORITY":"https://login.microsoftonline.com/","MS_ENTRA_SCOPES":"openid"}}}`)
		resp, _ := doJSONRequestMasterSettings(t, app, http.MethodPut, "/api/v1/master-settings/sso/microsoft", body)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})

	t.Run("upsert service generic error is treated as 400", func(t *testing.T) {
		svc := &masterSettingsServiceMock{
			upsertMicrosoftFn: func(_ mastersettings.MicrosoftEntraConfigInput, _ []byte) (*mastersettings.MicrosoftEntraConfigView, error) {
				return nil, errors.New("storage unavailable")
			},
		}
		h := NewMasterSettingsHandler(svc, nil, nil)
		app := newMasterSettingsTestApp(h, nil)
		body := []byte(`{"data":{"type":"master_settings_microsoft_entra_upsert","attributes":{"MS_ENTRA_TENANT_ID":"tenant","MS_ENTRA_CLIENT_ID":"client","MS_ENTRA_CLIENT_SECRET":"secret","MS_ENTRA_REDIRECT_URL":"https://example.com/callback","MS_ENTRA_AUTHORITY":"https://login.microsoftonline.com/","MS_ENTRA_SCOPES":"openid"}}}`)
		resp, _ := doJSONRequestMasterSettings(t, app, http.MethodPut, "/api/v1/master-settings/sso/microsoft", body)
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})

	t.Run("microsoft entra helper functions", func(t *testing.T) {
		resource := NewMasterSettingsHandler(&masterSettingsServiceMock{}, nil, nil).microsoftEntraResource(nil)
		if resource.Type != "master_settings_microsoft_entra_sso" {
			t.Fatalf("unexpected resource type %q", resource.Type)
		}
		if !isMicrosoftEntraValidationError(errors.New("required field missing")) {
			t.Fatalf("expected required error to be validation error")
		}
		if !isMicrosoftEntraValidationError(errors.New("MS_ENTRA_CLIENT_ID invalid")) {
			t.Fatalf("expected MS_ENTRA error to be validation error")
		}
		if isMicrosoftEntraValidationError(errors.New("network failed")) {
			t.Fatalf("expected generic error to be non-validation")
		}
	})

	t.Run("managed file asset helpers expose thumbnail and size", func(t *testing.T) {
		fileID := uuidv7.MustBytes()
		fileUUID := mustUUIDStringMasterSettings(t, fileID)
		thumbKey := "images/logo-thumb.png"
		fileRepo := &masterSettingsFileRepoMock{
			getByIDRow: &filemanager.File{
				ID:                 fileID,
				Status:             filemanager.FileStatusReady,
				ObjectKey:          "images/logo.png",
				ThumbnailObjectKey: &thumbKey,
				SizeBytes:          400,
				ThumbnailSizeBytes: 120,
			},
		}
		h := NewMasterSettingsHandler(&masterSettingsServiceMock{}, newMasterSettingsFileService(fileRepo), &masterSettingsStorageMock{presignURL: "https://signed/logo.png"})
		row := &mastersettings.MasterSettings{LogoFileID: fileID, CoverFileID: fileID}
		withFiberCtx := func(t *testing.T, fn func(*fiber.Ctx)) {
			t.Helper()
			app := fiber.New()
			app.Get("/", func(c *fiber.Ctx) error {
				fn(c)
				return c.SendStatus(fiber.StatusOK)
			})
			req, _ := http.NewRequest(http.MethodGet, "/", nil)
			if _, err := app.Test(req); err != nil {
				t.Fatalf("app.Test: %v", err)
			}
		}
		withFiberCtx(t, func(c *fiber.Ctx) {
			if got := h.resolveLogoThumbnailURL(c, row, &fileUUID); got == nil || *got == "" {
				t.Fatalf("expected logo thumbnail url")
			}
			if got := h.resolveCoverThumbnailURL(c, row, &fileUUID); got == nil || *got == "" {
				t.Fatalf("expected cover thumbnail url")
			}
			if got := h.resolveLogoOriginalSize(c, row, &fileUUID); got == nil || *got != 400 {
				t.Fatalf("expected logo original size 400, got %v", got)
			}
			if got := h.resolveCoverOriginalSize(c, row, &fileUUID); got == nil || *got != 400 {
				t.Fatalf("expected cover original size 400, got %v", got)
			}
		})
	})

	t.Run("managed file helper nil branches", func(t *testing.T) {
		h := NewMasterSettingsHandler(&masterSettingsServiceMock{}, nil, nil)
		withFiberCtx := func(t *testing.T, fn func(*fiber.Ctx)) {
			t.Helper()
			app := fiber.New()
			app.Get("/", func(c *fiber.Ctx) error {
				fn(c)
				return c.SendStatus(fiber.StatusOK)
			})
			req, _ := http.NewRequest(http.MethodGet, "/", nil)
			if _, err := app.Test(req); err != nil {
				t.Fatalf("app.Test: %v", err)
			}
		}
		withFiberCtx(t, func(c *fiber.Ctx) {
			if got := h.resolveLogoThumbnailURL(c, nil, nil); got != nil {
				t.Fatalf("expected nil logo thumbnail for nil row")
			}
			if got := h.resolveCoverThumbnailURL(c, nil, nil); got != nil {
				t.Fatalf("expected nil cover thumbnail for nil row")
			}
			if got := h.resolveLogoOriginalSize(c, nil, nil); got != nil {
				t.Fatalf("expected nil logo size for nil row")
			}
			if got := h.resolveCoverOriginalSize(c, nil, nil); got != nil {
				t.Fatalf("expected nil cover size for nil row")
			}
		})
		if isMicrosoftEntraValidationError(nil) {
			t.Fatalf("expected nil error to be non-validation")
		}
	})
}
