package handlers

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

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

	filemanager "wucher/internal/domain/file_manager"
	"wucher/internal/domain/user"
	filemanagerrealtime "wucher/internal/realtime/filemanager"
	"wucher/internal/service"
	"wucher/internal/shared/pkg/userctx"
	"wucher/internal/shared/pkg/uuidv7"
)

type memUserRepo struct {
	users map[string]*user.User

	createErr error
	updateErr error
	deleteErr error
	getErr    error
	listErr   error

	listUsers []user.User
	listTotal int64
}

func newMemUserRepo() *memUserRepo {
	return &memUserRepo{users: map[string]*user.User{}}
}

func (r *memUserRepo) Create(_ context.Context, u *user.User) error {
	if r.createErr != nil {
		return r.createErr
	}
	if len(u.ID) == 0 {
		u.ID = uuidv7.MustBytes()
	}
	r.users[string(u.ID)] = u
	return nil
}

func (r *memUserRepo) Update(_ context.Context, u *user.User) error {
	if r.updateErr != nil {
		return r.updateErr
	}
	r.users[string(u.ID)] = u
	return nil
}

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

func (r *memUserRepo) GetByID(_ context.Context, id []byte) (*user.User, error) {
	if r.getErr != nil {
		return nil, r.getErr
	}
	return r.users[string(id)], nil
}

func (r *memUserRepo) GetByEmail(_ context.Context, _ string) (*user.User, error)    { return nil, nil }
func (r *memUserRepo) GetByUsername(_ context.Context, _ string) (*user.User, error) { return nil, nil }

func (r *memUserRepo) List(_ context.Context, _ string, _ string, _ int, _ int) ([]user.User, int64, error) {
	if r.listErr != nil {
		return nil, 0, r.listErr
	}
	return r.listUsers, r.listTotal, nil
}

func (r *memUserRepo) GetLastNameByID(_ context.Context, _ []byte) (string, error) {
	return "", nil
}

func TestNewUserHandler(t *testing.T) {
	h := NewUserHandler(service.NewUserService(newMemUserRepo()))
	if h == nil || h.validate == nil {
		t.Fatalf("expected handler and validator initialized")
	}
}

