package paginate

import (
	"encoding/base64"
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/gofiber/fiber/v3"
	"github.com/stretchr/testify/require"
)

type paginateResponse struct {
	NextPageURL     string      `json:"next_page_url"`
	PreviousPageURL string      `json:"prev_page_url"`
	Sort            []SortField `json:"sort"`
	Page            int         `json:"page"`
	Limit           int         `json:"limit"`
	Offset          int         `json:"offset"`
	Start           int         `json:"start"`
}

type cursorResponse struct {
	Cursor     string      `json:"cursor"`
	NextCursor string      `json:"next_cursor"`
	Sort       []SortField `json:"sort"`
	Limit      int         `json:"limit"`
	HasMore    bool        `json:"has_more"`
}

// --- Config tests ---

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

	cfg := configDefault()
	require.Equal(t, "page", cfg.PageKey)
	require.Equal(t, 1, cfg.DefaultPage)
	require.Equal(t, "limit", cfg.LimitKey)
	require.Equal(t, 10, cfg.DefaultLimit)
	require.Equal(t, DefaultMaxLimit, cfg.MaxLimit)
}

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

	cfg := configDefault(Config{
		PageKey:      "p",
		LimitKey:     "l",
		DefaultPage:  5,
		DefaultLimit: 50,
	})
	require.Equal(t, "p", cfg.PageKey)
	require.Equal(t, "l", cfg.LimitKey)
	require.Equal(t, 5, cfg.DefaultPage)
	require.Equal(t, 50, cfg.DefaultLimit)
}

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

	cfg := configDefault()
	require.Equal(t, "cursor", cfg.CursorKey)
}

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

	cfg := configDefault(Config{
		CursorKey:   "after",
		CursorParam: "starting_after",
	})
	require.Equal(t, "after", cfg.CursorKey)
	require.Equal(t, "starting_after", cfg.CursorParam)
}

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

	cfg := configDefault(Config{
		DefaultPage:  -1,
		DefaultLimit: -1,
	})
	require.Equal(t, 1, cfg.DefaultPage)
	require.Equal(t, 10, cfg.DefaultLimit)
}

