package fiber

import (
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"os"
	"reflect"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/valyala/fasthttp"
)

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

	var handler Handler
	converted, ok := toFiberHandler(handler)
	require.False(t, ok)
	require.Nil(t, converted)
}

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

	fiberHandler := func(c Ctx) error { return c.SendStatus(http.StatusAccepted) }

	converted, ok := toFiberHandler(fiberHandler)
	require.True(t, ok)
	require.NotNil(t, converted)
	require.Equal(t, reflect.ValueOf(fiberHandler).Pointer(), reflect.ValueOf(converted).Pointer())
}

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

	app, ctx := newTestCtx(t)

	handler := func(c Ctx) {
		require.Equal(t, app, c.App())
		c.Set("X-Handler", "ok")
	}

	converted, ok := toFiberHandler(handler)
	require.True(t, ok)
	require.NotNil(t, converted)

	require.NoError(t, converted(ctx))
	require.Equal(t, "ok", string(ctx.Response().Header.Peek("X-Handler")))
}

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

	app, ctx := newTestCtx(t)
	require.NotNil(t, app)
	require.NotNil(t, ctx)
	require.Equal(t, app, ctx.App())
}

func newTestCtx(t *testing.T) (*App, *DefaultCtx) {
	t.Helper()

	app := New()
	fasthttpCtx := &fasthttp.RequestCtx{}
	customCtx := app.AcquireCtx(fasthttpCtx)
	ctx, ok := customCtx.(*DefaultCtx)
	require.True(t, ok)

	t.Cleanup(func() {
		app.ReleaseCtx(customCtx)
	})

	return app, ctx
}

