package port_test

import (
	"cmp"
	"fmt"
	"net/http"
	"net/http/httptest"
	"slices"
	"strconv"
	"strings"
	"testing"
	"time"

	"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
	"gitea.dwysokinski.me/twhelp/corev3/internal/domain/domaintest"
	"gitea.dwysokinski.me/twhelp/corev3/internal/port/internal/apimodel"
	"github.com/brianvoe/gofakeit/v7"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

const endpointListEnnoblements = "/v2/versions/%s/servers/%s/ennoblements"

func TestListEnnoblements(t *testing.T) {
	t.Parallel()

	handler := newAPIHTTPHandler(t)
	ennoblements := getAllEnnoblements(t, handler)
	server := ennoblements[0].Server

	tests := []struct {
		name        string
		reqModifier func(t *testing.T, req *http.Request)
		assertResp  func(t *testing.T, req *http.Request, resp *http.Response)
	}{
		{
			name: "OK: without params",
			assertResp: func(t *testing.T, _ *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusOK, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ListEnnoblementsResponse](t, resp.Body)
				assert.Zero(t, body.Cursor.Next)
				assert.NotZero(t, body.Cursor.Self)
				assert.NotZero(t, body.Data)
				assert.True(t, slices.IsSortedFunc(body.Data, func(a, b apimodel.Ennoblement) int {
					return cmp.Or(
						a.CreatedAt.Compare(b.CreatedAt),
						cmp.Compare(a.Id, b.Id),
					)
				}))
			},
		},
		{
			name: "OK: limit=1",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				q := req.URL.Query()
				q.Set("limit", "1")
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusOK, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ListEnnoblementsResponse](t, resp.Body)
				assert.NotZero(t, body.Cursor.Next)
				assert.NotZero(t, body.Cursor.Self)
				assert.NotZero(t, body.Data)
				limit, err := strconv.Atoi(req.URL.Query().Get("limit"))
				require.NoError(t, err)
				assert.Len(t, body.Data, limit)
			},
		},
		{
			name: "OK: limit=1 cursor",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()

				q := req.URL.Query()
				q.Set("limit", "1")
				req.URL.RawQuery = q.Encode()

				resp := doCustomRequest(handler, req.Clone(req.Context()))
				defer resp.Body.Close()
				body := decodeJSON[apimodel.ListEnnoblementsResponse](t, resp.Body)
				require.NotEmpty(t, body.Cursor.Next)

				q.Set("cursor", body.Cursor.Next)
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusOK, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ListEnnoblementsResponse](t, resp.Body)
				assert.Equal(t, req.URL.Query().Get("cursor"), body.Cursor.Self)
				limit, err := strconv.Atoi(req.URL.Query().Get("limit"))
				require.NoError(t, err)
				assert.Len(t, body.Data, limit)
			},
		},
		{
			name: "OK: sort=[createdAt:DESC]",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				q := req.URL.Query()
				q.Set("sort", "createdAt:DESC")
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, _ *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusOK, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ListEnnoblementsResponse](t, resp.Body)
				assert.Zero(t, body.Cursor.Next)
				assert.NotZero(t, body.Cursor.Self)
				assert.NotZero(t, body.Data)
				assert.True(t, slices.IsSortedFunc(body.Data, func(a, b apimodel.Ennoblement) int {
					return cmp.Or(
						a.CreatedAt.Compare(b.CreatedAt)*-1,
						cmp.Compare(a.Id, b.Id),
					)
				}))
			},
		},
		{
			name: "OK: since",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()

				var filtered []ennoblementWithServer
				for _, e := range ennoblements {
					if e.Server.Key == server.Key {
						filtered = append(filtered, e)
					}
				}
				slices.SortFunc(filtered, func(a, b ennoblementWithServer) int {
					return cmp.Or(
						a.CreatedAt.Compare(b.CreatedAt),
						cmp.Compare(a.Id, b.Id),
					)
				})
				require.GreaterOrEqual(t, len(filtered), 2)

				q := req.URL.Query()
				q.Set("since", filtered[1].CreatedAt.Format(time.RFC3339))
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusOK, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ListEnnoblementsResponse](t, resp.Body)
				assert.Zero(t, body.Cursor.Next)
				assert.NotZero(t, body.Cursor.Self)
				assert.NotZero(t, body.Data)
				since, err := time.Parse(time.RFC3339, req.URL.Query().Get("since"))
				require.NoError(t, err)
				for _, e := range body.Data {
					assert.True(t, e.CreatedAt.After(since) || e.CreatedAt.Equal(since))
				}
			},
		},
		{
			name: "OK: before",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()

				var filtered []ennoblementWithServer
				for _, e := range ennoblements {
					if e.Server.Key == server.Key {
						filtered = append(filtered, e)
					}
				}
				slices.SortFunc(filtered, func(a, b ennoblementWithServer) int {
					return cmp.Or(
						a.CreatedAt.Compare(b.CreatedAt),
						cmp.Compare(a.Id, b.Id),
					)
				})
				require.GreaterOrEqual(t, len(filtered), 2)

				q := req.URL.Query()
				q.Set("before", filtered[1].CreatedAt.Format(time.RFC3339))
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusOK, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ListEnnoblementsResponse](t, resp.Body)
				assert.Zero(t, body.Cursor.Next)
				assert.NotZero(t, body.Cursor.Self)
				assert.NotZero(t, body.Data)
				before, err := time.Parse(time.RFC3339, req.URL.Query().Get("before"))
				require.NoError(t, err)
				for _, e := range body.Data {
					assert.True(t, e.CreatedAt.Before(before))
				}
			},
		},
		{
			name: "ERR: limit is not an integer",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				q := req.URL.Query()
				q.Set("limit", "asd")
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusBadRequest, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
				assert.Equal(t, apimodel.ErrorResponse{
					Errors: []apimodel.Error{
						{
							Code: "invalid-param-format",
							Message: fmt.Sprintf(
								"error binding string parameter: strconv.ParseInt: parsing \"%s\": invalid syntax",
								req.URL.Query().Get("limit"),
							),
							Path: []string{"$query", "limit"},
						},
					},
				}, body)
			},
		},
		{
			name: "ERR: limit < 1",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				q := req.URL.Query()
				q.Set("limit", "0")
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusBadRequest, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
				limit, err := strconv.Atoi(req.URL.Query().Get("limit"))
				require.NoError(t, err)
				domainErr := domain.MinGreaterEqualError{
					Min:     1,
					Current: limit,
				}
				assert.Equal(t, apimodel.ErrorResponse{
					Errors: []apimodel.Error{
						{
							Code:    apimodel.ErrorCode(domainErr.Code()),
							Message: domainErr.Error(),
							Params: map[string]any{
								"current": float64(domainErr.Current),
								"min":     float64(domainErr.Min),
							},
							Path: []string{"$query", "limit"},
						},
					},
				}, body)
			},
		},
		{
			name: fmt.Sprintf("ERR: limit > %d", domain.EnnoblementListMaxLimit),
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				q := req.URL.Query()
				q.Set("limit", strconv.Itoa(domain.EnnoblementListMaxLimit+1))
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusBadRequest, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
				limit, err := strconv.Atoi(req.URL.Query().Get("limit"))
				require.NoError(t, err)
				domainErr := domain.MaxLessEqualError{
					Max:     domain.EnnoblementListMaxLimit,
					Current: limit,
				}
				assert.Equal(t, apimodel.ErrorResponse{
					Errors: []apimodel.Error{
						{
							Code:    apimodel.ErrorCode(domainErr.Code()),
							Message: domainErr.Error(),
							Params: map[string]any{
								"current": float64(domainErr.Current),
								"max":     float64(domainErr.Max),
							},
							Path: []string{"$query", "limit"},
						},
					},
				}, body)
			},
		},
		{
			name: "ERR: len(cursor) < 1",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				q := req.URL.Query()
				q.Set("cursor", "")
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusBadRequest, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
				domainErr := domain.LenOutOfRangeError{
					Min:     1,
					Max:     1000,
					Current: len(req.URL.Query().Get("cursor")),
				}
				assert.Equal(t, apimodel.ErrorResponse{
					Errors: []apimodel.Error{
						{
							Code:    apimodel.ErrorCode(domainErr.Code()),
							Message: domainErr.Error(),
							Params: map[string]any{
								"current": float64(domainErr.Current),
								"max":     float64(domainErr.Max),
								"min":     float64(domainErr.Min),
							},
							Path: []string{"$query", "cursor"},
						},
					},
				}, body)
			},
		},
		{
			name: "ERR: len(cursor) > 1000",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				q := req.URL.Query()
				q.Set("cursor", gofakeit.LetterN(1001))
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusBadRequest, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
				domainErr := domain.LenOutOfRangeError{
					Min:     1,
					Max:     1000,
					Current: len(req.URL.Query().Get("cursor")),
				}
				assert.Equal(t, apimodel.ErrorResponse{
					Errors: []apimodel.Error{
						{
							Code:    apimodel.ErrorCode(domainErr.Code()),
							Message: domainErr.Error(),
							Params: map[string]any{
								"current": float64(domainErr.Current),
								"max":     float64(domainErr.Max),
								"min":     float64(domainErr.Min),
							},
							Path: []string{"$query", "cursor"},
						},
					},
				}, body)
			},
		},
		{
			name: "ERR: invalid cursor",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				q := req.URL.Query()
				q.Set("cursor", gofakeit.LetterN(100))
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, _ *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusBadRequest, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
				var domainErr domain.Error
				require.ErrorAs(t, domain.ErrInvalidCursor, &domainErr)
				assert.Equal(t, apimodel.ErrorResponse{
					Errors: []apimodel.Error{
						{
							Code:    apimodel.ErrorCode(domainErr.Code()),
							Message: domainErr.Error(),
							Path:    []string{"$query", "cursor"},
						},
					},
				}, body)
			},
		},
		{
			name: "ERR: len(sort) > 1",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				q := req.URL.Query()
				q.Add("sort", "createdAt:DESC")
				q.Add("sort", "createdAt:ASC")
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusBadRequest, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
				domainErr := domain.LenOutOfRangeError{
					Min:     0,
					Max:     1,
					Current: len(req.URL.Query()["sort"]),
				}
				assert.Equal(t, apimodel.ErrorResponse{
					Errors: []apimodel.Error{
						{
							Code:    apimodel.ErrorCode(domainErr.Code()),
							Message: domainErr.Error(),
							Params: map[string]any{
								"current": float64(domainErr.Current),
								"max":     float64(domainErr.Max),
								"min":     float64(domainErr.Min),
							},
							Path: []string{"$query", "sort"},
						},
					},
				}, body)
			},
		},
		{
			name: "ERR: invalid sort",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				q := req.URL.Query()
				q.Add("sort", "createdAt:")
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusBadRequest, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
				domainErr := domain.UnsupportedSortStringError{
					Sort: req.URL.Query()["sort"][0],
				}
				assert.Equal(t, apimodel.ErrorResponse{
					Errors: []apimodel.Error{
						{
							Code:    apimodel.ErrorCode(domainErr.Code()),
							Message: domainErr.Error(),
							Params: map[string]any{
								"sort": domainErr.Sort,
							},
							Path: []string{"$query", "sort", "0"},
						},
					},
				}, body)
			},
		},
		{
			name: "ERR: invalid since",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				q := req.URL.Query()
				q.Set("since", gofakeit.LetterN(100))
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusBadRequest, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
				var domainErr domain.Error
				require.ErrorAs(t, domain.ErrInvalidCursor, &domainErr)
				since := req.URL.Query().Get("since")
				assert.Equal(t, apimodel.ErrorResponse{
					Errors: []apimodel.Error{
						{
							Code: "invalid-param-format",
							//nolint:lll
							Message: fmt.Sprintf("error parsing '%s' as RFC3339 or 2006-01-02 time: parsing time \"%s\" as \"2006-01-02\": cannot parse \"%s\" as \"2006\"", since, since, since),
							Path:    []string{"$query", "since"},
						},
					},
				}, body)
			},
		},
		{
			name: "ERR: invalid before",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				q := req.URL.Query()
				q.Set("before", gofakeit.LetterN(100))
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusBadRequest, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
				var domainErr domain.Error
				require.ErrorAs(t, domain.ErrInvalidCursor, &domainErr)
				before := req.URL.Query().Get("before")
				assert.Equal(t, apimodel.ErrorResponse{
					Errors: []apimodel.Error{
						{
							Code: "invalid-param-format",
							//nolint:lll
							Message: fmt.Sprintf("error parsing '%s' as RFC3339 or 2006-01-02 time: parsing time \"%s\" as \"2006-01-02\": cannot parse \"%s\" as \"2006\"", before, before, before),
							Path:    []string{"$query", "before"},
						},
					},
				}, body)
			},
		},
		{
			name: "ERR: version not found",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				req.URL.Path = fmt.Sprintf(endpointListEnnoblements, randInvalidVersionCode(t, handler), server.Key)
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusNotFound, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
				pathSegments := strings.Split(req.URL.Path, "/")
				require.Len(t, pathSegments, 7)
				domainErr := domain.VersionNotFoundError{
					VersionCode: pathSegments[3],
				}
				assert.Equal(t, apimodel.ErrorResponse{
					Errors: []apimodel.Error{
						{
							Code:    apimodel.ErrorCode(domainErr.Code()),
							Message: domainErr.Error(),
							Params: map[string]any{
								"code": domainErr.VersionCode,
							},
						},
					},
				}, body)
			},
		},
		{
			name: "ERR: server not found",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				req.URL.Path = fmt.Sprintf(endpointListEnnoblements, server.Version.Code, domaintest.RandServerKey())
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusNotFound, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
				pathSegments := strings.Split(req.URL.Path, "/")
				require.Len(t, pathSegments, 7)
				domainErr := domain.ServerNotFoundError{
					Key: pathSegments[5],
				}
				assert.Equal(t, apimodel.ErrorResponse{
					Errors: []apimodel.Error{
						{
							Code:    apimodel.ErrorCode(domainErr.Code()),
							Message: domainErr.Error(),
							Params: map[string]any{
								"key": domainErr.Key,
							},
						},
					},
				}, body)
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			req := httptest.NewRequest(
				http.MethodGet,
				fmt.Sprintf(endpointListEnnoblements, server.Version.Code, server.Key),
				nil,
			)
			if tt.reqModifier != nil {
				tt.reqModifier(t, req)
			}

			resp := doCustomRequest(handler, req)
			defer resp.Body.Close()
			tt.assertResp(t, req, resp)
		})
	}
}