// --- PageInfo tests ---

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

	tests := []struct {
		input    string
		expected SortOrder
	}{
		{"asc", ASC},
		{"desc", DESC},
		{"DESC", DESC},
		{"Desc", DESC},
		{"invalid", ASC},
		{"", ASC},
	}

	for _, tt := range tests {
		t.Run(tt.input, func(t *testing.T) {
			t.Parallel()
			require.Equal(t, tt.expected, SortOrderFromString(tt.input))
		})
	}
}

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

	tests := []struct {
		name     string
		pageInfo PageInfo
		expected int
	}{
		{"Page 1, limit 10", PageInfo{Page: 1, Limit: 10}, 0},
		{"Page 1, limit 1", PageInfo{Page: 1, Limit: 1}, 0},
		{"Page 2, limit 10", PageInfo{Page: 2, Limit: 10}, 10},
		{"Page 2, limit 1", PageInfo{Page: 2, Limit: 1}, 1},
		{"Page 3, limit 20", PageInfo{Page: 3, Limit: 20}, 40},
		{"With offset", PageInfo{Page: 2, Limit: 10, Offset: 25}, 25},
		{"Zero page", PageInfo{Page: 0, Limit: 10}, 0},
		{"Overflow clamps to MaxInt", PageInfo{Page: int(^uint(0) >> 1), Limit: 100}, int(^uint(0) >> 1)},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			require.Equal(t, tt.expected, tt.pageInfo.Start())
		})
	}
}

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

	p := NewPageInfo(1, 10, 0, nil)
	p.SortBy("name", ASC).SortBy("date", DESC)

	require.Len(t, p.Sort, 2)
	require.Equal(t, "name", p.Sort[0].Field)
	require.Equal(t, ASC, p.Sort[0].Order)
	require.Equal(t, "date", p.Sort[1].Field)
	require.Equal(t, DESC, p.Sort[1].Order)
}

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

	tests := []struct {
		name     string
		baseURL  string
		expected string
		pageInfo PageInfo
	}{
		{
			name:     "Middle page",
			baseURL:  "https://example.com/users",
			expected: "https://example.com/users?limit=10&page=3",
			pageInfo: PageInfo{Page: 2, Limit: 10},
		},
		{
			name:     "First page",
			baseURL:  "https://example.com/users",
			expected: "https://example.com/users?limit=20&page=2",
			pageInfo: PageInfo{Page: 1, Limit: 20},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			require.Equal(t, tt.expected, tt.pageInfo.NextPageURL(tt.baseURL))
		})
	}
}

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

	tests := []struct {
		name     string
		baseURL  string
		expected string
		pageInfo PageInfo
	}{
		{
			name:     "Middle page",
			baseURL:  "https://example.com/users",
			expected: "https://example.com/users?limit=10&page=1",
			pageInfo: PageInfo{Page: 2, Limit: 10},
		},
		{
			name:     "First page returns empty",
			baseURL:  "https://example.com/users",
			expected: "",
			pageInfo: PageInfo{Page: 1, Limit: 20},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			require.Equal(t, tt.expected, tt.pageInfo.PreviousPageURL(tt.baseURL))
		})
	}
}

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

	// In cursor mode, Page is 0 (not set). Start() should return 0, not negative.
	p := &PageInfo{Page: 0, Limit: 20}
	require.Equal(t, 0, p.Start())
}

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

	p := PageInfo{Page: 2, Limit: 10}
	result := p.NextPageURL("https://example.com/users?filter=active")
	require.Contains(t, result, "filter=active")
	require.Contains(t, result, "page=3")
	require.Contains(t, result, "limit=10")
}

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

	p := PageInfo{Page: 3, Limit: 10}
	result := p.PreviousPageURL("https://example.com/users?filter=active")
	require.Contains(t, result, "filter=active")
	require.Contains(t, result, "page=2")
	require.Contains(t, result, "limit=10")
}

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

	p := &PageInfo{
		Cursor:     "abc123",
		HasMore:    true,
		NextCursor: "def456",
	}

	require.Equal(t, "abc123", p.Cursor)
	require.True(t, p.HasMore)
	require.Equal(t, "def456", p.NextCursor)
}

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

	original := map[string]any{
		"id":         float64(42),
		"created_at": "2026-01-01T00:00:00Z",
	}

	p := &PageInfo{}
	require.NoError(t, p.SetNextCursor(original))

	require.True(t, p.HasMore)
	require.NotEmpty(t, p.NextCursor)

	p2 := &PageInfo{Cursor: p.NextCursor}
	decoded := p2.CursorValues()

	require.NotNil(t, decoded)
	require.InEpsilon(t, float64(42), decoded["id"], 0)
	require.Equal(t, "2026-01-01T00:00:00Z", decoded["created_at"])
}

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

	p := &PageInfo{Cursor: ""}
	require.Nil(t, p.CursorValues())
}

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

	p := &PageInfo{Cursor: "not-valid-base64!!!"}
	require.Nil(t, p.CursorValues())
}

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

	p := &PageInfo{Cursor: "bm90LWpzb24"}
	require.Nil(t, p.CursorValues())
}

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

	t.Run("with HasMore", func(t *testing.T) {
		t.Parallel()
		p := &PageInfo{Limit: 20}
		require.NoError(t, p.SetNextCursor(map[string]any{"id": float64(42)}))

		url := p.NextCursorURL("https://example.com/users")
		expected := fmt.Sprintf("https://example.com/users?cursor=%s&limit=20", p.NextCursor)
		require.Equal(t, expected, url)
	})

	t.Run("without HasMore", func(t *testing.T) {
		t.Parallel()
		p := &PageInfo{Limit: 20}
		require.Empty(t, p.NextCursorURL("https://example.com/users"))
	})
}

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

	p := &PageInfo{Limit: 10}
	require.NoError(t, p.SetNextCursor(map[string]any{"id": float64(1)}))
	require.True(t, p.HasMore)
	require.NotEmpty(t, p.NextCursor)
}

// --- Middleware handler tests ---

func Test_PaginateWithQueries(t *testing.T) {
	t.Parallel()
	app := fiber.New()

	app.Use(New(Config{
		DefaultSort: "id",
	}))

	app.Get("/", func(c fiber.Ctx) error {
		pageInfo, ok := FromContext(c)
		if !ok {
			return fiber.ErrBadRequest
		}

		return c.JSON(paginateResponse{
			Page:            pageInfo.Page,
			Limit:           pageInfo.Limit,
			Offset:          pageInfo.Offset,
			Start:           pageInfo.Start(),
			Sort:            pageInfo.Sort,
			NextPageURL:     pageInfo.NextPageURL(c.BaseURL()),
			PreviousPageURL: pageInfo.PreviousPageURL(c.BaseURL()),
		})
	})

	resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/?page=2&limit=20", http.NoBody))
	require.NoError(t, err)
	defer resp.Body.Close() //nolint:errcheck // close error not relevant in tests
	require.Equal(t, fiber.StatusOK, resp.StatusCode)

	var body paginateResponse
	require.NoError(t, json.NewDecoder(resp.Body).Decode(&body))

	require.Equal(t, 2, body.Page)
	require.Equal(t, 20, body.Limit)
	require.Equal(t, 0, body.Offset)
	require.Equal(t, 20, body.Start)
	require.Equal(t, "http://example.com?limit=20&page=3", body.NextPageURL)
	require.Equal(t, "http://example.com?limit=20&page=1", body.PreviousPageURL)
	require.Equal(t, []SortField{{Field: "id", Order: ASC}}, body.Sort)
}

