package handlers

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

	"github.com/gofiber/fiber/v2"

	"wucher/internal/domain/auth"
	"wucher/internal/service"
	"wucher/internal/shared/pkg/uuidv7"
)

type memRoleRepo struct {
	roles                map[string]*auth.Role
	permissions          map[string]*auth.Permission
	listRoles            []auth.Role
	listTotal            int64
	listErr              error
	listPermissions      []auth.Permission
	listPermissionsTotal int64
	listPermissionsErr   error

	createErr           error
	updateErr           error
	deleteErr           error
	getByIDErr          error
	getPermissionErr    error
	permissionNotFound  bool
	assignPermissionErr error
	removePermissionErr error
}

func newMemRoleRepo() *memRoleRepo {
	return &memRoleRepo{
		roles:       map[string]*auth.Role{},
		permissions: map[string]*auth.Permission{},
	}
}

func (r *memRoleRepo) CreateRole(_ context.Context, role *auth.Role) error {
	if len(role.ID) == 0 {
		role.ID = uuidv7.MustBytes()
	}
	r.roles[string(role.ID)] = role
	return r.createErr
}

func (r *memRoleRepo) UpdateRole(_ context.Context, role *auth.Role) error {
	r.roles[string(role.ID)] = role
	return r.updateErr
}

func (r *memRoleRepo) DeleteRole(_ context.Context, id []byte) error {
	delete(r.roles, string(id))
	return r.deleteErr
}

func (r *memRoleRepo) GetRoleByID(_ context.Context, id []byte) (*auth.Role, error) {
	return r.roles[string(id)], r.getByIDErr
}

func (r *memRoleRepo) GetRoleByName(_ context.Context, name string) (*auth.Role, error) {
	for _, role := range r.roles {
		if role.Name == name {
			return role, nil
		}
	}
	return nil, nil
}

func (r *memRoleRepo) ListRoles(_ context.Context, _ string, _ string, _ int, _ int) ([]auth.Role, int64, error) {
	return r.listRoles, r.listTotal, r.listErr
}

func (r *memRoleRepo) ListPermissions(_ context.Context, _ string, _ string, _ int, _ int) ([]auth.Permission, int64, error) {
	return r.listPermissions, r.listPermissionsTotal, r.listPermissionsErr
}
func (r *memRoleRepo) ListPermissionsByRoleID(_ context.Context, _ []byte) ([]auth.Permission, error) {
	return []auth.Permission{}, nil
}

func (r *memRoleRepo) HasRolePermission(_ context.Context, _ []byte, _ string) (bool, error) {
	return true, nil
}

func (r *memRoleRepo) GetPermissionByID(_ context.Context, id []byte) (*auth.Permission, error) {
	if r.permissionNotFound {
		return nil, r.getPermissionErr
	}
	if permission, ok := r.permissions[string(id)]; ok {
		return permission, r.getPermissionErr
	}
	return &auth.Permission{}, r.getPermissionErr
}

func (r *memRoleRepo) GetPermissionByKey(_ context.Context, key string) (*auth.Permission, error) {
	if r.permissionNotFound {
		return nil, r.getPermissionErr
	}
	for _, permission := range r.permissions {
		if permission.Key == key {
			return permission, r.getPermissionErr
		}
	}
	return &auth.Permission{}, r.getPermissionErr
}

func (r *memRoleRepo) UpdatePermission(_ context.Context, permission *auth.Permission) error {
	if permission != nil {
		r.permissions[string(permission.ID)] = permission
	}
	return r.updateErr
}

func (r *memRoleRepo) AssignPermission(_ context.Context, roleID, permissionID []byte) (*auth.RolePermission, error) {
	return &auth.RolePermission{ID: uuidv7.MustBytes(), RoleID: roleID, PermissionID: permissionID}, r.assignPermissionErr
}

func (r *memRoleRepo) RemovePermission(_ context.Context, _ []byte, _ []byte) error {
	return r.removePermissionErr
}