const endpointListPlayerEnnoblements = "/v2/versions/%s/servers/%s/players/%d/ennoblements"

func TestListPlayerEnnoblements(t *testing.T) {
	t.Parallel()

	handler := newAPIHTTPHandler(t)
	ennoblements := getAllEnnoblements(t, handler)
	server := ennoblements[0].Server
	player := ennoblements[0].NewOwner

	tests := []struct {
		name        string
		reqModifier func(t *testing.T, req *http.Request)
		assertResp  func(t *testing.T, req *http.Request, resp *http.Response)
	}{
		{
			name: "OK: without params",
			assertResp: func(t *testing.T, _ *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusOK, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ListEnnoblementsResponse](t, resp.Body)
				assert.Zero(t, body.Cursor.Next)
				assert.NotZero(t, body.Cursor.Self)
				assert.NotZero(t, body.Data)
				assert.True(t, slices.IsSortedFunc(body.Data, func(a, b apimodel.Ennoblement) int {
					return cmp.Or(
						a.CreatedAt.Compare(b.CreatedAt),
						cmp.Compare(a.Id, b.Id),
					)
				}))
			},
		},
		{
			name: "OK: limit=1",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				q := req.URL.Query()
				q.Set("limit", "1")
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusOK, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ListEnnoblementsResponse](t, resp.Body)
				assert.NotZero(t, body.Cursor.Next)
				assert.NotZero(t, body.Cursor.Self)
				assert.NotZero(t, body.Data)
				limit, err := strconv.Atoi(req.URL.Query().Get("limit"))
				require.NoError(t, err)
				assert.Len(t, body.Data, limit)
			},
		},
		{
			name: "OK: limit=1 cursor",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()

				q := req.URL.Query()
				q.Set("limit", "1")
				req.URL.RawQuery = q.Encode()

				resp := doCustomRequest(handler, req.Clone(req.Context()))
				defer resp.Body.Close()
				body := decodeJSON[apimodel.ListEnnoblementsResponse](t, resp.Body)
				require.NotEmpty(t, body.Cursor.Next)

				q.Set("cursor", body.Cursor.Next)
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusOK, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ListEnnoblementsResponse](t, resp.Body)
				assert.Equal(t, req.URL.Query().Get("cursor"), body.Cursor.Self)
				limit, err := strconv.Atoi(req.URL.Query().Get("limit"))
				require.NoError(t, err)
				assert.Len(t, body.Data, limit)
			},
		},
		{
			name: "OK: sort=[createdAt:DESC]",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				q := req.URL.Query()
				q.Set("sort", "createdAt:DESC")
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, _ *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusOK, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ListEnnoblementsResponse](t, resp.Body)
				assert.Zero(t, body.Cursor.Next)
				assert.NotZero(t, body.Cursor.Self)
				assert.NotZero(t, body.Data)
				assert.True(t, slices.IsSortedFunc(body.Data, func(a, b apimodel.Ennoblement) int {
					return cmp.Or(
						a.CreatedAt.Compare(b.CreatedAt)*-1,
						cmp.Compare(a.Id, b.Id),
					)
				}))
			},
		},
		{
			name: "OK: since",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()

				var filtered []ennoblementWithServer
				for _, e := range ennoblements {
					if e.Server.Key == server.Key {
						filtered = append(filtered, e)
					}
				}
				slices.SortFunc(filtered, func(a, b ennoblementWithServer) int {
					return cmp.Or(
						a.CreatedAt.Compare(b.CreatedAt),
						cmp.Compare(a.Id, b.Id),
					)
				})
				require.GreaterOrEqual(t, len(filtered), 2)

				q := req.URL.Query()
				q.Set("since", filtered[1].CreatedAt.Format(time.RFC3339))
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusOK, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ListEnnoblementsResponse](t, resp.Body)
				assert.Zero(t, body.Cursor.Next)
				assert.NotZero(t, body.Cursor.Self)
				assert.NotZero(t, body.Data)
				since, err := time.Parse(time.RFC3339, req.URL.Query().Get("since"))
				require.NoError(t, err)
				for _, e := range body.Data {
					assert.True(t, e.CreatedAt.After(since) || e.CreatedAt.Equal(since))
				}
			},
		},
		{
			name: "OK: before",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()

				var filtered []ennoblementWithServer
				for _, e := range ennoblements {
					if e.Server.Key == server.Key {
						filtered = append(filtered, e)
					}
				}
				slices.SortFunc(filtered, func(a, b ennoblementWithServer) int {
					return cmp.Or(
						a.CreatedAt.Compare(b.CreatedAt),
						cmp.Compare(a.Id, b.Id),
					)
				})
				require.GreaterOrEqual(t, len(filtered), 2)

				q := req.URL.Query()
				q.Set("before", filtered[1].CreatedAt.Format(time.RFC3339))
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusOK, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ListEnnoblementsResponse](t, resp.Body)
				assert.Zero(t, body.Cursor.Next)
				assert.NotZero(t, body.Cursor.Self)
				assert.NotZero(t, body.Data)
				before, err := time.Parse(time.RFC3339, req.URL.Query().Get("before"))
				require.NoError(t, err)
				for _, e := range body.Data {
					assert.True(t, e.CreatedAt.Before(before))
				}
			},
		},
		{
			name: "ERR: limit is not an integer",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				q := req.URL.Query()
				q.Set("limit", "asd")
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusBadRequest, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
				assert.Equal(t, apimodel.ErrorResponse{
					Errors: []apimodel.Error{
						{
							Code: "invalid-param-format",
							Message: fmt.Sprintf(
								"error binding string parameter: strconv.ParseInt: parsing \"%s\": invalid syntax",
								req.URL.Query().Get("limit"),
							),
							Path: []string{"$query", "limit"},
						},
					},
				}, body)
			},
		},
		{
			name: "ERR: limit < 1",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				q := req.URL.Query()
				q.Set("limit", "0")
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusBadRequest, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
				limit, err := strconv.Atoi(req.URL.Query().Get("limit"))
				require.NoError(t, err)
				domainErr := domain.MinGreaterEqualError{
					Min:     1,
					Current: limit,
				}
				assert.Equal(t, apimodel.ErrorResponse{
					Errors: []apimodel.Error{
						{
							Code:    apimodel.ErrorCode(domainErr.Code()),
							Message: domainErr.Error(),
							Params: map[string]any{
								"current": float64(domainErr.Current),
								"min":     float64(domainErr.Min),
							},
							Path: []string{"$query", "limit"},
						},
					},
				}, body)
			},
		},
		{
			name: fmt.Sprintf("ERR: limit > %d", domain.EnnoblementListMaxLimit),
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				q := req.URL.Query()
				q.Set("limit", strconv.Itoa(domain.EnnoblementListMaxLimit+1))
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusBadRequest, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
				limit, err := strconv.Atoi(req.URL.Query().Get("limit"))
				require.NoError(t, err)
				domainErr := domain.MaxLessEqualError{
					Max:     domain.EnnoblementListMaxLimit,
					Current: limit,
				}
				assert.Equal(t, apimodel.ErrorResponse{
					Errors: []apimodel.Error{
						{
							Code:    apimodel.ErrorCode(domainErr.Code()),
							Message: domainErr.Error(),
							Params: map[string]any{
								"current": float64(domainErr.Current),
								"max":     float64(domainErr.Max),
							},
							Path: []string{"$query", "limit"},
						},
					},
				}, body)
			},
		},
		{
			name: "ERR: len(cursor) < 1",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				q := req.URL.Query()
				q.Set("cursor", "")
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusBadRequest, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
				domainErr := domain.LenOutOfRangeError{
					Min:     1,
					Max:     1000,
					Current: len(req.URL.Query().Get("cursor")),
				}
				assert.Equal(t, apimodel.ErrorResponse{
					Errors: []apimodel.Error{
						{
							Code:    apimodel.ErrorCode(domainErr.Code()),
							Message: domainErr.Error(),
							Params: map[string]any{
								"current": float64(domainErr.Current),
								"max":     float64(domainErr.Max),
								"min":     float64(domainErr.Min),
							},
							Path: []string{"$query", "cursor"},
						},
					},
				}, body)
			},
		},
		{
			name: "ERR: len(cursor) > 1000",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				q := req.URL.Query()
				q.Set("cursor", gofakeit.LetterN(1001))
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusBadRequest, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
				domainErr := domain.LenOutOfRangeError{
					Min:     1,
					Max:     1000,
					Current: len(req.URL.Query().Get("cursor")),
				}
				assert.Equal(t, apimodel.ErrorResponse{
					Errors: []apimodel.Error{
						{
							Code:    apimodel.ErrorCode(domainErr.Code()),
							Message: domainErr.Error(),
							Params: map[string]any{
								"current": float64(domainErr.Current),
								"max":     float64(domainErr.Max),
								"min":     float64(domainErr.Min),
							},
							Path: []string{"$query", "cursor"},
						},
					},
				}, body)
			},
		},
		{
			name: "ERR: invalid cursor",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				q := req.URL.Query()
				q.Set("cursor", gofakeit.LetterN(100))
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, _ *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusBadRequest, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
				var domainErr domain.Error
				require.ErrorAs(t, domain.ErrInvalidCursor, &domainErr)
				assert.Equal(t, apimodel.ErrorResponse{
					Errors: []apimodel.Error{
						{
							Code:    apimodel.ErrorCode(domainErr.Code()),
							Message: domainErr.Error(),
							Path:    []string{"$query", "cursor"},
						},
					},
				}, body)
			},
		},
		{
			name: "ERR: len(sort) > 1",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				q := req.URL.Query()
				q.Add("sort", "createdAt:DESC")
				q.Add("sort", "createdAt:ASC")
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusBadRequest, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
				domainErr := domain.LenOutOfRangeError{
					Min:     0,
					Max:     1,
					Current: len(req.URL.Query()["sort"]),
				}
				assert.Equal(t, apimodel.ErrorResponse{
					Errors: []apimodel.Error{
						{
							Code:    apimodel.ErrorCode(domainErr.Code()),
							Message: domainErr.Error(),
							Params: map[string]any{
								"current": float64(domainErr.Current),
								"max":     float64(domainErr.Max),
								"min":     float64(domainErr.Min),
							},
							Path: []string{"$query", "sort"},
						},
					},
				}, body)
			},
		},
		{
			name: "ERR: invalid sort",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				q := req.URL.Query()
				q.Add("sort", "createdAt:")
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusBadRequest, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
				domainErr := domain.UnsupportedSortStringError{
					Sort: req.URL.Query()["sort"][0],
				}
				assert.Equal(t, apimodel.ErrorResponse{
					Errors: []apimodel.Error{
						{
							Code:    apimodel.ErrorCode(domainErr.Code()),
							Message: domainErr.Error(),
							Params: map[string]any{
								"sort": domainErr.Sort,
							},
							Path: []string{"$query", "sort", "0"},
						},
					},
				}, body)
			},
		},
		{
			name: "ERR: invalid since",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				q := req.URL.Query()
				q.Set("since", gofakeit.LetterN(100))
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusBadRequest, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
				var domainErr domain.Error
				require.ErrorAs(t, domain.ErrInvalidCursor, &domainErr)
				since := req.URL.Query().Get("since")
				assert.Equal(t, apimodel.ErrorResponse{
					Errors: []apimodel.Error{
						{
							Code: "invalid-param-format",
							//nolint:lll
							Message: fmt.Sprintf("error parsing '%s' as RFC3339 or 2006-01-02 time: parsing time \"%s\" as \"2006-01-02\": cannot parse \"%s\" as \"2006\"", since, since, since),
							Path:    []string{"$query", "since"},
						},
					},
				}, body)
			},
		},
		{
			name: "ERR: invalid before",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				q := req.URL.Query()
				q.Set("before", gofakeit.LetterN(100))
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusBadRequest, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
				var domainErr domain.Error
				require.ErrorAs(t, domain.ErrInvalidCursor, &domainErr)
				before := req.URL.Query().Get("before")
				assert.Equal(t, apimodel.ErrorResponse{
					Errors: []apimodel.Error{
						{
							Code: "invalid-param-format",
							//nolint:lll
							Message: fmt.Sprintf("error parsing '%s' as RFC3339 or 2006-01-02 time: parsing time \"%s\" as \"2006-01-02\": cannot parse \"%s\" as \"2006\"", before, before, before),
							Path:    []string{"$query", "before"},
						},
					},
				}, body)
			},
		},
		{
			name: "ERR: version not found",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				req.URL.Path = fmt.Sprintf(
					endpointListPlayerEnnoblements,
					randInvalidVersionCode(t, handler),
					server.Key,
					player.Id,
				)
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusNotFound, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
				pathSegments := strings.Split(req.URL.Path, "/")
				require.Len(t, pathSegments, 9)
				domainErr := domain.VersionNotFoundError{
					VersionCode: pathSegments[3],
				}
				assert.Equal(t, apimodel.ErrorResponse{
					Errors: []apimodel.Error{
						{
							Code:    apimodel.ErrorCode(domainErr.Code()),
							Message: domainErr.Error(),
							Params: map[string]any{
								"code": domainErr.VersionCode,
							},
						},
					},
				}, body)
			},
		},
		{
			name: "ERR: server not found",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				req.URL.Path = fmt.Sprintf(
					endpointListPlayerEnnoblements,
					server.Version.Code,
					domaintest.RandServerKey(),
					player.Id,
				)
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusNotFound, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
				pathSegments := strings.Split(req.URL.Path, "/")
				require.Len(t, pathSegments, 9)
				domainErr := domain.ServerNotFoundError{
					Key: pathSegments[5],
				}
				assert.Equal(t, apimodel.ErrorResponse{
					Errors: []apimodel.Error{
						{
							Code:    apimodel.ErrorCode(domainErr.Code()),
							Message: domainErr.Error(),
							Params: map[string]any{
								"key": domainErr.Key,
							},
						},
					},
				}, body)
			},
		},
		{
			name: "ERR: player not found",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				req.URL.Path = fmt.Sprintf(
					endpointListPlayerEnnoblements,
					server.Version.Code,
					server.Key,
					domaintest.RandID(),
				)
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusNotFound, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
				pathSegments := strings.Split(req.URL.Path, "/")
				require.Len(t, pathSegments, 9)
				id, err := strconv.Atoi(pathSegments[7])
				require.NoError(t, err)
				domainErr := domain.PlayerNotFoundError{
					ID:        id,
					ServerKey: pathSegments[5],
				}
				assert.Equal(t, apimodel.ErrorResponse{
					Errors: []apimodel.Error{
						{
							Code:    apimodel.ErrorCode(domainErr.Code()),
							Message: domainErr.Error(),
							Params: map[string]any{
								"id":        float64(domainErr.ID),
								"serverKey": domainErr.ServerKey,
							},
						},
					},
				}, body)
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			req := httptest.NewRequest(
				http.MethodGet,
				fmt.Sprintf(endpointListPlayerEnnoblements, server.Version.Code, server.Key, player.Id),
				nil,
			)
			if tt.reqModifier != nil {
				tt.reqModifier(t, req)
			}

			resp := doCustomRequest(handler, req)
			defer resp.Body.Close()
			tt.assertResp(t, req, resp)
		})
	}
}