func TestUserHandler_Create(t *testing.T) {
	t.Run("success", func(t *testing.T) {
		repo := newMemUserRepo()
		svc := service.NewUserService(repo)
		h := NewUserHandler(svc)
		app := fiber.New()
		actor := uuidv7.MustBytes()
		app.Use(func(c *fiber.Ctx) error {
			c.Locals("user_id", actor)
			return c.Next()
		})
		app.Post("/api/v1/users/create", h.Create)

		roleID, _ := uuidv7.BytesToString(uuidv7.MustBytes())
		payload := []byte(`{"data":{"type":"user","attributes":{"email":"u@e.com","username":"john.doe","first_name":"John","last_name":"Doe","role_id":"` + roleID + `"}}}`)
		req, _ := http.NewRequest(http.MethodPost, "/api/v1/users/create", bytes.NewReader(payload))
		req.Header.Set("Content-Type", "application/json")
		resp, err := app.Test(req)
		if err != nil {
			t.Fatalf("app.Test: %v", err)
		}
		if resp.StatusCode != http.StatusCreated {
			t.Fatalf("expected 201, got %d", resp.StatusCode)
		}
		body, _ := io.ReadAll(resp.Body)
		if !bytes.Contains(body, []byte(`"username":"john.doe"`)) {
			t.Fatalf("expected response to include username, got %s", string(body))
		}
		if len(repo.users) != 1 {
			t.Fatalf("expected 1 created user, got %d", len(repo.users))
		}
		for _, created := range repo.users {
			if created.Username != "john.doe" {
				t.Fatalf("expected username persisted, got %q", created.Username)
			}
			if created.Timezone != "UTC" {
				t.Fatalf("expected default timezone UTC, got %q", created.Timezone)
			}
			if string(created.CreatedBy) != string(actor) {
				t.Fatalf("expected created_by from actor")
			}
		}
	})

	t.Run("success with role_ids", func(t *testing.T) {
		repo := newMemUserRepo()
		svc := service.NewUserService(repo)
		h := NewUserHandler(svc)
		app := fiber.New()
		app.Post("/api/v1/users/create", h.Create)

		roleID, _ := uuidv7.BytesToString(uuidv7.MustBytes())
		extraRoleID, _ := uuidv7.BytesToString(uuidv7.MustBytes())
		payload := []byte(`{"data":{"type":"user","attributes":{"email":"roles@e.com","username":"roles.user","first_name":"Roles","last_name":"User","role_id":"` + roleID + `","role_ids":["` + roleID + `","` + extraRoleID + `"]}}}`)
		req, _ := http.NewRequest(http.MethodPost, "/api/v1/users/create", bytes.NewReader(payload))
		req.Header.Set("Content-Type", "application/json")
		resp, err := app.Test(req)
		if err != nil {
			t.Fatalf("app.Test: %v", err)
		}
		if resp.StatusCode != http.StatusCreated {
			t.Fatalf("expected 201, got %d", resp.StatusCode)
		}
		for _, created := range repo.users {
			if len(created.RoleIDs) != 2 {
				t.Fatalf("expected 2 role ids persisted, got %d", len(created.RoleIDs))
			}
		}
	})

	t.Run("invalid json", func(t *testing.T) {
		repo := newMemUserRepo()
		svc := service.NewUserService(repo)
		h := NewUserHandler(svc)
		app := fiber.New()
		app.Post("/api/v1/users/create", h.Create)
		req, _ := http.NewRequest(http.MethodPost, "/api/v1/users/create", bytes.NewReader([]byte(`{"data":`)))
		req.Header.Set("Content-Type", "application/json")
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})

	t.Run("invalid role id", func(t *testing.T) {
		repo := newMemUserRepo()
		svc := service.NewUserService(repo)
		h := NewUserHandler(svc)
		app := fiber.New()
		app.Post("/api/v1/users/create", h.Create)
		payload := []byte(`{"data":{"type":"user","attributes":{"email":"u@e.com","username":"john.doe","first_name":"John","last_name":"Doe","role_id":"bad"}}}`)
		req, _ := http.NewRequest(http.MethodPost, "/api/v1/users/create", bytes.NewReader(payload))
		req.Header.Set("Content-Type", "application/json")
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})

	t.Run("invalid image_uuid requires file manager and returns bad request", func(t *testing.T) {
		repo := newMemUserRepo()
		svc := service.NewUserService(repo)
		h := NewUserHandler(svc)
		app := fiber.New()
		app.Post("/api/v1/users/create", h.Create)
		roleID, _ := uuidv7.BytesToString(uuidv7.MustBytes())
		payload := []byte(`{"data":{"type":"user","attributes":{"email":"u@e.com","username":"john.doe","first_name":"John","last_name":"Doe","role_id":"` + roleID + `","image_uuid":"bad"}}}`)
		req, _ := http.NewRequest(http.MethodPost, "/api/v1/users/create", bytes.NewReader(payload))
		req.Header.Set("Content-Type", "application/json")
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})

	t.Run("image_uuid requires file manager service", func(t *testing.T) {
		repo := newMemUserRepo()
		svc := service.NewUserService(repo)
		h := NewUserHandler(svc)
		app := fiber.New()
		app.Post("/api/v1/users/create", h.Create)
		roleID, _ := uuidv7.BytesToString(uuidv7.MustBytes())
		fileUUID, _ := uuidv7.BytesToString(uuidv7.MustBytes())
		payload := []byte(`{"data":{"type":"user","attributes":{"email":"u@e.com","username":"john.doe","first_name":"John","last_name":"Doe","role_id":"` + roleID + `","image_uuid":"` + fileUUID + `"}}}`)
		req, _ := http.NewRequest(http.MethodPost, "/api/v1/users/create", bytes.NewReader(payload))
		req.Header.Set("Content-Type", "application/json")
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})

	t.Run("invalid role_ids", func(t *testing.T) {
		repo := newMemUserRepo()
		svc := service.NewUserService(repo)
		h := NewUserHandler(svc)
		app := fiber.New()
		app.Post("/api/v1/users/create", h.Create)
		roleID, _ := uuidv7.BytesToString(uuidv7.MustBytes())
		payload := []byte(`{"data":{"type":"user","attributes":{"email":"u@e.com","username":"john.doe","first_name":"John","last_name":"Doe","role_id":"` + roleID + `","role_ids":["bad"]}}}`)
		req, _ := http.NewRequest(http.MethodPost, "/api/v1/users/create", bytes.NewReader(payload))
		req.Header.Set("Content-Type", "application/json")
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})

	t.Run("invalid timezone", func(t *testing.T) {
		repo := newMemUserRepo()
		svc := service.NewUserService(repo)
		h := NewUserHandler(svc)
		app := fiber.New()
		app.Post("/api/v1/users/create", h.Create)
		roleID, _ := uuidv7.BytesToString(uuidv7.MustBytes())
		payload := []byte(`{"data":{"type":"user","attributes":{"email":"u@e.com","username":"john.doe","first_name":"John","last_name":"Doe","timezone":"UTC+7","role_id":"` + roleID + `"}}}`)
		req, _ := http.NewRequest(http.MethodPost, "/api/v1/users/create", bytes.NewReader(payload))
		req.Header.Set("Content-Type", "application/json")
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})

	t.Run("create service error", func(t *testing.T) {
		repo := newMemUserRepo()
		repo.createErr = errors.New("db error")
		svc := service.NewUserService(repo)
		h := NewUserHandler(svc)
		app := fiber.New()
		app.Post("/api/v1/users/create", h.Create)
		roleID, _ := uuidv7.BytesToString(uuidv7.MustBytes())
		payload := []byte(`{"data":{"type":"user","attributes":{"email":"u@e.com","username":"john.doe","first_name":"John","last_name":"Doe","role_id":"` + roleID + `"}}}`)
		req, _ := http.NewRequest(http.MethodPost, "/api/v1/users/create", bytes.NewReader(payload))
		req.Header.Set("Content-Type", "application/json")
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})
}