func Test_PaginateWithOffset(t *testing.T) {
	t.Parallel()
	app := fiber.New()
	app.Use(New())

	app.Get("/", func(c fiber.Ctx) error {
		pageInfo, ok := FromContext(c)
		if !ok {
			return fiber.ErrBadRequest
		}
		return c.JSON(paginateResponse{
			Page:   pageInfo.Page,
			Limit:  pageInfo.Limit,
			Offset: pageInfo.Offset,
			Start:  pageInfo.Start(),
			Sort:   pageInfo.Sort,
		})
	})

	resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/?offset=20&limit=20", http.NoBody))
	require.NoError(t, err)
	defer resp.Body.Close() //nolint:errcheck // close error not relevant in tests
	require.Equal(t, fiber.StatusOK, resp.StatusCode)

	var body paginateResponse
	require.NoError(t, json.NewDecoder(resp.Body).Decode(&body))

	require.Equal(t, 1, body.Page)
	require.Equal(t, 20, body.Limit)
	require.Equal(t, 20, body.Offset)
	require.Equal(t, 20, body.Start)
}

func Test_PaginateCheckDefaultsWhenNoQueries(t *testing.T) {
	t.Parallel()
	app := fiber.New()
	app.Use(New())

	app.Get("/", func(c fiber.Ctx) error {
		pageInfo, ok := FromContext(c)
		if !ok {
			return fiber.ErrBadRequest
		}
		return c.JSON(paginateResponse{
			Page:   pageInfo.Page,
			Limit:  pageInfo.Limit,
			Offset: pageInfo.Offset,
			Start:  pageInfo.Start(),
			Sort:   pageInfo.Sort,
		})
	})

	resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", http.NoBody))
	require.NoError(t, err)
	defer resp.Body.Close() //nolint:errcheck // close error not relevant in tests
	require.Equal(t, fiber.StatusOK, resp.StatusCode)

	var body paginateResponse
	require.NoError(t, json.NewDecoder(resp.Body).Decode(&body))

	require.Equal(t, 1, body.Page)
	require.Equal(t, 10, body.Limit)
	require.Equal(t, 0, body.Offset)
	require.Equal(t, 0, body.Start)
	require.Equal(t, []SortField{{Field: "id", Order: ASC}}, body.Sort)
}

func Test_PaginateCheckDefaultsWhenNoPage(t *testing.T) {
	t.Parallel()
	app := fiber.New()
	app.Use(New())

	app.Get("/", func(c fiber.Ctx) error {
		pageInfo, ok := FromContext(c)
		if !ok {
			return fiber.ErrBadRequest
		}
		return c.JSON(paginateResponse{
			Page:  pageInfo.Page,
			Limit: pageInfo.Limit,
			Start: pageInfo.Start(),
		})
	})

	resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/?limit=20", http.NoBody))
	require.NoError(t, err)
	defer resp.Body.Close() //nolint:errcheck // close error not relevant in tests

	var body paginateResponse
	require.NoError(t, json.NewDecoder(resp.Body).Decode(&body))

	require.Equal(t, 1, body.Page)
	require.Equal(t, 20, body.Limit)
	require.Equal(t, 0, body.Start)
}

func Test_PaginateCheckDefaultsWhenNoLimit(t *testing.T) {
	t.Parallel()
	app := fiber.New()
	app.Use(New())

	app.Get("/", func(c fiber.Ctx) error {
		pageInfo, ok := FromContext(c)
		if !ok {
			return fiber.ErrBadRequest
		}
		return c.JSON(paginateResponse{
			Page:  pageInfo.Page,
			Limit: pageInfo.Limit,
			Start: pageInfo.Start(),
		})
	})

	resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/?page=2", http.NoBody))
	require.NoError(t, err)
	defer resp.Body.Close() //nolint:errcheck // close error not relevant in tests

	var body paginateResponse
	require.NoError(t, json.NewDecoder(resp.Body).Decode(&body))

	require.Equal(t, 2, body.Page)
	require.Equal(t, 10, body.Limit)
	require.Equal(t, 10, body.Start)
}