const endpointListTribeEnnoblements = "/v2/versions/%s/servers/%s/tribes/%d/ennoblements"

func TestListTribeEnnoblements(t *testing.T) {
	t.Parallel()

	handler := newAPIHTTPHandler(t)
	ennoblements := getAllEnnoblements(t, handler)
	var server serverWithVersion
	var tribe apimodel.TribeMeta

	for _, e := range ennoblements {
		if e.NewOwner.Tribe != nil {
			server = e.Server
			tribe = *e.NewOwner.Tribe
			break
		}
	}

	tests := []struct {
		name        string
		reqModifier func(t *testing.T, req *http.Request)
		assertResp  func(t *testing.T, req *http.Request, resp *http.Response)
	}{
		{
			name: "OK: without params",
			assertResp: func(t *testing.T, _ *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusOK, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ListEnnoblementsResponse](t, resp.Body)
				assert.Zero(t, body.Cursor.Next)
				assert.NotZero(t, body.Cursor.Self)
				assert.NotZero(t, body.Data)
				assert.True(t, slices.IsSortedFunc(body.Data, func(a, b apimodel.Ennoblement) int {
					return cmp.Or(
						a.CreatedAt.Compare(b.CreatedAt),
						cmp.Compare(a.Id, b.Id),
					)
				}))
			},
		},
		{
			name: "OK: limit=1",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				q := req.URL.Query()
				q.Set("limit", "1")
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusOK, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ListEnnoblementsResponse](t, resp.Body)
				assert.NotZero(t, body.Cursor.Next)
				assert.NotZero(t, body.Cursor.Self)
				assert.NotZero(t, body.Data)
				limit, err := strconv.Atoi(req.URL.Query().Get("limit"))
				require.NoError(t, err)
				assert.Len(t, body.Data, limit)
			},
		},
		{
			name: "OK: limit=1 cursor",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()

				q := req.URL.Query()
				q.Set("limit", "1")
				req.URL.RawQuery = q.Encode()

				resp := doCustomRequest(handler, req.Clone(req.Context()))
				defer resp.Body.Close()
				body := decodeJSON[apimodel.ListEnnoblementsResponse](t, resp.Body)
				require.NotEmpty(t, body.Cursor.Next)

				q.Set("cursor", body.Cursor.Next)
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusOK, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ListEnnoblementsResponse](t, resp.Body)
				assert.Equal(t, req.URL.Query().Get("cursor"), body.Cursor.Self)
				limit, err := strconv.Atoi(req.URL.Query().Get("limit"))
				require.NoError(t, err)
				assert.Len(t, body.Data, limit)
			},
		},
		{
			name: "OK: sort=[createdAt:DESC]",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				q := req.URL.Query()
				q.Set("sort", "createdAt:DESC")
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, _ *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusOK, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ListEnnoblementsResponse](t, resp.Body)
				assert.Zero(t, body.Cursor.Next)
				assert.NotZero(t, body.Cursor.Self)
				assert.NotZero(t, body.Data)
				assert.True(t, slices.IsSortedFunc(body.Data, func(a, b apimodel.Ennoblement) int {
					return cmp.Or(
						a.CreatedAt.Compare(b.CreatedAt)*-1,
						cmp.Compare(a.Id, b.Id),
					)
				}))
			},
		},
		{
			name: "OK: since",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()

				var filtered []ennoblementWithServer
				for _, e := range ennoblements {
					if e.Server.Key == server.Key {
						filtered = append(filtered, e)
					}
				}
				slices.SortFunc(filtered, func(a, b ennoblementWithServer) int {
					return cmp.Or(
						a.CreatedAt.Compare(b.CreatedAt),
						cmp.Compare(a.Id, b.Id),
					)
				})
				require.GreaterOrEqual(t, len(filtered), 2)

				q := req.URL.Query()
				q.Set("since", filtered[1].CreatedAt.Format(time.RFC3339))
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusOK, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ListEnnoblementsResponse](t, resp.Body)
				assert.Zero(t, body.Cursor.Next)
				assert.NotZero(t, body.Cursor.Self)
				assert.NotZero(t, body.Data)
				since, err := time.Parse(time.RFC3339, req.URL.Query().Get("since"))
				require.NoError(t, err)
				for _, e := range body.Data {
					assert.True(t, e.CreatedAt.After(since) || e.CreatedAt.Equal(since))
				}
			},
		},
		{
			name: "OK: before",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()

				var filtered []ennoblementWithServer
				for _, e := range ennoblements {
					if e.Server.Key == server.Key {
						filtered = append(filtered, e)
					}
				}
				slices.SortFunc(filtered, func(a, b ennoblementWithServer) int {
					return cmp.Or(
						a.CreatedAt.Compare(b.CreatedAt),
						cmp.Compare(a.Id, b.Id),
					)
				})
				require.GreaterOrEqual(t, len(filtered), 2)

				q := req.URL.Query()
				q.Set("before", filtered[1].CreatedAt.Format(time.RFC3339))
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusOK, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ListEnnoblementsResponse](t, resp.Body)
				assert.Zero(t, body.Cursor.Next)
				assert.NotZero(t, body.Cursor.Self)
				assert.NotZero(t, body.Data)
				before, err := time.Parse(time.RFC3339, req.URL.Query().Get("before"))
				require.NoError(t, err)
				for _, e := range body.Data {
					assert.True(t, e.CreatedAt.Before(before))
				}
			},
		},
		{
			name: "ERR: limit is not an integer",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				q := req.URL.Query()
				q.Set("limit", "asd")
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusBadRequest, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
				assert.Equal(t, apimodel.ErrorResponse{
					Errors: []apimodel.Error{
						{
							Code: "invalid-param-format",
							Message: fmt.Sprintf(
								"error binding string parameter: strconv.ParseInt: parsing \"%s\": invalid syntax",
								req.URL.Query().Get("limit"),
							),
							Path: []string{"$query", "limit"},
						},
					},
				}, body)
			},
		},
		{
			name: "ERR: limit < 1",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				q := req.URL.Query()
				q.Set("limit", "0")
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusBadRequest, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
				limit, err := strconv.Atoi(req.URL.Query().Get("limit"))
				require.NoError(t, err)
				domainErr := domain.MinGreaterEqualError{
					Min:     1,
					Current: limit,
				}
				assert.Equal(t, apimodel.ErrorResponse{
					Errors: []apimodel.Error{
						{
							Code:    apimodel.ErrorCode(domainErr.Code()),
							Message: domainErr.Error(),
							Params: map[string]any{
								"current": float64(domainErr.Current),
								"min":     float64(domainErr.Min),
							},
							Path: []string{"$query", "limit"},
						},
					},
				}, body)
			},
		},
		{
			name: fmt.Sprintf("ERR: limit > %d", domain.EnnoblementListMaxLimit),
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				q := req.URL.Query()
				q.Set("limit", strconv.Itoa(domain.EnnoblementListMaxLimit+1))
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusBadRequest, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
				limit, err := strconv.Atoi(req.URL.Query().Get("limit"))
				require.NoError(t, err)
				domainErr := domain.MaxLessEqualError{
					Max:     domain.EnnoblementListMaxLimit,
					Current: limit,
				}
				assert.Equal(t, apimodel.ErrorResponse{
					Errors: []apimodel.Error{
						{
							Code:    apimodel.ErrorCode(domainErr.Code()),
							Message: domainErr.Error(),
							Params: map[string]any{
								"current": float64(domainErr.Current),
								"max":     float64(domainErr.Max),
							},
							Path: []string{"$query", "limit"},
						},
					},
				}, body)
			},
		},
		{
			name: "ERR: len(cursor) < 1",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				q := req.URL.Query()
				q.Set("cursor", "")
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusBadRequest, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
				domainErr := domain.LenOutOfRangeError{
					Min:     1,
					Max:     1000,
					Current: len(req.URL.Query().Get("cursor")),
				}
				assert.Equal(t, apimodel.ErrorResponse{
					Errors: []apimodel.Error{
						{
							Code:    apimodel.ErrorCode(domainErr.Code()),
							Message: domainErr.Error(),
							Params: map[string]any{
								"current": float64(domainErr.Current),
								"max":     float64(domainErr.Max),
								"min":     float64(domainErr.Min),
							},
							Path: []string{"$query", "cursor"},
						},
					},
				}, body)
			},
		},
		{
			name: "ERR: len(cursor) > 1000",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				q := req.URL.Query()
				q.Set("cursor", gofakeit.LetterN(1001))
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusBadRequest, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
				domainErr := domain.LenOutOfRangeError{
					Min:     1,
					Max:     1000,
					Current: len(req.URL.Query().Get("cursor")),
				}
				assert.Equal(t, apimodel.ErrorResponse{
					Errors: []apimodel.Error{
						{
							Code:    apimodel.ErrorCode(domainErr.Code()),
							Message: domainErr.Error(),
							Params: map[string]any{
								"current": float64(domainErr.Current),
								"max":     float64(domainErr.Max),
								"min":     float64(domainErr.Min),
							},
							Path: []string{"$query", "cursor"},
						},
					},
				}, body)
			},
		},
		{
			name: "ERR: invalid cursor",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				q := req.URL.Query()
				q.Set("cursor", gofakeit.LetterN(100))
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, _ *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusBadRequest, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
				var domainErr domain.Error
				require.ErrorAs(t, domain.ErrInvalidCursor, &domainErr)
				assert.Equal(t, apimodel.ErrorResponse{
					Errors: []apimodel.Error{
						{
							Code:    apimodel.ErrorCode(domainErr.Code()),
							Message: domainErr.Error(),
							Path:    []string{"$query", "cursor"},
						},
					},
				}, body)
			},
		},
		{
			name: "ERR: len(sort) > 1",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				q := req.URL.Query()
				q.Add("sort", "createdAt:DESC")
				q.Add("sort", "createdAt:ASC")
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusBadRequest, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
				domainErr := domain.LenOutOfRangeError{
					Min:     0,
					Max:     1,
					Current: len(req.URL.Query()["sort"]),
				}
				assert.Equal(t, apimodel.ErrorResponse{
					Errors: []apimodel.Error{
						{
							Code:    apimodel.ErrorCode(domainErr.Code()),
							Message: domainErr.Error(),
							Params: map[string]any{
								"current": float64(domainErr.Current),
								"max":     float64(domainErr.Max),
								"min":     float64(domainErr.Min),
							},
							Path: []string{"$query", "sort"},
						},
					},
				}, body)
			},
		},
		{
			name: "ERR: invalid sort",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				q := req.URL.Query()
				q.Add("sort", "createdAt:")
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusBadRequest, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
				domainErr := domain.UnsupportedSortStringError{
					Sort: req.URL.Query()["sort"][0],
				}
				assert.Equal(t, apimodel.ErrorResponse{
					Errors: []apimodel.Error{
						{
							Code:    apimodel.ErrorCode(domainErr.Code()),
							Message: domainErr.Error(),
							Params: map[string]any{
								"sort": domainErr.Sort,
							},
							Path: []string{"$query", "sort", "0"},
						},
					},
				}, body)
			},
		},
		{
			name: "ERR: invalid since",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				q := req.URL.Query()
				q.Set("since", gofakeit.LetterN(100))
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusBadRequest, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
				var domainErr domain.Error
				require.ErrorAs(t, domain.ErrInvalidCursor, &domainErr)
				since := req.URL.Query().Get("since")
				assert.Equal(t, apimodel.ErrorResponse{
					Errors: []apimodel.Error{
						{
							Code: "invalid-param-format",
							//nolint:lll
							Message: fmt.Sprintf("error parsing '%s' as RFC3339 or 2006-01-02 time: parsing time \"%s\" as \"2006-01-02\": cannot parse \"%s\" as \"2006\"", since, since, since),
							Path:    []string{"$query", "since"},
						},
					},
				}, body)
			},
		},
		{
			name: "ERR: invalid before",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				q := req.URL.Query()
				q.Set("before", gofakeit.LetterN(100))
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusBadRequest, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
				var domainErr domain.Error
				require.ErrorAs(t, domain.ErrInvalidCursor, &domainErr)
				before := req.URL.Query().Get("before")
				assert.Equal(t, apimodel.ErrorResponse{
					Errors: []apimodel.Error{
						{
							Code: "invalid-param-format",
							//nolint:lll
							Message: fmt.Sprintf("error parsing '%s' as RFC3339 or 2006-01-02 time: parsing time \"%s\" as \"2006-01-02\": cannot parse \"%s\" as \"2006\"", before, before, before),
							Path:    []string{"$query", "before"},
						},
					},
				}, body)
			},
		},
		{
			name: "ERR: version not found",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				req.URL.Path = fmt.Sprintf(endpointListTribeEnnoblements, randInvalidVersionCode(t, handler), server.Key, tribe.Id)
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusNotFound, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
				pathSegments := strings.Split(req.URL.Path, "/")
				require.Len(t, pathSegments, 9)
				domainErr := domain.VersionNotFoundError{
					VersionCode: pathSegments[3],
				}
				assert.Equal(t, apimodel.ErrorResponse{
					Errors: []apimodel.Error{
						{
							Code:    apimodel.ErrorCode(domainErr.Code()),
							Message: domainErr.Error(),
							Params: map[string]any{
								"code": domainErr.VersionCode,
							},
						},
					},
				}, body)
			},
		},
		{
			name: "ERR: server not found",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				req.URL.Path = fmt.Sprintf(
					endpointListTribeEnnoblements,
					server.Version.Code,
					domaintest.RandServerKey(),
					tribe.Id,
				)
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusNotFound, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
				pathSegments := strings.Split(req.URL.Path, "/")
				require.Len(t, pathSegments, 9)
				domainErr := domain.ServerNotFoundError{
					Key: pathSegments[5],
				}
				assert.Equal(t, apimodel.ErrorResponse{
					Errors: []apimodel.Error{
						{
							Code:    apimodel.ErrorCode(domainErr.Code()),
							Message: domainErr.Error(),
							Params: map[string]any{
								"key": domainErr.Key,
							},
						},
					},
				}, body)
			},
		},
		{
			name: "ERR: tribe not found",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				req.URL.Path = fmt.Sprintf(
					endpointListTribeEnnoblements,
					server.Version.Code,
					server.Key,
					domaintest.RandID(),
				)
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusNotFound, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
				pathSegments := strings.Split(req.URL.Path, "/")
				require.Len(t, pathSegments, 9)
				id, err := strconv.Atoi(pathSegments[7])
				require.NoError(t, err)
				domainErr := domain.TribeNotFoundError{
					ID:        id,
					ServerKey: pathSegments[5],
				}
				assert.Equal(t, apimodel.ErrorResponse{
					Errors: []apimodel.Error{
						{
							Code:    apimodel.ErrorCode(domainErr.Code()),
							Message: domainErr.Error(),
							Params: map[string]any{
								"id":        float64(domainErr.ID),
								"serverKey": domainErr.ServerKey,
							},
						},
					},
				}, body)
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			req := httptest.NewRequest(
				http.MethodGet,
				fmt.Sprintf(endpointListTribeEnnoblements, server.Version.Code, server.Key, tribe.Id),
				nil,
			)
			if tt.reqModifier != nil {
				tt.reqModifier(t, req)
			}

			resp := doCustomRequest(handler, req)
			defer resp.Body.Close()
			tt.assertResp(t, req, resp)
		})
	}
}