func withRouteHandlers(t *testing.T, ctx *DefaultCtx, handlers ...Handler) {
	t.Helper()

	ctx.route = &Route{Handlers: handlers}
	ctx.indexHandler = 0
	t.Cleanup(func() {
		ctx.route = nil
		ctx.indexHandler = 0
	})
}

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

	app, ctx := newTestCtx(t)

	handler := func(req Req, res Res) error {
		assert.Equal(t, app, req.App())
		assert.Equal(t, app, res.App())
		return res.SendString("express")
	}

	converted, ok := toFiberHandler(handler)
	require.True(t, ok)

	require.NoError(t, converted(ctx))
	require.Equal(t, "express", string(ctx.Response().Body()))
}

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

	app, ctx := newTestCtx(t)

	handler := func(req Req, res Res) {
		assert.Equal(t, app, req.App())
		require.NoError(t, res.SendStatus(http.StatusCreated))
	}

	converted, ok := toFiberHandler(handler)
	require.True(t, ok)

	require.NoError(t, converted(ctx))
	require.Equal(t, http.StatusCreated, ctx.Response().StatusCode())
}

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

	app, ctx := newTestCtx(t)

	handler := func(req Req, res Res, next func() error) error {
		assert.Equal(t, app, req.App())
		assert.Equal(t, app, res.App())
		return next()
	}

	converted, ok := toFiberHandler(handler)
	require.True(t, ok)

	nextErr := errors.New("next")
	nextCalled := false
	nextHandler := func(_ Ctx) error {
		nextCalled = true
		return nextErr
	}

	withRouteHandlers(t, ctx, converted, nextHandler)

	err := converted(ctx)
	require.ErrorIs(t, err, nextErr)
	require.True(t, nextCalled)
}

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

	app, ctx := newTestCtx(t)

	handler := func(req Req, _ Res, next func() error) {
		assert.Equal(t, app, req.App())
		err := next()
		require.Error(t, err)
		assert.EqualError(t, err, "next without error")
	}

	converted, ok := toFiberHandler(handler)
	require.True(t, ok)

	nextHandler := func(_ Ctx) error {
		return errors.New("next without error")
	}

	withRouteHandlers(t, ctx, converted, nextHandler)

	err := converted(ctx)
	require.EqualError(t, err, "next without error")
}

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

	app, ctx := newTestCtx(t)

	handler := func(req Req, res Res, next func()) error {
		assert.Equal(t, app, req.App())
		assert.Equal(t, app, res.App())
		next()
		return nil
	}

	converted, ok := toFiberHandler(handler)
	require.True(t, ok)

	nextErr := errors.New("next without return value")
	nextCalled := false
	nextHandler := func(_ Ctx) error {
		nextCalled = true
		return nextErr
	}

	withRouteHandlers(t, ctx, converted, nextHandler)

	err := converted(ctx)
	require.ErrorIs(t, err, nextErr)
	require.True(t, nextCalled)
}

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

	app, ctx := newTestCtx(t)

	handler := func(req Req, res Res, next func(error)) {
		assert.Equal(t, app, req.App())
		assert.Equal(t, app, res.App())
		next(nil)
	}

	converted, ok := toFiberHandler(handler)
	require.True(t, ok)

	nextCalled := false
	nextHandler := func(_ Ctx) error {
		nextCalled = true
		return nil
	}

	withRouteHandlers(t, ctx, converted, nextHandler)

	err := converted(ctx)
	require.NoError(t, err)
	require.True(t, nextCalled)
}

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

	app, ctx := newTestCtx(t)

	handler := func(req Req, res Res, next func(error)) {
		assert.Equal(t, app, req.App())
		assert.Equal(t, app, res.App())
		next(errors.New("next error"))
	}

	converted, ok := toFiberHandler(handler)
	require.True(t, ok)

	nextCalled := false
	nextHandler := func(_ Ctx) error {
		nextCalled = true
		return nil
	}

	withRouteHandlers(t, ctx, converted, nextHandler)

	err := converted(ctx)
	require.EqualError(t, err, "next error")
	require.False(t, nextCalled)
}

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

	app, ctx := newTestCtx(t)

	handler := func(req Req, res Res, next func(error)) error {
		assert.Equal(t, app, req.App())
		assert.Equal(t, app, res.App())
		next(errors.New("next error"))
		return nil
	}

	converted, ok := toFiberHandler(handler)
	require.True(t, ok)

	nextCalled := false
	nextHandler := func(_ Ctx) error {
		nextCalled = true
		return nil
	}

	withRouteHandlers(t, ctx, converted, nextHandler)

	err := converted(ctx)
	require.EqualError(t, err, "next error")
	require.False(t, nextCalled)
}

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

	app, ctx := newTestCtx(t)

	handler := func(req Req, res Res, next func(error) error) {
		assert.Equal(t, app, req.App())
		assert.Equal(t, app, res.App())
		require.EqualError(t, next(nil), "next error")
	}

	converted, ok := toFiberHandler(handler)
	require.True(t, ok)

	nextErr := errors.New("next error")
	nextCalled := false
	nextHandler := func(_ Ctx) error {
		nextCalled = true
		return nextErr
	}

	withRouteHandlers(t, ctx, converted, nextHandler)

	err := converted(ctx)
	require.ErrorIs(t, err, nextErr)
	require.True(t, nextCalled)
}

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

	app, ctx := newTestCtx(t)

	handler := func(req Req, res Res, next func(error) error) {
		assert.Equal(t, app, req.App())
		assert.Equal(t, app, res.App())
		require.EqualError(t, next(errors.New("next error")), "next error")
	}

	converted, ok := toFiberHandler(handler)
	require.True(t, ok)

	nextCalled := false
	nextHandler := func(_ Ctx) error {
		nextCalled = true
		return nil
	}

	withRouteHandlers(t, ctx, converted, nextHandler)

	err := converted(ctx)
	require.EqualError(t, err, "next error")
	require.False(t, nextCalled)
}

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

	app, ctx := newTestCtx(t)

	handler := func(req Req, res Res, next func(error) error) error {
		assert.Equal(t, app, req.App())
		assert.Equal(t, app, res.App())
		require.EqualError(t, next(errors.New("next error")), "next error")
		return errors.New("handler error")
	}

	converted, ok := toFiberHandler(handler)
	require.True(t, ok)

	nextCalled := false
	nextHandler := func(_ Ctx) error {
		nextCalled = true
		return nil
	}

	withRouteHandlers(t, ctx, converted, nextHandler)

	err := converted(ctx)
	require.EqualError(t, err, "handler error")
	require.False(t, nextCalled)
}

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

	app, ctx := newTestCtx(t)

	handler := func(req Req, res Res, next func(error) error) error {
		assert.Equal(t, app, req.App())
		assert.Equal(t, app, res.App())
		return next(errors.New("next error"))
	}

	converted, ok := toFiberHandler(handler)
	require.True(t, ok)

	nextCalled := false
	nextHandler := func(_ Ctx) error {
		nextCalled = true
		return nil
	}

	withRouteHandlers(t, ctx, converted, nextHandler)

	err := converted(ctx)
	require.EqualError(t, err, "next error")
	require.False(t, nextCalled)
}

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

	app, ctx := newTestCtx(t)

	handler := func(req Req, res Res, _ func(error) error) {
		assert.Equal(t, app, req.App())
		assert.Equal(t, app, res.App())
		// Intentionally do not call next.
	}

	converted, ok := toFiberHandler(handler)
	require.True(t, ok)

	nextCalled := false
	nextHandler := func(_ Ctx) error {
		nextCalled = true
		return errors.New("should not be called")
	}

	withRouteHandlers(t, ctx, converted, nextHandler)

	err := converted(ctx)
	require.NoError(t, err)
	require.False(t, nextCalled)
}