func Test_PaginateConfigDefaultPageDefaultLimit(t *testing.T) {
	t.Parallel()
	app := fiber.New()
	app.Use(New(Config{
		DefaultPage:  100,
		DefaultLimit: DefaultMaxLimit,
		DefaultSort:  "name",
	}))

	app.Get("/", func(c fiber.Ctx) error {
		pageInfo, ok := FromContext(c)
		if !ok {
			return fiber.ErrBadRequest
		}
		return c.JSON(paginateResponse{
			Page:  pageInfo.Page,
			Limit: pageInfo.Limit,
			Start: pageInfo.Start(),
			Sort:  pageInfo.Sort,
		})
	})

	resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", http.NoBody))
	require.NoError(t, err)
	defer resp.Body.Close() //nolint:errcheck // close error not relevant in tests

	var body paginateResponse
	require.NoError(t, json.NewDecoder(resp.Body).Decode(&body))

	require.Equal(t, 100, body.Page)
	require.Equal(t, DefaultMaxLimit, body.Limit)
	require.Equal(t, 9900, body.Start)
	require.Equal(t, []SortField{{Field: "name", Order: ASC}}, body.Sort)
}

func Test_PaginateConfigPageKeyLimitKey(t *testing.T) {
	t.Parallel()
	app := fiber.New()
	app.Use(New(Config{
		PageKey:     "site",
		LimitKey:    "size",
		DefaultSort: "id",
	}))

	app.Get("/", func(c fiber.Ctx) error {
		pageInfo, ok := FromContext(c)
		if !ok {
			return fiber.ErrBadRequest
		}
		return c.JSON(paginateResponse{
			Page:  pageInfo.Page,
			Limit: pageInfo.Limit,
			Start: pageInfo.Start(),
			Sort:  pageInfo.Sort,
		})
	})

	resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/?site=2&size=5", http.NoBody))
	require.NoError(t, err)
	defer resp.Body.Close() //nolint:errcheck // close error not relevant in tests

	var body paginateResponse
	require.NoError(t, json.NewDecoder(resp.Body).Decode(&body))

	require.Equal(t, 2, body.Page)
	require.Equal(t, 5, body.Limit)
	require.Equal(t, 5, body.Start)
}

func Test_PaginateNegativeDefaultPageDefaultLimitValues(t *testing.T) {
	t.Parallel()
	app := fiber.New()
	app.Use(New(Config{
		DefaultPage:  -1,
		DefaultLimit: -1,
		DefaultSort:  "id",
	}))

	app.Get("/", func(c fiber.Ctx) error {
		pageInfo, ok := FromContext(c)
		if !ok {
			return fiber.ErrBadRequest
		}
		return c.JSON(paginateResponse{
			Page:  pageInfo.Page,
			Limit: pageInfo.Limit,
			Start: pageInfo.Start(),
		})
	})

	resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", http.NoBody))
	require.NoError(t, err)
	defer resp.Body.Close() //nolint:errcheck // close error not relevant in tests

	var body paginateResponse
	require.NoError(t, json.NewDecoder(resp.Body).Decode(&body))

	require.Equal(t, 1, body.Page)
	require.Equal(t, 10, body.Limit)
	require.Equal(t, 0, body.Start)
}

func Test_PaginateFromContextWithoutNew(t *testing.T) {
	t.Parallel()
	app := fiber.New()

	app.Get("/", func(c fiber.Ctx) error {
		_, ok := FromContext(c)
		if !ok {
			return fiber.ErrBadRequest
		}
		return c.JSON(nil)
	})

	resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", http.NoBody))
	require.NoError(t, err)
	defer resp.Body.Close() //nolint:errcheck // close error not relevant in tests
	require.Equal(t, fiber.StatusBadRequest, resp.StatusCode)
}