const endpointListVillageEnnoblements = "/v2/versions/%s/servers/%s/villages/%d/ennoblements"

func TestListVillageEnnoblements(t *testing.T) {
	t.Parallel()

	handler := newAPIHTTPHandler(t)
	ennoblements := getAllEnnoblements(t, handler)
	server := ennoblements[0].Server
	village := ennoblements[0].Village

	tests := []struct {
		name        string
		reqModifier func(t *testing.T, req *http.Request)
		assertResp  func(t *testing.T, req *http.Request, resp *http.Response)
	}{
		{
			name: "OK: without params",
			assertResp: func(t *testing.T, _ *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusOK, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ListEnnoblementsResponse](t, resp.Body)
				assert.Zero(t, body.Cursor.Next)
				assert.NotZero(t, body.Cursor.Self)
				assert.NotZero(t, body.Data)
				assert.True(t, slices.IsSortedFunc(body.Data, func(a, b apimodel.Ennoblement) int {
					return cmp.Or(
						a.CreatedAt.Compare(b.CreatedAt),
						cmp.Compare(a.Id, b.Id),
					)
				}))
			},
		},
		{
			name: "OK: limit=1",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				q := req.URL.Query()
				q.Set("limit", "1")
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusOK, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ListEnnoblementsResponse](t, resp.Body)
				assert.NotZero(t, body.Cursor.Next)
				assert.NotZero(t, body.Cursor.Self)
				assert.NotZero(t, body.Data)
				limit, err := strconv.Atoi(req.URL.Query().Get("limit"))
				require.NoError(t, err)
				assert.Len(t, body.Data, limit)
			},
		},
		{
			name: "OK: limit=1 cursor",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()

				q := req.URL.Query()
				q.Set("limit", "1")
				req.URL.RawQuery = q.Encode()

				resp := doCustomRequest(handler, req.Clone(req.Context()))
				defer resp.Body.Close()
				body := decodeJSON[apimodel.ListEnnoblementsResponse](t, resp.Body)
				require.NotEmpty(t, body.Cursor.Next)

				q.Set("cursor", body.Cursor.Next)
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusOK, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ListEnnoblementsResponse](t, resp.Body)
				assert.Equal(t, req.URL.Query().Get("cursor"), body.Cursor.Self)
				limit, err := strconv.Atoi(req.URL.Query().Get("limit"))
				require.NoError(t, err)
				assert.Len(t, body.Data, limit)
			},
		},
		{
			name: "OK: sort=[createdAt:DESC]",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				q := req.URL.Query()
				q.Set("sort", "createdAt:DESC")
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, _ *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusOK, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ListEnnoblementsResponse](t, resp.Body)
				assert.Zero(t, body.Cursor.Next)
				assert.NotZero(t, body.Cursor.Self)
				assert.NotZero(t, body.Data)
				assert.True(t, slices.IsSortedFunc(body.Data, func(a, b apimodel.Ennoblement) int {
					return cmp.Or(
						a.CreatedAt.Compare(b.CreatedAt)*-1,
						cmp.Compare(a.Id, b.Id),
					)
				}))
			},
		},
		{
			name: "OK: since",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()

				var filtered []ennoblementWithServer
				for _, e := range ennoblements {
					if e.Server.Key == server.Key {
						filtered = append(filtered, e)
					}
				}
				slices.SortFunc(filtered, func(a, b ennoblementWithServer) int {
					return cmp.Or(
						a.CreatedAt.Compare(b.CreatedAt),
						cmp.Compare(a.Id, b.Id),
					)
				})
				require.GreaterOrEqual(t, len(filtered), 2)

				q := req.URL.Query()
				q.Set("since", filtered[1].CreatedAt.Format(time.RFC3339))
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusOK, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ListEnnoblementsResponse](t, resp.Body)
				assert.Zero(t, body.Cursor.Next)
				assert.NotZero(t, body.Cursor.Self)
				assert.NotZero(t, body.Data)
				since, err := time.Parse(time.RFC3339, req.URL.Query().Get("since"))
				require.NoError(t, err)
				for _, e := range body.Data {
					assert.True(t, e.CreatedAt.After(since) || e.CreatedAt.Equal(since))
				}
			},
		},
		{
			name: "OK: before",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()

				var filtered []ennoblementWithServer
				for _, e := range ennoblements {
					if e.Server.Key == server.Key {
						filtered = append(filtered, e)
					}
				}
				slices.SortFunc(filtered, func(a, b ennoblementWithServer) int {
					return cmp.Or(
						a.CreatedAt.Compare(b.CreatedAt),
						cmp.Compare(a.Id, b.Id),
					)
				})
				require.GreaterOrEqual(t, len(filtered), 2)

				q := req.URL.Query()
				q.Set("before", filtered[1].CreatedAt.Format(time.RFC3339))
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusOK, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ListEnnoblementsResponse](t, resp.Body)
				assert.Zero(t, body.Cursor.Next)
				assert.NotZero(t, body.Cursor.Self)
				assert.NotZero(t, body.Data)
				before, err := time.Parse(time.RFC3339, req.URL.Query().Get("before"))
				require.NoError(t, err)
				for _, e := range body.Data {
					assert.True(t, e.CreatedAt.Before(before))
				}
			},
		},
		{
			name: "ERR: limit is not an integer",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				q := req.URL.Query()
				q.Set("limit", "asd")
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusBadRequest, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
				assert.Equal(t, apimodel.ErrorResponse{
					Errors: []apimodel.Error{
						{
							Code: "invalid-param-format",
							Message: fmt.Sprintf(
								"error binding string parameter: strconv.ParseInt: parsing \"%s\": invalid syntax",
								req.URL.Query().Get("limit"),
							),
							Path: []string{"$query", "limit"},
						},
					},
				}, body)
			},
		},
		{
			name: "ERR: limit < 1",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				q := req.URL.Query()
				q.Set("limit", "0")
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusBadRequest, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
				limit, err := strconv.Atoi(req.URL.Query().Get("limit"))
				require.NoError(t, err)
				domainErr := domain.MinGreaterEqualError{
					Min:     1,
					Current: limit,
				}
				assert.Equal(t, apimodel.ErrorResponse{
					Errors: []apimodel.Error{
						{
							Code:    apimodel.ErrorCode(domainErr.Code()),
							Message: domainErr.Error(),
							Params: map[string]any{
								"current": float64(domainErr.Current),
								"min":     float64(domainErr.Min),
							},
							Path: []string{"$query", "limit"},
						},
					},
				}, body)
			},
		},
		{
			name: fmt.Sprintf("ERR: limit > %d", domain.EnnoblementListMaxLimit),
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				q := req.URL.Query()
				q.Set("limit", strconv.Itoa(domain.EnnoblementListMaxLimit+1))
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusBadRequest, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
				limit, err := strconv.Atoi(req.URL.Query().Get("limit"))
				require.NoError(t, err)
				domainErr := domain.MaxLessEqualError{
					Max:     domain.EnnoblementListMaxLimit,
					Current: limit,
				}
				assert.Equal(t, apimodel.ErrorResponse{
					Errors: []apimodel.Error{
						{
							Code:    apimodel.ErrorCode(domainErr.Code()),
							Message: domainErr.Error(),
							Params: map[string]any{
								"current": float64(domainErr.Current),
								"max":     float64(domainErr.Max),
							},
							Path: []string{"$query", "limit"},
						},
					},
				}, body)
			},
		},
		{
			name: "ERR: len(cursor) < 1",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				q := req.URL.Query()
				q.Set("cursor", "")
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusBadRequest, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
				domainErr := domain.LenOutOfRangeError{
					Min:     1,
					Max:     1000,
					Current: len(req.URL.Query().Get("cursor")),
				}
				assert.Equal(t, apimodel.ErrorResponse{
					Errors: []apimodel.Error{
						{
							Code:    apimodel.ErrorCode(domainErr.Code()),
							Message: domainErr.Error(),
							Params: map[string]any{
								"current": float64(domainErr.Current),
								"max":     float64(domainErr.Max),
								"min":     float64(domainErr.Min),
							},
							Path: []string{"$query", "cursor"},
						},
					},
				}, body)
			},
		},
		{
			name: "ERR: len(cursor) > 1000",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				q := req.URL.Query()
				q.Set("cursor", gofakeit.LetterN(1001))
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusBadRequest, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
				domainErr := domain.LenOutOfRangeError{
					Min:     1,
					Max:     1000,
					Current: len(req.URL.Query().Get("cursor")),
				}
				assert.Equal(t, apimodel.ErrorResponse{
					Errors: []apimodel.Error{
						{
							Code:    apimodel.ErrorCode(domainErr.Code()),
							Message: domainErr.Error(),
							Params: map[string]any{
								"current": float64(domainErr.Current),
								"max":     float64(domainErr.Max),
								"min":     float64(domainErr.Min),
							},
							Path: []string{"$query", "cursor"},
						},
					},
				}, body)
			},
		},
		{
			name: "ERR: invalid cursor",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				q := req.URL.Query()
				q.Set("cursor", gofakeit.LetterN(100))
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, _ *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusBadRequest, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
				var domainErr domain.Error
				require.ErrorAs(t, domain.ErrInvalidCursor, &domainErr)
				assert.Equal(t, apimodel.ErrorResponse{
					Errors: []apimodel.Error{
						{
							Code:    apimodel.ErrorCode(domainErr.Code()),
							Message: domainErr.Error(),
							Path:    []string{"$query", "cursor"},
						},
					},
				}, body)
			},
		},
		{
			name: "ERR: len(sort) > 1",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				q := req.URL.Query()
				q.Add("sort", "createdAt:DESC")
				q.Add("sort", "createdAt:ASC")
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusBadRequest, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
				domainErr := domain.LenOutOfRangeError{
					Min:     0,
					Max:     1,
					Current: len(req.URL.Query()["sort"]),
				}
				assert.Equal(t, apimodel.ErrorResponse{
					Errors: []apimodel.Error{
						{
							Code:    apimodel.ErrorCode(domainErr.Code()),
							Message: domainErr.Error(),
							Params: map[string]any{
								"current": float64(domainErr.Current),
								"max":     float64(domainErr.Max),
								"min":     float64(domainErr.Min),
							},
							Path: []string{"$query", "sort"},
						},
					},
				}, body)
			},
		},
		{
			name: "ERR: invalid sort",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				q := req.URL.Query()
				q.Add("sort", "createdAt:")
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusBadRequest, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
				domainErr := domain.UnsupportedSortStringError{
					Sort: req.URL.Query()["sort"][0],
				}
				assert.Equal(t, apimodel.ErrorResponse{
					Errors: []apimodel.Error{
						{
							Code:    apimodel.ErrorCode(domainErr.Code()),
							Message: domainErr.Error(),
							Params: map[string]any{
								"sort": domainErr.Sort,
							},
							Path: []string{"$query", "sort", "0"},
						},
					},
				}, body)
			},
		},
		{
			name: "ERR: invalid since",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				q := req.URL.Query()
				q.Set("since", gofakeit.LetterN(100))
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusBadRequest, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
				var domainErr domain.Error
				require.ErrorAs(t, domain.ErrInvalidCursor, &domainErr)
				since := req.URL.Query().Get("since")
				assert.Equal(t, apimodel.ErrorResponse{
					Errors: []apimodel.Error{
						{
							Code: "invalid-param-format",
							//nolint:lll
							Message: fmt.Sprintf("error parsing '%s' as RFC3339 or 2006-01-02 time: parsing time \"%s\" as \"2006-01-02\": cannot parse \"%s\" as \"2006\"", since, since, since),
							Path:    []string{"$query", "since"},
						},
					},
				}, body)
			},
		},
		{
			name: "ERR: invalid before",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				q := req.URL.Query()
				q.Set("before", gofakeit.LetterN(100))
				req.URL.RawQuery = q.Encode()
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusBadRequest, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
				var domainErr domain.Error
				require.ErrorAs(t, domain.ErrInvalidCursor, &domainErr)
				before := req.URL.Query().Get("before")
				assert.Equal(t, apimodel.ErrorResponse{
					Errors: []apimodel.Error{
						{
							Code: "invalid-param-format",
							//nolint:lll
							Message: fmt.Sprintf("error parsing '%s' as RFC3339 or 2006-01-02 time: parsing time \"%s\" as \"2006-01-02\": cannot parse \"%s\" as \"2006\"", before, before, before),
							Path:    []string{"$query", "before"},
						},
					},
				}, body)
			},
		},
		{
			name: "ERR: version not found",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				req.URL.Path = fmt.Sprintf(
					endpointListVillageEnnoblements,
					randInvalidVersionCode(t, handler),
					server.Key,
					village.Id,
				)
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusNotFound, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
				pathSegments := strings.Split(req.URL.Path, "/")
				require.Len(t, pathSegments, 9)
				domainErr := domain.VersionNotFoundError{
					VersionCode: pathSegments[3],
				}
				assert.Equal(t, apimodel.ErrorResponse{
					Errors: []apimodel.Error{
						{
							Code:    apimodel.ErrorCode(domainErr.Code()),
							Message: domainErr.Error(),
							Params: map[string]any{
								"code": domainErr.VersionCode,
							},
						},
					},
				}, body)
			},
		},
		{
			name: "ERR: server not found",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				req.URL.Path = fmt.Sprintf(
					endpointListVillageEnnoblements,
					server.Version.Code,
					domaintest.RandServerKey(),
					village.Id,
				)
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusNotFound, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
				pathSegments := strings.Split(req.URL.Path, "/")
				require.Len(t, pathSegments, 9)
				domainErr := domain.ServerNotFoundError{
					Key: pathSegments[5],
				}
				assert.Equal(t, apimodel.ErrorResponse{
					Errors: []apimodel.Error{
						{
							Code:    apimodel.ErrorCode(domainErr.Code()),
							Message: domainErr.Error(),
							Params: map[string]any{
								"key": domainErr.Key,
							},
						},
					},
				}, body)
			},
		},
		{
			name: "ERR: village not found",
			reqModifier: func(t *testing.T, req *http.Request) {
				t.Helper()
				req.URL.Path = fmt.Sprintf(
					endpointListVillageEnnoblements,
					server.Version.Code,
					server.Key,
					domaintest.RandID(),
				)
			},
			assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
				t.Helper()

				assert.Equal(t, http.StatusNotFound, resp.StatusCode)

				// body
				body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
				pathSegments := strings.Split(req.URL.Path, "/")
				require.Len(t, pathSegments, 9)
				id, err := strconv.Atoi(pathSegments[7])
				require.NoError(t, err)
				domainErr := domain.VillageNotFoundError{
					ID:        id,
					ServerKey: pathSegments[5],
				}
				assert.Equal(t, apimodel.ErrorResponse{
					Errors: []apimodel.Error{
						{
							Code:    apimodel.ErrorCode(domainErr.Code()),
							Message: domainErr.Error(),
							Params: map[string]any{
								"id":        float64(domainErr.ID),
								"serverKey": domainErr.ServerKey,
							},
						},
					},
				}, body)
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			req := httptest.NewRequest(
				http.MethodGet,
				fmt.Sprintf(endpointListVillageEnnoblements, server.Version.Code, server.Key, village.Id),
				nil,
			)
			if tt.reqModifier != nil {
				tt.reqModifier(t, req)
			}

			resp := doCustomRequest(handler, req)
			defer resp.Body.Close()
			tt.assertResp(t, req, resp)
		})
	}
}

type ennoblementWithServer struct {
	apimodel.Ennoblement
	Server serverWithVersion
}

func getAllEnnoblements(tb testing.TB, h http.Handler) []ennoblementWithServer {
	tb.Helper()

	servers := getAllServers(tb, h)

	var ennoblements []ennoblementWithServer

	for _, s := range servers {
		resp := doRequest(h, http.MethodGet, fmt.Sprintf(endpointListEnnoblements, s.Version.Code, s.Key), nil)
		require.Equal(tb, http.StatusOK, resp.StatusCode)

		for _, e := range decodeJSON[apimodel.ListEnnoblementsResponse](tb, resp.Body).Data {
			ennoblements = append(ennoblements, ennoblementWithServer{
				Ennoblement: e,
				Server:      s,
			})
		}

		_ = resp.Body.Close()
	}

	require.NotZero(tb, ennoblements)

	return ennoblements
}