func TestRoleHandler_Update_MissingBodyID(t *testing.T) {
	repo := newMemRoleRepo()
	roleID := uuidv7.MustBytes()
	repo.roles[string(roleID)] = &auth.Role{ID: roleID, Name: "admin"}

	svc := service.NewRoleService(repo)
	h := NewRoleHandler(svc)
	app := fiber.New()
	app.Patch("/api/v1/roles/update/:uuid", h.Update)

	payload := []byte(`{"data":{"type":"role","attributes":{"name":"test_role"}}}`)
	idStr, _ := uuidv7.BytesToString(roleID)
	req, _ := http.NewRequest(http.MethodPatch, "/api/v1/roles/update/"+idStr, 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.StatusUnprocessableEntity {
		t.Fatalf("expected 422, got %d", resp.StatusCode)
	}
}

func TestRoleHandler_Update_IDMismatch(t *testing.T) {
	repo := newMemRoleRepo()
	roleID := uuidv7.MustBytes()
	repo.roles[string(roleID)] = &auth.Role{ID: roleID, Name: "admin"}

	svc := service.NewRoleService(repo)
	h := NewRoleHandler(svc)
	app := fiber.New()
	app.Patch("/api/v1/roles/update/:uuid", h.Update)

	pathID, _ := uuidv7.BytesToString(roleID)
	otherID, _ := uuidv7.BytesToString(uuidv7.MustBytes())
	payload := []byte(`{"data":{"type":"role","id":"` + otherID + `","attributes":{"name":"test_role"}}}`)
	req, _ := http.NewRequest(http.MethodPatch, "/api/v1/roles/update/"+pathID, 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.StatusUnprocessableEntity {
		t.Fatalf("expected 422, got %d", resp.StatusCode)
	}
}

func TestRoleHandler_Update_Success(t *testing.T) {
	repo := newMemRoleRepo()
	roleID := uuidv7.MustBytes()
	repo.roles[string(roleID)] = &auth.Role{ID: roleID, Name: "admin"}

	svc := service.NewRoleService(repo)
	h := NewRoleHandler(svc)
	app := fiber.New()
	app.Patch("/api/v1/roles/update/:uuid", h.Update)

	idStr, _ := uuidv7.BytesToString(roleID)
	payload := []byte(`{"data":{"type":"role","id":"` + idStr + `","attributes":{"name":"test_role"}}}`)
	req, _ := http.NewRequest(http.MethodPatch, "/api/v1/roles/update/"+idStr, 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.StatusOK {
		t.Fatalf("expected 200, got %d", resp.StatusCode)
	}
	if repo.roles[string(roleID)].Name != "test_role" {
		t.Fatalf("expected role name updated")
	}
}

func TestRoleHandler_Create_Success(t *testing.T) {
	repo := newMemRoleRepo()
	svc := service.NewRoleService(repo)
	h := NewRoleHandler(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/roles/create", h.Create)

	payload := []byte(`{"data":{"type":"role","attributes":{"name":"admin","description":"full"}}}`)
	req, _ := http.NewRequest(http.MethodPost, "/api/v1/roles/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.roles {
		if string(created.CreatedBy) != string(actor) {
			t.Fatalf("expected created_by from actor")
		}
	}
}

func TestRoleHandler_Create_InvalidJSON(t *testing.T) {
	repo := newMemRoleRepo()
	svc := service.NewRoleService(repo)
	h := NewRoleHandler(svc)
	app := fiber.New()
	app.Post("/api/v1/roles/create", h.Create)

	payload := []byte(`{"data":`)
	req, _ := http.NewRequest(http.MethodPost, "/api/v1/roles/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.StatusBadRequest {
		t.Fatalf("expected 400, got %d", resp.StatusCode)
	}
}

func TestRoleHandler_Create_ValidationError(t *testing.T) {
	repo := newMemRoleRepo()
	svc := service.NewRoleService(repo)
	h := NewRoleHandler(svc)
	app := fiber.New()
	app.Post("/api/v1/roles/create", h.Create)

	payload := []byte(`{"data":{"type":"role","attributes":{"name":""}}}`)
	req, _ := http.NewRequest(http.MethodPost, "/api/v1/roles/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.StatusUnprocessableEntity {
		t.Fatalf("expected 422, got %d", resp.StatusCode)
	}
}

func TestRoleHandler_Create_ServiceError(t *testing.T) {
	repo := newMemRoleRepo()
	repo.createErr = errors.New("db error")
	svc := service.NewRoleService(repo)
	h := NewRoleHandler(svc)
	app := fiber.New()
	app.Post("/api/v1/roles/create", h.Create)

	payload := []byte(`{"data":{"type":"role","attributes":{"name":"admin"}}}`)
	req, _ := http.NewRequest(http.MethodPost, "/api/v1/roles/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.StatusBadRequest {
		t.Fatalf("expected 400, got %d", resp.StatusCode)
	}
}

func TestRoleHandler_Delete_Success(t *testing.T) {
	repo := newMemRoleRepo()
	roleID := uuidv7.MustBytes()
	repo.roles[string(roleID)] = &auth.Role{ID: roleID, Name: "admin"}

	svc := service.NewRoleService(repo)
	h := NewRoleHandler(svc)
	app := fiber.New()
	app.Delete("/api/v1/roles/delete/:uuid", h.Delete)

	idStr, _ := uuidv7.BytesToString(roleID)
	req, _ := http.NewRequest(http.MethodDelete, "/api/v1/roles/delete/"+idStr, nil)
	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("app.Test: %v", err)
	}
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("expected 200, got %d", resp.StatusCode)
	}
}

func TestRoleHandler_Delete_InvalidID(t *testing.T) {
	repo := newMemRoleRepo()
	svc := service.NewRoleService(repo)
	h := NewRoleHandler(svc)
	app := fiber.New()
	app.Delete("/api/v1/roles/delete/:uuid", h.Delete)

	req, _ := http.NewRequest(http.MethodDelete, "/api/v1/roles/delete/not-a-uuid", nil)
	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("app.Test: %v", err)
	}
	if resp.StatusCode != http.StatusUnprocessableEntity {
		t.Fatalf("expected 422, got %d", resp.StatusCode)
	}
}

func TestRoleHandler_Delete_ServiceError(t *testing.T) {
	repo := newMemRoleRepo()
	repo.deleteErr = errors.New("db error")
	roleID := uuidv7.MustBytes()
	repo.roles[string(roleID)] = &auth.Role{ID: roleID, Name: "admin"}

	svc := service.NewRoleService(repo)
	h := NewRoleHandler(svc)
	app := fiber.New()
	app.Delete("/api/v1/roles/delete/:uuid", h.Delete)

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

func TestRoleHandler_GetByID_Success(t *testing.T) {
	repo := newMemRoleRepo()
	roleID := uuidv7.MustBytes()
	repo.roles[string(roleID)] = &auth.Role{ID: roleID, Name: "admin"}

	svc := service.NewRoleService(repo)
	h := NewRoleHandler(svc)
	app := fiber.New()
	app.Get("/api/v1/roles/get/:uuid", h.GetByID)

	idStr, _ := uuidv7.BytesToString(roleID)
	req, _ := http.NewRequest(http.MethodGet, "/api/v1/roles/get/"+idStr, nil)
	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("app.Test: %v", err)
	}
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("expected 200, got %d", resp.StatusCode)
	}
}