func TestUserEventsStream(t *testing.T) {
	hub := filemanagerrealtime.NewHub(1, nil)
	actor := uuidv7.MustBytes()
	userUUID, _ := uuidv7.BytesToString(actor)

	app := fiber.New()
	fctx := &fasthttp.RequestCtx{}
	fctx.Request.SetRequestURI("/api/v1/users/events")
	c := app.AcquireCtx(fctx)
	c.Locals("user_id", actor)
	if err := NewFileManagerEventsHandler(hub).Stream(c); err != nil {
		t.Fatalf("unexpected stream setup error: %v", err)
	}

	hub.PublishToUsers([]string{userUUID}, filemanagerrealtime.FileStatusEvent{
		FileUUID:  mustUUIDStringBase(t, uuidv7.MustBytes()),
		Status:    "ready",
		Name:      "avatar.png",
		MimeType:  "image/png",
		UpdatedAt: time.Now().UTC().Format(time.RFC3339),
	})
	go func() {
		time.Sleep(10 * time.Millisecond)
		closeFirstHubSubscriberForUser(hub, userUUID)
	}()

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

func TestUserHandler_Update(t *testing.T) {
	repo := newMemUserRepo()
	userID := uuidv7.MustBytes()
	roleID := uuidv7.MustBytes()
	repo.users[string(userID)] = &user.User{ID: userID, Email: "u@e.com", FirstName: "John", LastName: "Doe", RoleID: roleID}

	svc := service.NewUserService(repo)
	h := NewUserHandler(svc)
	app := fiber.New()
	app.Patch("/api/v1/users/update/:uuid", h.Update)

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

	t.Run("empty image_uuid clears existing attachment", func(t *testing.T) {
		idStr, _ := uuidv7.BytesToString(userID)
		imageID := uuidv7.MustBytes()
		repo.users[string(userID)].ProfileAttachmentID = append([]byte(nil), imageID...)

		payload := []byte(`{"data":{"type":"user","id":"` + idStr + `","attributes":{"first_name":"Jane","image_uuid":""}}}`)
		req, _ := http.NewRequest(http.MethodPatch, "/api/v1/users/update/"+idStr, bytes.NewReader(payload))
		req.Header.Set("Content-Type", "application/json")
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d", resp.StatusCode)
		}
		if repo.users[string(userID)].ProfileAttachmentID != nil {
			t.Fatalf("expected profile attachment to be cleared")
		}
	})

	t.Run("null image_uuid clears existing attachment", func(t *testing.T) {
		idStr, _ := uuidv7.BytesToString(userID)
		imageID := uuidv7.MustBytes()
		repo.users[string(userID)].ProfileAttachmentID = append([]byte(nil), imageID...)

		payload := []byte(`{"data":{"type":"user","id":"` + idStr + `","attributes":{"first_name":"Jane","image_uuid":null}}}`)
		req, _ := http.NewRequest(http.MethodPatch, "/api/v1/users/update/"+idStr, bytes.NewReader(payload))
		req.Header.Set("Content-Type", "application/json")
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d", resp.StatusCode)
		}
		if repo.users[string(userID)].ProfileAttachmentID != nil {
			t.Fatalf("expected profile attachment to be cleared on null image_uuid")
		}
	})

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

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

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

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

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

	t.Run("invalid image_uuid requires file manager and returns bad request", func(t *testing.T) {
		idStr, _ := uuidv7.BytesToString(userID)
		payload := []byte(`{"data":{"type":"user","id":"` + idStr + `","attributes":{"image_uuid":"bad"}}}`)
		req, _ := http.NewRequest(http.MethodPatch, "/api/v1/users/update/"+idStr, bytes.NewReader(payload))
		req.Header.Set("Content-Type", "application/json")
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})

	t.Run("image_uuid requires file manager service", func(t *testing.T) {
		idStr, _ := uuidv7.BytesToString(userID)
		fileUUID, _ := uuidv7.BytesToString(uuidv7.MustBytes())
		payload := []byte(`{"data":{"type":"user","id":"` + idStr + `","attributes":{"image_uuid":"` + fileUUID + `"}}}`)
		req, _ := http.NewRequest(http.MethodPatch, "/api/v1/users/update/"+idStr, bytes.NewReader(payload))
		req.Header.Set("Content-Type", "application/json")
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})

	t.Run("image_uuid accepts existing attachment id", func(t *testing.T) {
		idStr, _ := uuidv7.BytesToString(userID)
		attachmentID := uuidv7.MustBytes()
		attachmentIDStr, _ := uuidv7.BytesToString(attachmentID)
		h.WithFileManagerService(&fileManagerAttachmentHandlerServiceMock{
			getAttachmentFn: func(_ context.Context, id []byte) (*filemanager.Attachment, error) {
				if string(id) == string(attachmentID) {
					return &filemanager.Attachment{ID: attachmentID, FileID: uuidv7.MustBytes()}, nil
				}
				return nil, nil
			},
		})
		payload := []byte(`{"data":{"type":"user","id":"` + idStr + `","attributes":{"image_uuid":"` + attachmentIDStr + `"}}}`)
		req, _ := http.NewRequest(http.MethodPatch, "/api/v1/users/update/"+idStr, bytes.NewReader(payload))
		req.Header.Set("Content-Type", "application/json")
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d", resp.StatusCode)
		}
		if string(repo.users[string(userID)].ProfileAttachmentID) != string(attachmentID) {
			t.Fatalf("expected profile_attachment_id updated with attachment id")
		}
	})

	t.Run("image_uuid file uuid replaces existing profile attachment", func(t *testing.T) {
		idStr, _ := uuidv7.BytesToString(userID)
		oldAttachmentID := uuidv7.MustBytes()
		newAttachmentID := uuidv7.MustBytes()
		newFileID := uuidv7.MustBytes()
		newFileIDStr, _ := uuidv7.BytesToString(newFileID)
		repo.users[string(userID)].ProfileAttachmentID = append([]byte(nil), oldAttachmentID...)

		h.WithFileManagerService(&fileManagerAttachmentHandlerServiceMock{
			getAttachmentFn: func(_ context.Context, id []byte) (*filemanager.Attachment, error) {
				// Incoming image_uuid is file UUID, not attachment UUID.
				if string(id) == string(newFileID) {
					return nil, nil
				}
				return nil, nil
			},
			listAttachmentsFn: func(_ context.Context, _ filemanager.ListAttachmentsParams) ([]filemanager.Attachment, int64, error) {
				return []filemanager.Attachment{{ID: oldAttachmentID, FileID: uuidv7.MustBytes()}}, 1, nil
			},
			deleteAttachmentFn: func(_ context.Context, id []byte) error {
				if string(id) != string(oldAttachmentID) {
					t.Fatalf("unexpected deleted attachment id")
				}
				return nil
			},
			createAttachmentFn: func(_ context.Context, p filemanager.CreateAttachmentParams) (*filemanager.Attachment, error) {
				if string(p.FileID) != string(newFileID) {
					t.Fatalf("expected create attachment from new file id")
				}
				return &filemanager.Attachment{ID: newAttachmentID, FileID: newFileID}, nil
			},
		})

		payload := []byte(`{"data":{"type":"user","id":"` + idStr + `","attributes":{"image_uuid":"` + newFileIDStr + `"}}}`)
		req, _ := http.NewRequest(http.MethodPatch, "/api/v1/users/update/"+idStr, bytes.NewReader(payload))
		req.Header.Set("Content-Type", "application/json")
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusOK {
			body, _ := io.ReadAll(resp.Body)
			t.Fatalf("expected 200, got %d body=%s", resp.StatusCode, string(body))
		}
		if string(repo.users[string(userID)].ProfileAttachmentID) != string(newAttachmentID) {
			t.Fatalf("expected profile_attachment_id replaced with new attachment id")
		}
	})

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

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

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

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

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

	t.Run("update timezone success", func(t *testing.T) {
		idStr, _ := uuidv7.BytesToString(userID)
		payload := []byte(`{"data":{"type":"user","id":"` + idStr + `","attributes":{"timezone":"Asia/Jakarta"}}}`)
		req, _ := http.NewRequest(http.MethodPatch, "/api/v1/users/update/"+idStr, bytes.NewReader(payload))
		req.Header.Set("Content-Type", "application/json")
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d", resp.StatusCode)
		}
		if got := repo.users[string(userID)].Timezone; got != "Asia/Jakarta" {
			t.Fatalf("expected timezone updated, got %q", got)
		}
	})

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