func Test_PaginateNextSkip(t *testing.T) {
	t.Parallel()
	app := fiber.New()
	app.Use(New(Config{
		Next: func(_ fiber.Ctx) bool {
			return true
		},
	}))

	app.Get("/", func(c fiber.Ctx) error {
		_, ok := FromContext(c)
		if !ok {
			return fiber.ErrBadRequest
		}
		return c.JSON(nil)
	})

	resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", http.NoBody))
	require.NoError(t, err)
	defer resp.Body.Close() //nolint:errcheck // close error not relevant in tests
	require.Equal(t, fiber.StatusBadRequest, resp.StatusCode)
}

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

	testCases := []struct {
		name          string
		url           string
		expectedPage  int
		expectedLimit int
	}{
		{"Negative page", "/?page=-1", 1, 10},
		{"Page zero", "/?page=0", 1, 10},
		{"Negative limit", "/?limit=-10", 1, 10},
		{"Limit zero", "/?limit=0", 1, 10},
		{"Limit exceeds max", "/?limit=200", 1, DefaultMaxLimit},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			app := fiber.New()
			app.Use(New(Config{
				DefaultSort:  "id",
				DefaultLimit: 10,
			}))

			app.Get("/", func(c fiber.Ctx) error {
				pageInfo, ok := FromContext(c)
				if !ok {
					return fiber.ErrBadRequest
				}
				return c.JSON(pageInfo)
			})

			resp, err := app.Test(httptest.NewRequest(http.MethodGet, tc.url, http.NoBody))
			require.NoError(t, err)
			defer resp.Body.Close() //nolint:errcheck // close error not relevant in tests
			require.Equal(t, 200, resp.StatusCode)

			var result PageInfo
			require.NoError(t, json.NewDecoder(resp.Body).Decode(&result))
			require.Equal(t, tc.expectedPage, result.Page)
			require.Equal(t, tc.expectedLimit, result.Limit)
		})
	}
}

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

	testCases := []struct {
		name         string
		url          string
		expectedSort []SortField
	}{
		{"Default Sort", "/", []SortField{{Field: "id", Order: ASC}}},
		{"Single Sort", "/?sort=name", []SortField{{Field: "name", Order: ASC}}},
		{"Multiple Sort", "/?sort=name,-date", []SortField{{Field: "name", Order: ASC}, {Field: "date", Order: DESC}}},
		{"Invalid Sort", "/?sort=invalid", []SortField{{Field: "id", Order: ASC}}},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			app := fiber.New()
			app.Use(New(Config{
				SortKey:      "sort",
				DefaultSort:  "id",
				AllowedSorts: []string{"id", "name", "date"},
			}))

			app.Get("/", func(c fiber.Ctx) error {
				pageInfo, ok := FromContext(c)
				if !ok {
					return fiber.ErrBadRequest
				}
				return c.JSON(paginateResponse{
					Sort: pageInfo.Sort,
				})
			})

			resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, tc.url, http.NoBody))
			require.NoError(t, err)
			defer resp.Body.Close() //nolint:errcheck // close error not relevant in tests

			var result paginateResponse
			require.NoError(t, json.NewDecoder(resp.Body).Decode(&result))
			require.Equal(t, tc.expectedSort, result.Sort)
		})
	}
}

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

	tests := []struct {
		name         string
		query        string
		allowedSorts []string
		defaultSort  string
		expected     []SortField
	}{
		{
			"Empty query",
			"",
			[]string{"id", "name", "date"},
			"id",
			[]SortField{{Field: "id", Order: ASC}},
		},
		{
			"Single allowed field",
			"name",
			[]string{"id", "name", "date"},
			"id",
			[]SortField{{Field: "name", Order: ASC}},
		},
		{
			"Multiple fields with mixed order",
			"name,-date,id",
			[]string{"id", "name", "date"},
			"id",
			[]SortField{
				{Field: "name", Order: ASC},
				{Field: "date", Order: DESC},
				{Field: "id", Order: ASC},
			},
		},
		{
			"Disallowed field",
			"email,name",
			[]string{"id", "name", "date"},
			"id",
			[]SortField{{Field: "name", Order: ASC}},
		},
		{
			"All disallowed fields",
			"email,phone",
			[]string{"id", "name", "date"},
			"id",
			[]SortField{{Field: "id", Order: ASC}},
		},
		{
			"Nil AllowedSorts falls back to default sort",
			"email,-phone",
			nil,
			"id",
			[]SortField{{Field: "id", Order: ASC}},
		},
		{
			"Empty AllowedSorts falls back to default sort",
			"email,-phone",
			[]string{},
			"id",
			[]SortField{{Field: "id", Order: ASC}},
		},
		{
			"Bare dash is skipped",
			"-",
			nil,
			"id",
			[]SortField{{Field: "id", Order: ASC}},
		},
		{
			"Dash in comma list is skipped",
			"name,-,email",
			nil,
			"id",
			[]SortField{{Field: "id", Order: ASC}},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			result := parseSortQuery(tt.query, tt.allowedSorts, tt.defaultSort)
			require.Equal(t, tt.expected, result)
		})
	}
}