func TestRoleHandler_GetByID_NotFound(t *testing.T) {
	repo := newMemRoleRepo()
	svc := service.NewRoleService(repo)
	h := NewRoleHandler(svc)
	app := fiber.New()
	app.Get("/api/v1/roles/get/:uuid", h.GetByID)

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

func TestRoleHandler_List_Success(t *testing.T) {
	repo := newMemRoleRepo()
	roleID := uuidv7.MustBytes()
	repo.listRoles = []auth.Role{{ID: roleID, Name: "admin"}}
	repo.listTotal = 1

	svc := service.NewRoleService(repo)
	h := NewRoleHandler(svc)
	app := fiber.New()
	app.Get("/api/v1/roles/get-all", h.List)

	req, _ := http.NewRequest(http.MethodGet, "/api/v1/roles/get-all?page[number]=1&page[size]=10&filter[name]=ad", nil)
	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("app.Test: %v", err)
	}
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("expected 200, got %d", resp.StatusCode)
	}
}

func TestRoleHandler_ListDatatable_Success(t *testing.T) {
	repo := newMemRoleRepo()
	roleID := uuidv7.MustBytes()
	repo.listRoles = []auth.Role{{ID: roleID, Name: "admin"}}
	repo.listTotal = 1

	svc := service.NewRoleService(repo)
	h := NewRoleHandler(svc)
	app := fiber.New()
	app.Get("/api/v1/roles/get-all/dt", h.ListDatatable)

	req, _ := http.NewRequest(http.MethodGet, "/api/v1/roles/get-all/dt?page=1&size=10&search=ad", nil)
	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("app.Test: %v", err)
	}
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("expected 200, got %d", resp.StatusCode)
	}
}

func TestParseIntDefault(t *testing.T) {
	if got := parseIntDefault("", 7); got != 7 {
		t.Fatalf("expected default value")
	}
	if got := parseIntDefault("abc", 7); got != 7 {
		t.Fatalf("expected default on invalid")
	}
	if got := parseIntDefault("9", 7); got != 9 {
		t.Fatalf("expected parsed value")
	}
}