func TestAdapter_MixedHandlerIntegration(t *testing.T) {
	app := New()

	app.Use(func(c Ctx) error {
		c.Set("X-Middleware", "fiber")
		return c.Next()
	})

	app.Use(func(_ Req, res Res, next func() error) error {
		res.Set("X-Express", "middleware")
		return next()
	})

	app.Get("/fiber", func(c Ctx) error {
		c.Set("X-Route", "fiber")
		return c.SendString("fiber handler")
	})

	app.Post("/express", func(_ Req, res Res) error {
		res.Set("X-Route", "express")
		return res.SendString("express handler")
	})

	var httpHandlerWriteErr error
	app.Put("/http", func(w http.ResponseWriter, _ *http.Request) {
		w.Header().Set("X-Route", "http")
		w.WriteHeader(http.StatusAccepted)
		_, httpHandlerWriteErr = w.Write([]byte("http handler"))
	})

	app.Delete("/fasthttp", func(ctx *fasthttp.RequestCtx) error {
		ctx.Response.Header.Set("X-Route", "fasthttp")
		ctx.SetStatusCode(http.StatusCreated)
		ctx.SetBodyString("fasthttp handler")
		return nil
	})

	run := func(name string, buildRequest func() *http.Request, expectStatus int, expectBody, expectRoute string) {
		t.Run(name, func(t *testing.T) {
			req := buildRequest()

			resp, err := app.Test(req)
			require.NoError(t, err)
			t.Cleanup(func() {
				require.NoError(t, resp.Body.Close())
			})

			body, err := io.ReadAll(resp.Body)
			require.NoError(t, err)

			require.Equal(t, expectStatus, resp.StatusCode)
			require.Equal(t, expectBody, string(body))
			require.Equal(t, "fiber", resp.Header.Get("X-Middleware"))
			require.Equal(t, "middleware", resp.Header.Get("X-Express"))
			require.Equal(t, expectRoute, resp.Header.Get("X-Route"))
		})
	}

	run("fiber", func() *http.Request {
		return httptest.NewRequest(http.MethodGet, "/fiber", http.NoBody)
	}, http.StatusOK, "fiber handler", "fiber")

	run("express", func() *http.Request {
		return httptest.NewRequest(http.MethodPost, "/express", http.NoBody)
	}, http.StatusOK, "express handler", "express")

	run("net/http", func() *http.Request {
		return httptest.NewRequest(http.MethodPut, "/http", http.NoBody)
	}, http.StatusAccepted, "http handler", "http")

	require.NoError(t, httpHandlerWriteErr)

	run("fasthttp", func() *http.Request {
		return httptest.NewRequest(http.MethodDelete, "/fasthttp", http.NoBody)
	}, http.StatusCreated, "fasthttp handler", "fasthttp")
}

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

	app, ctx := newTestCtx(t)

	handler := func(req Req, res Res, next func()) {
		assert.Equal(t, app, req.App())
		assert.Equal(t, app, res.App())
		next()
	}

	converted, ok := toFiberHandler(handler)
	require.True(t, ok)

	nextErr := errors.New("next without return value")
	nextCalled := false
	nextHandler := func(_ Ctx) error {
		nextCalled = true
		return nextErr
	}

	withRouteHandlers(t, ctx, converted, nextHandler)

	err := converted(ctx)
	require.ErrorIs(t, err, nextErr)
	require.True(t, nextCalled)
}

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

	app, ctx := newTestCtx(t)

	handler := func(req Req, res Res, _ func()) {
		assert.Equal(t, app, req.App())
		assert.Equal(t, app, res.App())
		// Intentionally do not call next().
	}

	converted, ok := toFiberHandler(handler)
	require.True(t, ok)

	nextCalled := false
	nextHandler := func(_ Ctx) error {
		nextCalled = true
		return errors.New("should not be called")
	}

	withRouteHandlers(t, ctx, converted, nextHandler)

	err := converted(ctx)
	require.NoError(t, err)
	require.False(t, nextCalled)
}

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

	app := New()
	t.Cleanup(func() {
		require.NoError(t, app.Shutdown())
	})

	callOrder := make([]string, 0, 2)

	app.Use(func(req Req, res Res, next func()) {
		callOrder = append(callOrder, "middleware")
		next()
		assert.Equal(t, app, req.App())
		assert.Equal(t, app, res.App())
	})

	app.Get("/", func(c Ctx) error {
		callOrder = append(callOrder, "handler")
		return c.SendStatus(http.StatusOK)
	})

	req := httptest.NewRequest(http.MethodGet, "/", http.NoBody)
	resp, err := app.Test(req)
	require.NoError(t, err)
	require.Equal(t, http.StatusOK, resp.StatusCode)
	require.NoError(t, resp.Body.Close())

	require.Equal(t, []string{"middleware", "handler"}, callOrder)
}

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

	var writeErr error
	httpHandler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.Header().Set("X-HTTP", "ok")
		w.WriteHeader(http.StatusTeapot)
		_, writeErr = w.Write([]byte("http"))
	})

	handlers := collectHandlers("test", httpHandler)
	require.Len(t, handlers, 1)
	converted := handlers[0]
	require.NotNil(t, converted)

	app := New()
	ctx := app.AcquireCtx(&fasthttp.RequestCtx{})
	t.Cleanup(func() {
		app.ReleaseCtx(ctx)
	})

	err := converted(ctx)
	require.NoError(t, err)
	require.Equal(t, http.StatusTeapot, ctx.Response().StatusCode())
	require.Equal(t, "ok", string(ctx.Response().Header.Peek("X-HTTP")))
	require.Equal(t, "http", string(ctx.Response().Body()))
	require.NoError(t, writeErr)
}

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

	var writeErr error
	var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.Header().Set("X-HTTP", "handler")
		_, writeErr = w.Write([]byte("through"))
	})

	converted, ok := toFiberHandler(handler)
	require.True(t, ok)
	require.NotNil(t, converted)

	app := New()
	ctx := app.AcquireCtx(&fasthttp.RequestCtx{})
	t.Cleanup(func() {
		app.ReleaseCtx(ctx)
	})

	err := converted(ctx)
	require.NoError(t, err)
	require.Equal(t, "handler", string(ctx.Response().Header.Peek("X-HTTP")))
	require.Equal(t, "through", string(ctx.Response().Body()))
	require.NoError(t, writeErr)
}

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

	_, ctx := newTestCtx(t)

	fasthttpHandler := func(fctx *fasthttp.RequestCtx) error {
		fctx.Response.Header.Set("X-FASTHTTP", "error")
		return errors.New("fasthttp error")
	}

	converted, ok := toFiberHandler(fasthttpHandler)
	require.True(t, ok)
	require.NotNil(t, converted)

	err := converted(ctx)
	require.EqualError(t, err, "fasthttp error")
	require.Equal(t, "error", string(ctx.Response().Header.Peek("X-FASTHTTP")))
}

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

	var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.Header().Set("X-HTTP", "handler")
		_, err := w.Write([]byte("through"))
		flusher, ok := w.(http.Flusher)
		assert.True(t, ok, "w does not implement http.Flusher")
		flusher.Flush()
		assert.NoError(t, err)
	})

	converted, ok := toFiberHandler(handler)
	require.True(t, ok)
	require.NotNil(t, converted)

	app := New()
	ctx := app.AcquireCtx(&fasthttp.RequestCtx{})
	t.Cleanup(func() {
		app.ReleaseCtx(ctx)
	})

	err := converted(ctx)
	require.NoError(t, err)
	require.Equal(t, "handler", string(ctx.Response().Header.Peek("X-HTTP")))
	require.Equal(t, "through", string(ctx.Response().Body()))
}

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

	app := New()

	app.Get("/", func(w http.ResponseWriter, _ *http.Request) {
		flusher, ok := w.(http.Flusher)
		if !ok {
			t.Fatal("w does not implement http.Flusher")
		}
		w.WriteHeader(StatusOK)
		fmt.Fprintf(w, "Hello ")
		flusher.Flush()
		fmt.Fprintf(w, "World!")
	})

	resp, err := app.Test(httptest.NewRequest(MethodGet, "/", http.NoBody))
	require.NoError(t, err)
	defer resp.Body.Close() //nolint:errcheck // not needed

	require.Equal(t, StatusOK, resp.StatusCode)

	body, err := io.ReadAll(resp.Body)
	require.NoError(t, err)
	require.Equal(t, "Hello World!", string(body))
}

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

	app := New()

	app.Get("/", func(w http.ResponseWriter, _ *http.Request) {
		flusher, ok := w.(http.Flusher)
		if !ok {
			t.Fatal("w does not implement http.Flusher")
		}
		w.WriteHeader(StatusOK)
		fmt.Fprintf(w, "Hello ")
		flusher.Flush()
		time.Sleep(500 * time.Millisecond)
		fmt.Fprintf(w, "World!")
	})

	resp, err := app.Test(httptest.NewRequest(MethodGet, "/", http.NoBody), TestConfig{
		Timeout:       200 * time.Millisecond,
		FailOnTimeout: true, // Changed to true to test interrupted behavior
	})
	// With FailOnTimeout: true, we should get a timeout error
	require.ErrorIs(t, err, os.ErrDeadlineExceeded)
	require.Nil(t, resp)
}

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

	httpFunc := func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusNoContent)
	}

	converted, ok := toFiberHandler(httpFunc)
	require.True(t, ok)
	require.NotNil(t, converted)

	app := New()
	ctx := app.AcquireCtx(&fasthttp.RequestCtx{})
	t.Cleanup(func() {
		app.ReleaseCtx(ctx)
	})

	err := converted(ctx)
	require.NoError(t, err)
	require.Equal(t, http.StatusNoContent, ctx.Response().StatusCode())
}

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

	require.Nil(t, wrapHTTPHandler(nil))
}

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

	require.PanicsWithValue(t, "context: invalid handler #0 (int)\n", func() {
		collectHandlers("context", 42)
	})
}

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

	var handlerFunc http.HandlerFunc
	var handler http.Handler
	var raw func(http.ResponseWriter, *http.Request)

	tests := []struct {
		handler any
		name    string
	}{
		{
			name:    "HandlerFunc",
			handler: handlerFunc,
		},
		{
			name:    "Handler",
			handler: handler,
		},
		{
			name:    "Function",
			handler: raw,
		},
	}

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

			expected := fmt.Sprintf("context: invalid handler #0 (%T)\n", tt.handler)

			require.PanicsWithValue(t, expected, func() {
				collectHandlers("context", tt.handler)
			})
		})
	}
}