func TestUserHandler_DeleteGetListDatatable(t *testing.T) {
	repo := newMemUserRepo()
	roleID := uuidv7.MustBytes()
	userID := uuidv7.MustBytes()
	repo.users[string(userID)] = &user.User{ID: userID, Email: "u@e.com", FirstName: "John", LastName: "Doe", RoleID: roleID}
	repo.listUsers = []user.User{{ID: userID, Email: "u@e.com", FirstName: "John", LastName: "Doe", RoleID: roleID}}
	repo.listTotal = 1

	svc := service.NewUserService(repo)
	h := NewUserHandler(svc)
	app := fiber.New()
	app.Delete("/api/v1/users/delete/:uuid", h.Delete)
	app.Get("/api/v1/users/get/:uuid", h.Get)
	app.Get("/api/v1/users/get-all", h.List)
	app.Get("/api/v1/users/get-all/dt", h.ListDatatable)

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

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

	t.Run("delete service error", func(t *testing.T) {
		failingID := uuidv7.MustBytes()
		repo.users[string(failingID)] = &user.User{ID: failingID, Email: "fail@e.com", RoleID: roleID}
		repo.deleteErr = errors.New("delete failed")
		defer func() { repo.deleteErr = nil }()

		idStr, _ := uuidv7.BytesToString(failingID)
		req, _ := http.NewRequest(http.MethodDelete, "/api/v1/users/delete/"+idStr, nil)
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})

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

	t.Run("get success", func(t *testing.T) {
		id := uuidv7.MustBytes()
		repo.users[string(id)] = &user.User{ID: id, Email: "a@b.com", FirstName: "A", LastName: "B", RoleID: roleID}
		idStr, _ := uuidv7.BytesToString(id)
		req, _ := http.NewRequest(http.MethodGet, "/api/v1/users/get/"+idStr, nil)
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d", resp.StatusCode)
		}
	})

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

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

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