func TestRoleHandler_Update_InvalidID(t *testing.T) {
	repo := newMemRoleRepo()
	svc := service.NewRoleService(repo)
	h := NewRoleHandler(svc)
	app := fiber.New()
	app.Patch("/api/v1/roles/update/:uuid", h.Update)

	payload := []byte(`{"data":{"type":"role","id":"bad","attributes":{"name":"test_role"}}}`)
	req, _ := http.NewRequest(http.MethodPatch, "/api/v1/roles/update/not-a-uuid", 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.StatusUnprocessableEntity {
		t.Fatalf("expected 422, got %d", resp.StatusCode)
	}
}

func TestRoleHandler_Update_NoAttributes(t *testing.T) {
	repo := newMemRoleRepo()
	roleID := uuidv7.MustBytes()
	repo.roles[string(roleID)] = &auth.Role{ID: roleID, Name: "admin"}

	svc := service.NewRoleService(repo)
	h := NewRoleHandler(svc)
	app := fiber.New()
	app.Patch("/api/v1/roles/update/:uuid", h.Update)

	idStr, _ := uuidv7.BytesToString(roleID)
	payload := []byte(`{"data":{"type":"role","id":"` + idStr + `","attributes":{}}}`)
	req, _ := http.NewRequest(http.MethodPatch, "/api/v1/roles/update/"+idStr, 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.StatusUnprocessableEntity {
		t.Fatalf("expected 422, got %d", resp.StatusCode)
	}
}

func TestRoleHandler_Update_ServiceError(t *testing.T) {
	repo := newMemRoleRepo()
	repo.updateErr = errors.New("db error")
	roleID := uuidv7.MustBytes()
	repo.roles[string(roleID)] = &auth.Role{ID: roleID, Name: "admin"}

	svc := service.NewRoleService(repo)
	h := NewRoleHandler(svc)
	app := fiber.New()
	app.Patch("/api/v1/roles/update/:uuid", h.Update)

	idStr, _ := uuidv7.BytesToString(roleID)
	payload := []byte(`{"data":{"type":"role","id":"` + idStr + `","attributes":{"name":"test_role"}}}`)
	req, _ := http.NewRequest(http.MethodPatch, "/api/v1/roles/update/"+idStr, 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.StatusBadRequest {
		t.Fatalf("expected 400, got %d", resp.StatusCode)
	}
}

func TestRoleHandler_AssignPermission_Success(t *testing.T) {
	repo := newMemRoleRepo()
	roleID := uuidv7.MustBytes()
	repo.roles[string(roleID)] = &auth.Role{ID: roleID, Name: "admin"}

	svc := service.NewRoleService(repo)
	h := NewRoleHandler(svc)
	app := fiber.New()
	app.Post("/api/v1/roles/:uuid/permissions", h.AssignPermission)

	idStr, _ := uuidv7.BytesToString(roleID)
	permID, _ := uuidv7.BytesToString(uuidv7.MustBytes())
	payload := []byte(`{"data":{"type":"role_permission","attributes":{"permission_id":"` + permID + `"}}}`)
	req, _ := http.NewRequest(http.MethodPost, "/api/v1/roles/"+idStr+"/permissions", 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.StatusOK {
		t.Fatalf("expected 200, got %d", resp.StatusCode)
	}
}

func TestRoleHandler_RemovePermission_Success(t *testing.T) {
	repo := newMemRoleRepo()
	roleID := uuidv7.MustBytes()
	repo.roles[string(roleID)] = &auth.Role{ID: roleID, Name: "admin"}

	svc := service.NewRoleService(repo)
	h := NewRoleHandler(svc)
	app := fiber.New()
	app.Delete("/api/v1/roles/:uuid/permissions/:permission_uuid", h.RemovePermission)

	idStr, _ := uuidv7.BytesToString(roleID)
	permID, _ := uuidv7.BytesToString(uuidv7.MustBytes())
	req, _ := http.NewRequest(http.MethodDelete, "/api/v1/roles/"+idStr+"/permissions/"+permID, nil)
	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("app.Test: %v", err)
	}
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("expected 200, got %d", resp.StatusCode)
	}
}

func TestRoleHandler_AssignPermissionsBulk_Success(t *testing.T) {
	repo := newMemRoleRepo()
	roleID := uuidv7.MustBytes()
	repo.roles[string(roleID)] = &auth.Role{ID: roleID, Name: "admin"}

	svc := service.NewRoleService(repo)
	h := NewRoleHandler(svc)
	app := fiber.New()
	app.Post("/api/v1/roles/:uuid/permissions/assign-bulk", h.AssignPermissionsBulk)

	idStr, _ := uuidv7.BytesToString(roleID)
	permID1, _ := uuidv7.BytesToString(uuidv7.MustBytes())
	permID2, _ := uuidv7.BytesToString(uuidv7.MustBytes())
	payload := []byte(`{"data":{"type":"role_assign_permission_bulk","attributes":{"permission_ids":["` + permID1 + `","` + permID2 + `"]}}}`)
	req, _ := http.NewRequest(http.MethodPost, "/api/v1/roles/"+idStr+"/permissions/assign-bulk", 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.StatusOK {
		t.Fatalf("expected 200, got %d", resp.StatusCode)
	}
}

func TestRoleHandler_RemovePermissionsBulk_Success(t *testing.T) {
	repo := newMemRoleRepo()
	roleID := uuidv7.MustBytes()
	repo.roles[string(roleID)] = &auth.Role{ID: roleID, Name: "admin"}

	svc := service.NewRoleService(repo)
	h := NewRoleHandler(svc)
	app := fiber.New()
	app.Post("/api/v1/roles/:uuid/permissions/remove-bulk", h.RemovePermissionsBulk)

	idStr, _ := uuidv7.BytesToString(roleID)
	permID1, _ := uuidv7.BytesToString(uuidv7.MustBytes())
	permID2, _ := uuidv7.BytesToString(uuidv7.MustBytes())
	payload := []byte(`{"data":{"type":"role_remove_permission_bulk","attributes":{"permission_ids":["` + permID1 + `","` + permID2 + `"]}}}`)
	req, _ := http.NewRequest(http.MethodPost, "/api/v1/roles/"+idStr+"/permissions/remove-bulk", 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.StatusOK {
		t.Fatalf("expected 200, got %d", resp.StatusCode)
	}
}

func TestRoleHandler_AssignPermission_InvalidAndErrorPaths(t *testing.T) {
	t.Run("invalid role id", func(t *testing.T) {
		repo := newMemRoleRepo()
		svc := service.NewRoleService(repo)
		h := NewRoleHandler(svc)
		app := fiber.New()
		app.Post("/api/v1/roles/:uuid/permissions", h.AssignPermission)

		payload := []byte(`{"data":{"type":"role_permission","attributes":{"permission_id":"bad"}}}`)
		req, _ := http.NewRequest(http.MethodPost, "/api/v1/roles/not-uuid/permissions", 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("permission not found", func(t *testing.T) {
		repo := newMemRoleRepo()
		repo.permissionNotFound = true
		roleID := uuidv7.MustBytes()
		repo.roles[string(roleID)] = &auth.Role{ID: roleID, Name: "admin"}
		svc := service.NewRoleService(repo)
		h := NewRoleHandler(svc)
		app := fiber.New()
		app.Post("/api/v1/roles/:uuid/permissions", h.AssignPermission)

		idStr, _ := uuidv7.BytesToString(roleID)
		permID, _ := uuidv7.BytesToString(uuidv7.MustBytes())
		payload := []byte(`{"data":{"type":"role_permission","attributes":{"permission_id":"` + permID + `"}}}`)
		req, _ := http.NewRequest(http.MethodPost, "/api/v1/roles/"+idStr+"/permissions", bytes.NewReader(payload))
		req.Header.Set("Content-Type", "application/json")
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusNotFound {
			t.Fatalf("expected 404, got %d", resp.StatusCode)
		}
	})

	t.Run("assign service error", func(t *testing.T) {
		repo := newMemRoleRepo()
		repo.assignPermissionErr = errors.New("db error")
		roleID := uuidv7.MustBytes()
		repo.roles[string(roleID)] = &auth.Role{ID: roleID, Name: "admin"}
		svc := service.NewRoleService(repo)
		h := NewRoleHandler(svc)
		app := fiber.New()
		app.Post("/api/v1/roles/:uuid/permissions", h.AssignPermission)

		idStr, _ := uuidv7.BytesToString(roleID)
		permID, _ := uuidv7.BytesToString(uuidv7.MustBytes())
		payload := []byte(`{"data":{"type":"role_permission","attributes":{"permission_id":"` + permID + `"}}}`)
		req, _ := http.NewRequest(http.MethodPost, "/api/v1/roles/"+idStr+"/permissions", 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 TestRoleHandler_RemovePermission_ErrorPaths(t *testing.T) {
	repo := newMemRoleRepo()
	roleID := uuidv7.MustBytes()
	repo.roles[string(roleID)] = &auth.Role{ID: roleID, Name: "admin"}
	repo.removePermissionErr = errors.New("db error")
	svc := service.NewRoleService(repo)
	h := NewRoleHandler(svc)
	app := fiber.New()
	app.Delete("/api/v1/roles/:uuid/permissions/:permission_uuid", h.RemovePermission)

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

func TestRoleHandler_RemovePermission_InvalidPermissionID(t *testing.T) {
	repo := newMemRoleRepo()
	roleID := uuidv7.MustBytes()
	repo.roles[string(roleID)] = &auth.Role{ID: roleID, Name: "admin"}
	svc := service.NewRoleService(repo)
	h := NewRoleHandler(svc)
	app := fiber.New()
	app.Delete("/api/v1/roles/:uuid/permissions/:permission_uuid", h.RemovePermission)

	idStr, _ := uuidv7.BytesToString(roleID)
	req, _ := http.NewRequest(http.MethodDelete, "/api/v1/roles/"+idStr+"/permissions/not-uuid", nil)
	resp, _ := app.Test(req)
	if resp.StatusCode != http.StatusUnprocessableEntity {
		t.Fatalf("expected 422, got %d", resp.StatusCode)
	}
}

func TestRoleHandler_AssignPermissionsBulk_ErrorPaths(t *testing.T) {
	t.Run("empty list", func(t *testing.T) {
		repo := newMemRoleRepo()
		roleID := uuidv7.MustBytes()
		repo.roles[string(roleID)] = &auth.Role{ID: roleID, Name: "admin"}
		svc := service.NewRoleService(repo)
		h := NewRoleHandler(svc)
		app := fiber.New()
		app.Post("/api/v1/roles/:uuid/permissions/assign-bulk", h.AssignPermissionsBulk)

		idStr, _ := uuidv7.BytesToString(roleID)
		payload := []byte(`{"data":{"type":"role_assign_permission_bulk","attributes":{"permission_ids":[]}}}`)
		req, _ := http.NewRequest(http.MethodPost, "/api/v1/roles/"+idStr+"/permissions/assign-bulk", 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 permission id", func(t *testing.T) {
		repo := newMemRoleRepo()
		roleID := uuidv7.MustBytes()
		repo.roles[string(roleID)] = &auth.Role{ID: roleID, Name: "admin"}
		svc := service.NewRoleService(repo)
		h := NewRoleHandler(svc)
		app := fiber.New()
		app.Post("/api/v1/roles/:uuid/permissions/assign-bulk", h.AssignPermissionsBulk)

		idStr, _ := uuidv7.BytesToString(roleID)
		payload := []byte(`{"data":{"type":"role_assign_permission_bulk","attributes":{"permission_ids":["bad"]}}}`)
		req, _ := http.NewRequest(http.MethodPost, "/api/v1/roles/"+idStr+"/permissions/assign-bulk", 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("assign error", func(t *testing.T) {
		repo := newMemRoleRepo()
		repo.assignPermissionErr = errors.New("db error")
		roleID := uuidv7.MustBytes()
		repo.roles[string(roleID)] = &auth.Role{ID: roleID, Name: "admin"}
		svc := service.NewRoleService(repo)
		h := NewRoleHandler(svc)
		app := fiber.New()
		app.Post("/api/v1/roles/:uuid/permissions/assign-bulk", h.AssignPermissionsBulk)

		idStr, _ := uuidv7.BytesToString(roleID)
		permID, _ := uuidv7.BytesToString(uuidv7.MustBytes())
		payload := []byte(`{"data":{"type":"role_assign_permission_bulk","attributes":{"permission_ids":["` + permID + `"]}}}`)
		req, _ := http.NewRequest(http.MethodPost, "/api/v1/roles/"+idStr+"/permissions/assign-bulk", 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 type", func(t *testing.T) {
		repo := newMemRoleRepo()
		roleID := uuidv7.MustBytes()
		repo.roles[string(roleID)] = &auth.Role{ID: roleID, Name: "admin"}
		svc := service.NewRoleService(repo)
		h := NewRoleHandler(svc)
		app := fiber.New()
		app.Post("/api/v1/roles/:uuid/permissions/assign-bulk", h.AssignPermissionsBulk)

		idStr, _ := uuidv7.BytesToString(roleID)
		permID, _ := uuidv7.BytesToString(uuidv7.MustBytes())
		payload := []byte(`{"data":{"type":"role_remove_permission_bulk","attributes":{"permission_ids":["` + permID + `"]}}}`)
		req, _ := http.NewRequest(http.MethodPost, "/api/v1/roles/"+idStr+"/permissions/assign-bulk", bytes.NewReader(payload))
		req.Header.Set("Content-Type", "application/json")
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})
}

func TestRoleHandler_RemovePermissionsBulk_ErrorPaths(t *testing.T) {
	t.Run("remove error", func(t *testing.T) {
		repo := newMemRoleRepo()
		repo.removePermissionErr = errors.New("db error")
		roleID := uuidv7.MustBytes()
		repo.roles[string(roleID)] = &auth.Role{ID: roleID, Name: "admin"}
		svc := service.NewRoleService(repo)
		h := NewRoleHandler(svc)
		app := fiber.New()
		app.Post("/api/v1/roles/:uuid/permissions/remove-bulk", h.RemovePermissionsBulk)

		idStr, _ := uuidv7.BytesToString(roleID)
		permID, _ := uuidv7.BytesToString(uuidv7.MustBytes())
		payload := []byte(`{"data":{"type":"role_remove_permission_bulk","attributes":{"permission_ids":["` + permID + `"]}}}`)
		req, _ := http.NewRequest(http.MethodPost, "/api/v1/roles/"+idStr+"/permissions/remove-bulk", 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 type", func(t *testing.T) {
		repo := newMemRoleRepo()
		roleID := uuidv7.MustBytes()
		repo.roles[string(roleID)] = &auth.Role{ID: roleID, Name: "admin"}
		svc := service.NewRoleService(repo)
		h := NewRoleHandler(svc)
		app := fiber.New()
		app.Post("/api/v1/roles/:uuid/permissions/remove-bulk", h.RemovePermissionsBulk)

		idStr, _ := uuidv7.BytesToString(roleID)
		permID, _ := uuidv7.BytesToString(uuidv7.MustBytes())
		payload := []byte(`{"data":{"type":"role_assign_permission_bulk","attributes":{"permission_ids":["` + permID + `"]}}}`)
		req, _ := http.NewRequest(http.MethodPost, "/api/v1/roles/"+idStr+"/permissions/remove-bulk", bytes.NewReader(payload))
		req.Header.Set("Content-Type", "application/json")
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})
}

func TestRoleHandler_AssignPermissionsBulk_InvalidIDAndRoleNotFound(t *testing.T) {
	repo := newMemRoleRepo()
	svc := service.NewRoleService(repo)
	h := NewRoleHandler(svc)
	app := fiber.New()
	app.Post("/api/v1/roles/:uuid/permissions/assign-bulk", h.AssignPermissionsBulk)

	req1, _ := http.NewRequest(http.MethodPost, "/api/v1/roles/not-uuid/permissions/assign-bulk", bytes.NewReader([]byte(`{}`)))
	req1.Header.Set("Content-Type", "application/json")
	resp1, _ := app.Test(req1)
	if resp1.StatusCode != http.StatusUnprocessableEntity {
		t.Fatalf("expected 422, got %d", resp1.StatusCode)
	}

	roleID := uuidv7.MustBytes()
	idStr, _ := uuidv7.BytesToString(roleID)
	permID, _ := uuidv7.BytesToString(uuidv7.MustBytes())
	payload := []byte(`{"data":{"type":"role_assign_permission_bulk","attributes":{"permission_ids":["` + permID + `"]}}}`)
	req2, _ := http.NewRequest(http.MethodPost, "/api/v1/roles/"+idStr+"/permissions/assign-bulk", bytes.NewReader(payload))
	req2.Header.Set("Content-Type", "application/json")
	resp2, _ := app.Test(req2)
	if resp2.StatusCode != http.StatusNotFound {
		t.Fatalf("expected 404, got %d", resp2.StatusCode)
	}
}

func TestRoleHandler_AssignPermissionsBulk_PermissionNotFound(t *testing.T) {
	repo := newMemRoleRepo()
	repo.permissionNotFound = true
	roleID := uuidv7.MustBytes()
	repo.roles[string(roleID)] = &auth.Role{ID: roleID, Name: "admin"}
	svc := service.NewRoleService(repo)
	h := NewRoleHandler(svc)
	app := fiber.New()
	app.Post("/api/v1/roles/:uuid/permissions/assign-bulk", h.AssignPermissionsBulk)

	idStr, _ := uuidv7.BytesToString(roleID)
	permID, _ := uuidv7.BytesToString(uuidv7.MustBytes())
	payload := []byte(`{"data":{"type":"role_assign_permission_bulk","attributes":{"permission_ids":["` + permID + `"]}}}`)
	req, _ := http.NewRequest(http.MethodPost, "/api/v1/roles/"+idStr+"/permissions/assign-bulk", 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 TestRoleHandler_RemovePermissionsBulk_ValidationAndNotFoundPaths(t *testing.T) {
	repo := newMemRoleRepo()
	svc := service.NewRoleService(repo)
	h := NewRoleHandler(svc)
	app := fiber.New()
	app.Post("/api/v1/roles/:uuid/permissions/remove-bulk", h.RemovePermissionsBulk)

	req1, _ := http.NewRequest(http.MethodPost, "/api/v1/roles/not-uuid/permissions/remove-bulk", bytes.NewReader([]byte(`{}`)))
	req1.Header.Set("Content-Type", "application/json")
	resp1, _ := app.Test(req1)
	if resp1.StatusCode != http.StatusUnprocessableEntity {
		t.Fatalf("expected 422, got %d", resp1.StatusCode)
	}

	roleID := uuidv7.MustBytes()
	idStr, _ := uuidv7.BytesToString(roleID)
	payloadEmpty := []byte(`{"data":{"type":"role_remove_permission_bulk","attributes":{"permission_ids":[]}}}`)
	req2, _ := http.NewRequest(http.MethodPost, "/api/v1/roles/"+idStr+"/permissions/remove-bulk", bytes.NewReader(payloadEmpty))
	req2.Header.Set("Content-Type", "application/json")
	resp2, _ := app.Test(req2)
	if resp2.StatusCode != http.StatusUnprocessableEntity {
		t.Fatalf("expected 422, got %d", resp2.StatusCode)
	}

	permID, _ := uuidv7.BytesToString(uuidv7.MustBytes())
	payload := []byte(`{"data":{"type":"role_remove_permission_bulk","attributes":{"permission_ids":["` + permID + `"]}}}`)
	req3, _ := http.NewRequest(http.MethodPost, "/api/v1/roles/"+idStr+"/permissions/remove-bulk", bytes.NewReader(payload))
	req3.Header.Set("Content-Type", "application/json")
	resp3, _ := app.Test(req3)
	if resp3.StatusCode != http.StatusNotFound {
		t.Fatalf("expected 404, got %d", resp3.StatusCode)
	}
}

func TestRoleHandler_ListPermissions_Success(t *testing.T) {
	repo := newMemRoleRepo()
	repo.listPermissions = []auth.Permission{
		{
			ID:          uuidv7.MustBytes(),
			Name:        "Read User",
			Key:         "user.read",
			Description: "Read users",
		},
	}
	repo.listPermissionsTotal = 1

	svc := service.NewRoleService(repo)
	h := NewRoleHandler(svc)
	app := fiber.New()
	app.Get("/api/v1/roles/permissions/get-all", h.ListPermissions)

	req, _ := http.NewRequest(http.MethodGet, "/api/v1/roles/permissions/get-all?page[number]=1&page[size]=10&filter[name]=read&sort=name", nil)
	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("app.Test: %v", err)
	}
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("expected 200, got %d", resp.StatusCode)
	}
}

func TestRoleHandler_ListPermissions_Error(t *testing.T) {
	repo := newMemRoleRepo()
	repo.listPermissionsErr = errors.New("db error")

	svc := service.NewRoleService(repo)
	h := NewRoleHandler(svc)
	app := fiber.New()
	app.Get("/api/v1/roles/permissions/get-all", h.ListPermissions)

	req, _ := http.NewRequest(http.MethodGet, "/api/v1/roles/permissions/get-all", nil)
	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("app.Test: %v", err)
	}
	if resp.StatusCode != http.StatusBadRequest {
		t.Fatalf("expected 400, got %d", resp.StatusCode)
	}
}

func TestRoleHandler_UpdatePermission_Success(t *testing.T) {
	repo := newMemRoleRepo()
	permissionID := uuidv7.MustBytes()
	repo.permissions[string(permissionID)] = &auth.Permission{
		ID:          permissionID,
		Name:        "Delete User",
		Key:         "user.delete",
		Description: "Delete users",
		RequiresPIN: false,
	}

	svc := service.NewRoleService(repo)
	h := NewRoleHandler(svc)
	app := fiber.New()
	app.Patch("/api/v1/roles/permissions/:permission_uuid", h.UpdatePermission)

	permissionIDStr, _ := uuidv7.BytesToString(permissionID)
	payload := []byte(`{"data":{"type":"permission_update","id":"` + permissionIDStr + `","attributes":{"requires_pin":true}}}`)
	req, _ := http.NewRequest(http.MethodPatch, "/api/v1/roles/permissions/"+permissionIDStr, 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.StatusOK {
		t.Fatalf("expected 200, got %d", resp.StatusCode)
	}
	if !repo.permissions[string(permissionID)].RequiresPIN {
		t.Fatalf("expected permission requires_pin updated")
	}
}

func TestRoleHandler_UpdatePermission_NotFound(t *testing.T) {
	repo := newMemRoleRepo()
	repo.permissionNotFound = true

	svc := service.NewRoleService(repo)
	h := NewRoleHandler(svc)
	app := fiber.New()
	app.Patch("/api/v1/roles/permissions/:permission_uuid", h.UpdatePermission)

	permissionIDStr, _ := uuidv7.BytesToString(uuidv7.MustBytes())
	payload := []byte(`{"data":{"type":"permission_update","id":"` + permissionIDStr + `","attributes":{"requires_pin":true}}}`)
	req, _ := http.NewRequest(http.MethodPatch, "/api/v1/roles/permissions/"+permissionIDStr, 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.StatusNotFound {
		t.Fatalf("expected 404, got %d", resp.StatusCode)
	}
}

func TestRoleHandler_UpdatePermission_ErrorBranches(t *testing.T) {
	repo := newMemRoleRepo()
	svc := service.NewRoleService(repo)
	h := NewRoleHandler(svc)
	app := fiber.New()
	app.Patch("/api/v1/roles/permissions/:permission_uuid", h.UpdatePermission)

	t.Run("invalid path id", func(t *testing.T) {
		req, _ := http.NewRequest(http.MethodPatch, "/api/v1/roles/permissions/not-a-uuid", bytes.NewReader([]byte(`{"data":{"type":"permission_update","id":"x","attributes":{"requires_pin":true}}}`)))
		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(uuidv7.MustBytes())
		req, _ := http.NewRequest(http.MethodPatch, "/api/v1/roles/permissions/"+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("missing data id", func(t *testing.T) {
		idStr, _ := uuidv7.BytesToString(uuidv7.MustBytes())
		req, _ := http.NewRequest(http.MethodPatch, "/api/v1/roles/permissions/"+idStr, bytes.NewReader([]byte(`{"data":{"type":"permission_update","attributes":{"requires_pin":true}}}`)))
		req.Header.Set("Content-Type", "application/json")
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})

	t.Run("id mismatch", func(t *testing.T) {
		pathID, _ := uuidv7.BytesToString(uuidv7.MustBytes())
		otherID, _ := uuidv7.BytesToString(uuidv7.MustBytes())
		req, _ := http.NewRequest(http.MethodPatch, "/api/v1/roles/permissions/"+pathID, bytes.NewReader([]byte(`{"data":{"type":"permission_update","id":"`+otherID+`","attributes":{"requires_pin":true}}}`)))
		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("missing requires_pin", func(t *testing.T) {
		pathID, _ := uuidv7.BytesToString(uuidv7.MustBytes())
		req, _ := http.NewRequest(http.MethodPatch, "/api/v1/roles/permissions/"+pathID, bytes.NewReader([]byte(`{"data":{"type":"permission_update","id":"`+pathID+`","attributes":{}}}`)))
		req.Header.Set("Content-Type", "application/json")
		resp, _ := app.Test(req)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})

	t.Run("update service error", func(t *testing.T) {
		id := uuidv7.MustBytes()
		idStr, _ := uuidv7.BytesToString(id)
		repo.permissions[string(id)] = &auth.Permission{ID: id, Key: "x", Name: "x"}
		repo.updateErr = errors.New("update failed")
		defer func() { repo.updateErr = nil }()
		req, _ := http.NewRequest(http.MethodPatch, "/api/v1/roles/permissions/"+idStr, bytes.NewReader([]byte(`{"data":{"type":"permission_update","id":"`+idStr+`","attributes":{"requires_pin":true}}}`)))
		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 TestParseIntToString(t *testing.T) {
	if got := parseIntToString(0); got != "0" {
		t.Fatalf("expected 0, got %s", got)
	}
	if got := parseIntToString(123); got != "123" {
		t.Fatalf("expected 123, got %s", got)
	}
}

func TestRoleResource(t *testing.T) {
	now := time.Now().UTC()
	role := &auth.Role{
		ID:          uuidv7.MustBytes(),
		Name:        "admin",
		Description: "full access",
		CreatedBy:   uuidv7.MustBytes(),
		CreatedAt:   now,
		UpdatedAt:   now,
	}

	res := roleResource(role)
	if res.Type != "role" || res.ID == "" {
		t.Fatalf("expected role resource type/id")
	}
	if res.Attributes.CreatedBy == "" {
		t.Fatalf("expected created_by in role resource")
	}
}