type dummyHandler struct{}

func (dummyHandler) ServeHTTP(http.ResponseWriter, *http.Request) {}

type dummyFuncHandler func(http.ResponseWriter, *http.Request)

func (handler dummyFuncHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if handler == nil {
		return
	}
	handler(w, r)
}

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

	var handler http.Handler = (*dummyHandler)(nil)

	require.PanicsWithValue(t, "context: invalid handler #0 (*fiber.dummyHandler)\n", func() {
		collectHandlers("context", handler)
	})
}

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

	var handler http.Handler = dummyFuncHandler(nil)
	expected := fmt.Sprintf("context: invalid handler #0 (%T)\n", handler)

	require.PanicsWithValue(t, expected, func() {
		collectHandlers("context", handler)
	})
}

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

	var requestHandler fasthttp.RequestHandler
	var requestHandlerWithError func(*fasthttp.RequestCtx) error

	tests := []struct {
		handler any
		name    string
	}{
		{
			name:    "RequestHandler",
			handler: requestHandler,
		},
		{
			name:    "RequestHandlerWithError",
			handler: requestHandlerWithError,
		},
	}

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

			expected := fmt.Sprintf("context: invalid handler #0 (%T)\n", tt.handler)

			require.PanicsWithValue(t, expected, func() {
				collectHandlers("context", tt.handler)
			})
		})
	}
}

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

	before := func(c Ctx) error {
		c.Set("X-Before", "fiber")
		return nil
	}

	fasthttpHandler := fasthttp.RequestHandler(func(ctx *fasthttp.RequestCtx) {
		ctx.Response.Header.Set("X-FASTHTTP", "ok")
		ctx.SetBody([]byte("done"))
	})

	handlers := collectHandlers("fasthttp", before, fasthttpHandler)
	require.Len(t, handlers, 2)

	app := New()
	ctx := app.AcquireCtx(&fasthttp.RequestCtx{})
	t.Cleanup(func() {
		app.ReleaseCtx(ctx)
	})

	for _, handler := range handlers {
		require.NoError(t, handler(ctx))
	}

	require.Equal(t, "fiber", string(ctx.Response().Header.Peek("X-Before")))
	require.Equal(t, "ok", string(ctx.Response().Header.Peek("X-FASTHTTP")))
	require.Equal(t, "done", string(ctx.Response().Body()))
}

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

	noError := func(c Ctx) {
		c.Set("X-Handler", "fiber")
	}

	handlers := collectHandlers("ctx", noError)
	require.Len(t, handlers, 1)

	app := New()
	ctx := app.AcquireCtx(&fasthttp.RequestCtx{})
	t.Cleanup(func() {
		app.ReleaseCtx(ctx)
	})

	require.NoError(t, handlers[0](ctx))
	require.Equal(t, "fiber", string(ctx.Response().Header.Peek("X-Handler")))
}

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

	before := func(c Ctx) error {
		c.Set("X-Before", "fiber")
		return nil
	}
	var writeErr error
	httpHandler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		_, writeErr = w.Write([]byte("done"))
	})

	handlers := collectHandlers("test", before, httpHandler)
	require.Len(t, handlers, 2)
	require.Equal(t, reflect.ValueOf(before).Pointer(), reflect.ValueOf(handlers[0]).Pointer())
	require.NotNil(t, handlers[1])

	app := New()
	ctx := app.AcquireCtx(&fasthttp.RequestCtx{})
	t.Cleanup(func() {
		app.ReleaseCtx(ctx)
	})

	err := handlers[0](ctx)
	require.NoError(t, err)

	err = handlers[1](ctx)
	require.NoError(t, err)
	require.Equal(t, "done", string(ctx.Response().Body()))
	require.Equal(t, "fiber", string(ctx.Response().Header.Peek("X-Before")))
	require.NoError(t, writeErr)
}

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

	require.PanicsWithValue(t, "nil: invalid handler #0 (<nil>)\n", func() {
		collectHandlers("nil", nil)
	})
}