func TestUserResource(t *testing.T) {
	now := time.Now()
	primaryRoleID := uuidv7.MustBytes()
	extraRoleID := uuidv7.MustBytes()
	u := &user.User{
		ID:                  uuidv7.MustBytes(),
		Email:               "u@e.com",
		FirstName:           "John",
		LastName:            "Doe",
		MobilePhone:         "08123",
		ProfileAttachmentID: uuidv7.MustBytes(),
		RoleID:              primaryRoleID,
		RoleName:            "staff",
		RoleDescription:     "Staff role",
		RoleIDs:             [][]byte{primaryRoleID, extraRoleID},
		Roles: []user.RoleSummary{
			{ID: primaryRoleID, Name: "staff", Description: "Staff role"},
			{ID: extraRoleID, Name: "chief_pilot", Description: "Chief Pilot role"},
		},
		CreatedBy:       uuidv7.MustBytes(),
		EmailVerifiedAt: &now,
	}
	h := NewUserHandler(service.NewUserService(newMemUserRepo()))
	r := h.userResource(context.Background(), u)
	if r.Type != "user" || r.Attributes.Role.Name != "staff" || r.Attributes.EmailVerifiedAt == "" {
		t.Fatalf("unexpected user resource: %#v", r)
	}
	if len(r.Attributes.RoleIDs) == 0 || len(r.Attributes.Roles) == 0 {
		t.Fatalf("expected role_ids and roles in response: %#v", r.Attributes)
	}
	if r.Attributes.Timezone != "UTC" {
		t.Fatalf("expected default timezone UTC, got %q", r.Attributes.Timezone)
	}
	if r.Attributes.CreatedBy == "" {
		t.Fatalf("expected created_by in response")
	}
	if r.Attributes.ProfilePicture == nil || r.Attributes.ProfilePicture.UUID == "" {
		t.Fatalf("expected profile_picture in response")
	}
}