// --- Cursor tests ---

func Test_PaginateWithCursor(t *testing.T) {
	t.Parallel()
	app := fiber.New()
	app.Use(New(Config{
		DefaultSort: "id",
	}))

	app.Get("/", func(c fiber.Ctx) error {
		pageInfo, ok := FromContext(c)
		if !ok {
			return fiber.ErrBadRequest
		}
		return c.JSON(cursorResponse{
			Cursor: pageInfo.Cursor,
			Limit:  pageInfo.Limit,
			Sort:   pageInfo.Sort,
		})
	})

	cursorJSON := `{"id":42}`
	cursor := base64.RawURLEncoding.EncodeToString([]byte(cursorJSON))

	resp, err := app.Test(httptest.NewRequest(http.MethodGet, "/?cursor="+cursor+"&limit=20", http.NoBody))
	require.NoError(t, err)
	defer resp.Body.Close() //nolint:errcheck // close error not relevant in tests
	require.Equal(t, 200, resp.StatusCode)

	var result cursorResponse
	require.NoError(t, json.NewDecoder(resp.Body).Decode(&result))
	require.Equal(t, cursor, result.Cursor)
	require.Equal(t, 20, result.Limit)
}

func Test_PaginateCursorPriorityOverPage(t *testing.T) {
	t.Parallel()
	app := fiber.New()
	app.Use(New())

	app.Get("/", func(c fiber.Ctx) error {
		pageInfo, ok := FromContext(c)
		if !ok {
			return fiber.ErrBadRequest
		}
		return c.JSON(pageInfo)
	})

	cursorJSON := `{"id":42}`
	cursor := base64.RawURLEncoding.EncodeToString([]byte(cursorJSON))

	resp, err := app.Test(httptest.NewRequest(http.MethodGet, "/?cursor="+cursor+"&page=5&limit=10", http.NoBody))
	require.NoError(t, err)
	defer resp.Body.Close() //nolint:errcheck // close error not relevant in tests

	var result PageInfo
	require.NoError(t, json.NewDecoder(resp.Body).Decode(&result))
	require.Equal(t, cursor, result.Cursor)
	require.Equal(t, 0, result.Page)
}

func Test_PaginateEmptyCursorIsFirstPage(t *testing.T) {
	t.Parallel()
	app := fiber.New()
	app.Use(New())

	app.Get("/", func(c fiber.Ctx) error {
		pageInfo, ok := FromContext(c)
		if !ok {
			return fiber.ErrBadRequest
		}
		return c.JSON(pageInfo)
	})

	resp, err := app.Test(httptest.NewRequest(http.MethodGet, "/?cursor=&limit=10", http.NoBody))
	require.NoError(t, err)
	defer resp.Body.Close() //nolint:errcheck // close error not relevant in tests
	require.Equal(t, 200, resp.StatusCode)

	var result PageInfo
	require.NoError(t, json.NewDecoder(resp.Body).Decode(&result))
	require.Empty(t, result.Cursor)
}

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

	testCases := []struct {
		name   string
		cursor string
	}{
		{"Invalid base64", "not-valid!!!"},
		{"Valid base64 but invalid JSON", base64.RawURLEncoding.EncodeToString([]byte("not-json"))},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			app := fiber.New()
			app.Use(New())

			app.Get("/", func(c fiber.Ctx) error {
				pageInfo, _ := FromContext(c)
				return c.JSON(pageInfo)
			})

			resp, err := app.Test(httptest.NewRequest(http.MethodGet, "/?cursor="+tc.cursor, http.NoBody))
			require.NoError(t, err)
			defer resp.Body.Close() //nolint:errcheck // close error not relevant in tests
			require.Equal(t, 400, resp.StatusCode)
		})
	}
}

func Test_PaginateCursorWithSort(t *testing.T) {
	t.Parallel()
	app := fiber.New()
	app.Use(New(Config{
		SortKey:      "sort",
		DefaultSort:  "id",
		AllowedSorts: []string{"id", "name"},
	}))

	app.Get("/", func(c fiber.Ctx) error {
		pageInfo, ok := FromContext(c)
		if !ok {
			return fiber.ErrBadRequest
		}
		return c.JSON(cursorResponse{
			Cursor: pageInfo.Cursor,
			Sort:   pageInfo.Sort,
		})
	})

	cursorJSON := `{"id":42}`
	cursor := base64.RawURLEncoding.EncodeToString([]byte(cursorJSON))

	resp, err := app.Test(httptest.NewRequest(http.MethodGet, "/?cursor="+cursor+"&sort=name,-id", http.NoBody))
	require.NoError(t, err)
	defer resp.Body.Close() //nolint:errcheck // close error not relevant in tests

	var result cursorResponse
	require.NoError(t, json.NewDecoder(resp.Body).Decode(&result))
	require.Equal(t, []SortField{{Field: "name", Order: ASC}, {Field: "id", Order: DESC}}, result.Sort)
}