type staticUserNameGetter struct {
	lastName string
}

func (g staticUserNameGetter) GetLastName(context.Context, []byte) (string, error) {
	return g.lastName, nil
}

func TestUserHandler_HelperBranches(t *testing.T) {
	t.Run("with setters return same handler", func(t *testing.T) {
		h := NewUserHandler(service.NewUserService(newMemUserRepo()))
		if h.WithFileManagerService(nil) != h {
			t.Fatalf("expected WithFileManagerService to return same handler")
		}
		if h.WithFileStorage(nil) != h {
			t.Fatalf("expected WithFileStorage to return same handler")
		}
	})

	t.Run("resolve profile image with nil file uuid is ignored", func(t *testing.T) {
		h := NewUserHandler(service.NewUserService(newMemUserRepo()))
		app := fiber.New()
		app.Get("/", func(c *fiber.Ctx) error {
			got, handled := h.resolveProfileImageAttachmentID(c, uuidv7.MustBytes(), nil)
			if handled {
				t.Fatalf("expected nil file uuid to be ignored")
			}
			if got != nil {
				t.Fatalf("expected nil attachment id")
			}
			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("parse optional image_uuid from raw body", func(t *testing.T) {
		state := parseOptionalStringFieldFromJSONBody([]byte(`{"data":{"attributes":{"image_uuid":null}}}`), "image_uuid")
		if !state.Set || state.Valid {
			t.Fatalf("expected image_uuid null to be set+invalid, got %+v", state)
		}

		state = parseOptionalStringFieldFromJSONBody([]byte(`{"data":{"attributes":{"image_uuid":"abc"}}}`), "image_uuid")
		if !state.Set || !state.Valid || state.Value != "abc" {
			t.Fatalf("expected image_uuid string to be set+valid, got %+v", state)
		}

		state = parseOptionalStringFieldFromJSONBody([]byte(`{"data":{"attributes":{"other":"x"}}}`), "image_uuid")
		if state.Set {
			t.Fatalf("expected image_uuid absent to be unset, got %+v", state)
		}
	})

	t.Run("contains role id matches only valid uuid bytes", func(t *testing.T) {
		roleID := uuidv7.MustBytes()
		if !containsRoleID([][]byte{roleID}, roleID) {
			t.Fatalf("expected containsRoleID to match")
		}
		if containsRoleID([][]byte{roleID}, []byte("short")) {
			t.Fatalf("expected invalid role id bytes to fail")
		}
		if containsRoleID([][]byte{roleID}, uuidv7.MustBytes()) {
			t.Fatalf("expected non-member role id to fail")
		}
	})

	t.Run("wire user name getter forwards to userctx", func(t *testing.T) {
		prev := userctx.GlobalUserNameGetter
		defer func() { userctx.GlobalUserNameGetter = prev }()

		getter := staticUserNameGetter{lastName: "Doe"}
		WireUserNameGetter(getter)
		if userctx.GlobalUserNameGetter == nil {
			t.Fatalf("expected global getter configured")
		}
		got, err := userctx.GlobalUserNameGetter.GetLastName(context.Background(), uuidv7.MustBytes())
		if err != nil {
			t.Fatalf("GetLastName: %v", err)
		}
		if got != "Doe" {
			t.Fatalf("expected wired getter result, got %q", got)
		}
	})

	t.Run("profile picture download url resolves when storage exists", func(t *testing.T) {
		fileID := uuidv7.MustBytes()
		attachmentID := uuidv7.MustBytes()
		thumbnailKey := "avatars/avatar.thumb.png"
		u := &user.User{
			ID:                  uuidv7.MustBytes(),
			Email:               "u@e.com",
			FirstName:           "John",
			LastName:            "Doe",
			ProfileAttachmentID: attachmentID,
			ProfileAttachment: &filemanager.Attachment{
				ID: attachmentID,
				File: &filemanager.File{
					ID:                 fileID,
					Name:               "avatar.png",
					ObjectKey:          "avatars/avatar.png",
					ThumbnailObjectKey: &thumbnailKey,
					SizeBytes:          123,
				},
			},
		}
		h := NewUserHandler(service.NewUserService(newMemUserRepo())).WithFileStorage(&fileManagerDownloadURLStorageMock{presignedURL: "https://signed/avatar.png"})
		res := h.userResource(context.Background(), u)
		if res.Attributes.ProfilePicture == nil || res.Attributes.ProfilePicture.DownloadURL != "https://signed/avatar.png" {
			t.Fatalf("expected signed profile picture download url, got %#v", res.Attributes.ProfilePicture)
		}
		if res.Attributes.ProfilePicture.ThumbnailDownloadURL != "https://signed/avatar.png" {
			t.Fatalf("expected signed profile picture thumbnail url, got %#v", res.Attributes.ProfilePicture)
		}
		if res.Attributes.ProfilePicture.OriginalSizeBytes == nil || *res.Attributes.ProfilePicture.OriginalSizeBytes != 123 {
			t.Fatalf("expected original size bytes 123, got %#v", res.Attributes.ProfilePicture)
		}
	})
}