func Test_PaginateCursorWithCustomKey(t *testing.T) {
	t.Parallel()
	app := fiber.New()
	app.Use(New(Config{
		CursorKey: "after",
	}))

	app.Get("/", func(c fiber.Ctx) error {
		pageInfo, ok := FromContext(c)
		if !ok {
			return fiber.ErrBadRequest
		}
		return c.JSON(cursorResponse{
			Cursor: pageInfo.Cursor,
			Limit:  pageInfo.Limit,
		})
	})

	cursorJSON := `{"id":1}`
	cursor := base64.RawURLEncoding.EncodeToString([]byte(cursorJSON))

	resp, err := app.Test(httptest.NewRequest(http.MethodGet, "/?after="+cursor, http.NoBody))
	require.NoError(t, err)
	defer resp.Body.Close() //nolint:errcheck // close error not relevant in tests
	require.Equal(t, 200, resp.StatusCode)

	var result cursorResponse
	require.NoError(t, json.NewDecoder(resp.Body).Decode(&result))
	require.Equal(t, cursor, result.Cursor)
}

func Test_PaginateCursorWithParamAlias(t *testing.T) {
	t.Parallel()
	app := fiber.New()
	app.Use(New(Config{
		CursorParam: "starting_after",
	}))

	app.Get("/", func(c fiber.Ctx) error {
		pageInfo, ok := FromContext(c)
		if !ok {
			return fiber.ErrBadRequest
		}
		return c.JSON(cursorResponse{
			Cursor: pageInfo.Cursor,
		})
	})

	cursorJSON := `{"id":1}`
	cursor := base64.RawURLEncoding.EncodeToString([]byte(cursorJSON))

	resp, err := app.Test(httptest.NewRequest(http.MethodGet, "/?starting_after="+cursor, http.NoBody))
	require.NoError(t, err)
	defer resp.Body.Close() //nolint:errcheck // close error not relevant in tests
	require.Equal(t, 200, resp.StatusCode)

	var result cursorResponse
	require.NoError(t, json.NewDecoder(resp.Body).Decode(&result))
	require.Equal(t, cursor, result.Cursor)
}

func Test_PaginateNoCursorFallsBackToPageMode(t *testing.T) {
	t.Parallel()
	app := fiber.New()
	app.Use(New(Config{
		DefaultSort: "id",
	}))

	app.Get("/", func(c fiber.Ctx) error {
		pageInfo, ok := FromContext(c)
		if !ok {
			return fiber.ErrBadRequest
		}
		return c.JSON(paginateResponse{
			Page:  pageInfo.Page,
			Limit: pageInfo.Limit,
			Start: pageInfo.Start(),
		})
	})

	resp, err := app.Test(httptest.NewRequest(http.MethodGet, "/?page=3&limit=15", http.NoBody))
	require.NoError(t, err)
	defer resp.Body.Close() //nolint:errcheck // close error not relevant in tests

	var result paginateResponse
	require.NoError(t, json.NewDecoder(resp.Body).Decode(&result))
	require.Equal(t, 3, result.Page)
	require.Equal(t, 15, result.Limit)
	require.Equal(t, 30, result.Start)
}

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

	p := PageInfo{Page: 2, Limit: 10}
	result := p.NextPageURLWithKeys("https://example.com/users", "p", "per_page")
	require.Equal(t, "https://example.com/users?p=3&per_page=10", result)
}

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

	t.Run("has previous", func(t *testing.T) {
		t.Parallel()
		p := PageInfo{Page: 3, Limit: 15}
		result := p.PreviousPageURLWithKeys("https://example.com/items", "p", "size")
		require.Equal(t, "https://example.com/items?p=2&size=15", result)
	})

	t.Run("first page returns empty", func(t *testing.T) {
		t.Parallel()
		p := PageInfo{Page: 1, Limit: 15}
		result := p.PreviousPageURLWithKeys("https://example.com/items", "p", "size")
		require.Empty(t, result)
	})
}

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

	t.Run("has more", func(t *testing.T) {
		t.Parallel()
		p := &PageInfo{Limit: 20}
		require.NoError(t, p.SetNextCursor(map[string]any{"id": float64(42)}))

		result := p.NextCursorURLWithKeys("https://example.com/users", "after", "per_page")
		expected := fmt.Sprintf("https://example.com/users?after=%s&per_page=20", p.NextCursor)
		require.Equal(t, expected, result)
	})

	t.Run("no more", func(t *testing.T) {
		t.Parallel()
		p := &PageInfo{Limit: 20}
		result := p.NextCursorURLWithKeys("https://example.com/users", "after", "per_page")
		require.Empty(t, result)
	})
}

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

	testCases := []struct {
		name          string
		url           string
		expectedLimit int
	}{
		{"Limit within custom max", "/?limit=40", 40},
		{"Limit exceeds custom max", "/?limit=200", 50},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			app := fiber.New()
			app.Use(New(Config{
				DefaultSort:  "id",
				DefaultLimit: 10,
				MaxLimit:     50,
			}))

			app.Get("/", func(c fiber.Ctx) error {
				pageInfo, ok := FromContext(c)
				if !ok {
					return fiber.ErrBadRequest
				}
				return c.JSON(pageInfo)
			})

			resp, err := app.Test(httptest.NewRequest(http.MethodGet, tc.url, http.NoBody))
			require.NoError(t, err)
			defer resp.Body.Close() //nolint:errcheck // close error not relevant in tests
			require.Equal(t, http.StatusOK, resp.StatusCode)

			var result PageInfo
			require.NoError(t, json.NewDecoder(resp.Body).Decode(&result))
			require.Equal(t, tc.expectedLimit, result.Limit)
		})
	}
}

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

	cfg := configDefault(Config{MaxLimit: 0})
	require.Equal(t, DefaultMaxLimit, cfg.MaxLimit)

	cfg2 := configDefault(Config{MaxLimit: 50})
	require.Equal(t, 50, cfg2.MaxLimit)
}

// --- Benchmarks ---

func Benchmark_PaginateMiddleware(b *testing.B) {
	app := fiber.New()
	app.Use(New())

	app.Get("/", func(c fiber.Ctx) error {
		pageInfo, _ := FromContext(c)
		return c.JSON(pageInfo)
	})

	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		req := httptest.NewRequest(http.MethodGet, "/?page=2&limit=20&sort=name,-date", http.NoBody)
		resp, err := app.Test(req, fiber.TestConfig{Timeout: 0})
		if err != nil {
			b.Fatal(err)
		}
		resp.Body.Close() //nolint:errcheck // close error not relevant in tests
	}
}

func Benchmark_PaginateMiddlewareWithCustomConfig(b *testing.B) {
	app := fiber.New()
	app.Use(New(Config{
		PageKey:      "p",
		LimitKey:     "l",
		SortKey:      "s",
		DefaultPage:  1,
		DefaultLimit: 30,
		DefaultSort:  "id",
		AllowedSorts: []string{"id", "name", "date"},
	}))

	app.Get("/", func(c fiber.Ctx) error {
		pageInfo, _ := FromContext(c)
		return c.JSON(pageInfo)
	})

	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		req := httptest.NewRequest(http.MethodGet, "/?p=3&l=25&s=name,-id", http.NoBody)
		resp, err := app.Test(req, fiber.TestConfig{Timeout: 0})
		if err != nil {
			b.Fatal(err)
		}
		resp.Body.Close() //nolint:errcheck // close error not relevant in tests
	}
}

func Benchmark_PaginateCursorMiddleware(b *testing.B) {
	app := fiber.New()
	app.Use(New(Config{
		SortKey:      "sort",
		DefaultSort:  "id",
		AllowedSorts: []string{"id", "name", "date"},
	}))

	app.Get("/", func(c fiber.Ctx) error {
		pageInfo, _ := FromContext(c)
		return c.JSON(pageInfo)
	})

	cursorJSON := `{"id":42,"created_at":"2026-01-01T00:00:00Z"}`
	cursor := base64.RawURLEncoding.EncodeToString([]byte(cursorJSON))

	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		req := httptest.NewRequest(http.MethodGet, "/?cursor="+cursor+"&limit=20&sort=name,-id", http.NoBody)
		resp, err := app.Test(req, fiber.TestConfig{Timeout: 0})
		if err != nil {
			b.Fatal(err)
		}
		resp.Body.Close() //nolint:errcheck // close error not relevant in tests
	}
}
