package fiber_test

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

	"github.com/stretchr/testify/require"

	"github.com/gofiber/fiber/v3"
	"github.com/gofiber/fiber/v3/middleware/basicauth"
	"github.com/gofiber/fiber/v3/middleware/cache"
	"github.com/gofiber/fiber/v3/middleware/compress"
	"github.com/gofiber/fiber/v3/middleware/cors"
	"github.com/gofiber/fiber/v3/middleware/csrf"
	"github.com/gofiber/fiber/v3/middleware/encryptcookie"
	"github.com/gofiber/fiber/v3/middleware/envvar"
	"github.com/gofiber/fiber/v3/middleware/helmet"
	"github.com/gofiber/fiber/v3/middleware/keyauth"
	"github.com/gofiber/fiber/v3/middleware/limiter"
	"github.com/gofiber/fiber/v3/middleware/recover"
	"github.com/gofiber/fiber/v3/middleware/requestid"
	"github.com/gofiber/fiber/v3/middleware/session"
	"github.com/valyala/fasthttp"
	"github.com/valyala/fasthttp/fasthttputil"
)

type integrationCustomCtx struct {
	*fiber.DefaultCtx
}

func newIntegrationCustomCtx(app *fiber.App) fiber.CustomCtx {
	return &integrationCustomCtx{DefaultCtx: fiber.NewDefaultCtx(app)}
}

func performOversizedRequest(t *testing.T, app *fiber.App, configure func(req *fasthttp.Request)) *fasthttp.Response {
	t.Helper()

	ln := fasthttputil.NewInmemoryListener()
	errCh := make(chan error, 1)

	go func() {
		errCh <- app.Listener(ln, fiber.ListenConfig{DisableStartupMessage: true})
	}()

	t.Cleanup(func() {
		require.NoError(t, app.Shutdown())
		if err := <-errCh; err != nil && !errors.Is(err, net.ErrClosed) {
			require.NoError(t, err)
		}
	})

	require.Eventually(t, func() bool {
		conn, err := ln.Dial()
		if err != nil {
			return false
		}
		if err := conn.Close(); err != nil {
			return false
		}
		return true
	}, time.Second, 10*time.Millisecond)

	req := fasthttp.AcquireRequest()
	resp := fasthttp.AcquireResponse()

	req.SetRequestURI("http://example.com/")
	req.Header.SetMethod(fiber.MethodPost)
	req.Header.Set(fiber.HeaderOrigin, "https://example.com")
	req.SetBody(bytes.Repeat([]byte{'a'}, 32))
	if configure != nil {
		configure(req)
	}

	client := fasthttp.Client{
		Dial: func(string) (net.Conn, error) {
			return ln.Dial()
		},
	}

	require.NoError(t, client.Do(req, resp))

	respCopy := fasthttp.AcquireResponse()
	resp.CopyTo(respCopy)

	fasthttp.ReleaseRequest(req)
	fasthttp.ReleaseResponse(resp)

	t.Cleanup(func() {
		fasthttp.ReleaseResponse(respCopy)
	})

	return respCopy
}

var integrationEncryptCookieKey = encryptcookie.GenerateKey(32)

// middlewareCombinationTestCase describes a middleware stack that should keep its
// headers intact even when the default error handler runs. Keeping it as a named
// type (instead of an inline struct) makes the massive table below easier to
// scan and extend.
//
//nolint:govet // field alignment is secondary to readability for this test table
type middlewareCombinationTestCase struct { // betteralign:ignore - readability takes priority in tests
	name             string
	setup            func(app *fiber.App)
	configureRequest func(req *fasthttp.Request)
	handler          func(c fiber.Ctx) error
	assertions       func(t *testing.T, resp *fasthttp.Response)
	expectedStatus   int
}

func (tc middlewareCombinationTestCase) statusOrDefault() int {
	if tc.expectedStatus == 0 {
		return fiber.StatusInternalServerError
	}
	return tc.expectedStatus
}

func (tc middlewareCombinationTestCase) handlerOrDefault() func(fiber.Ctx) error {
	if tc.handler != nil {
		return tc.handler
	}

	return func(fiber.Ctx) error {
		return fiber.NewError(fiber.StatusInternalServerError, "middleware combination failure")
	}
}

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

	t.Run("disabled by default", func(t *testing.T) {
		t.Parallel()

		app := fiber.New()
		app.Use(requestid.New(requestid.Config{Generator: func() string { return "rid-disabled" }}))

		app.Get("/", func(c fiber.Ctx) error {
			require.Equal(t, "rid-disabled", requestid.FromContext(c))
			require.Empty(t, requestid.FromContext(c.Context()))
			return c.SendStatus(fiber.StatusOK)
		})

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

	t.Run("enabled", func(t *testing.T) {
		t.Parallel()

		app := fiber.New(fiber.Config{PassLocalsToContext: true})
		app.Use(requestid.New(requestid.Config{Generator: func() string { return "rid-enabled" }}))

		app.Get("/", func(c fiber.Ctx) error {
			require.Equal(t, "rid-enabled", requestid.FromContext(c))
			require.Equal(t, "rid-enabled", requestid.FromContext(c.Context()))
			return c.SendStatus(fiber.StatusOK)
		})

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

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

	// This integration suite exercises representative middleware stacks to ensure their
	// response headers survive after Fiber's default error handler emits a failure.

	const (
		// Origins used by the CORS stacks in this suite.
		corsHelmetOrigin    = "https://cors-and-helmet.example"
		corsRequestIDOrigin = "https://cors-and-requestid.example"
		corsCSRForigin      = "https://cors-and-csrf.example"
		corsCacheOrigin     = "https://cors-and-cache.example"
		corsSessionOrigin   = "https://cors-and-session.example"
		corsHelmetRequestID = "https://cors-helmet-requestid.example"

		csrfCookieName      = "combo-csrf"
		generatedRequestID  = "generated-combo-request-id"
		helmetLimiterMax    = 7
		helmetLimiterReset  = 60
		requestIDHeader     = "combo-request-id"
		csrfTokenValue      = "csrf-token"
		encryptedCookieName = "combo-encrypted"
		encryptedCookieVal  = "unencrypted"
		envvarAllowHeader   = fiber.MethodGet + ", " + fiber.MethodHead
		basicRealm          = "combo-basic"
		keyAuthRealm        = "combo-key"
		keyAuthErrorDesc    = "missing-key"
	)

	// Each entry wires up a different middleware stack so we can ensure response mutations
	// survive the hop through the default error handler.
	testCases := []middlewareCombinationTestCase{
		// --- CORS-focused stacks keep cross-origin metadata on error responses.
		{
			name: "cors+helmet",
			setup: func(app *fiber.App) {
				app.Use(cors.New(cors.Config{AllowOrigins: []string{corsHelmetOrigin}}))
				app.Use(helmet.New())
			},
			configureRequest: func(req *fasthttp.Request) {
				req.Header.Set(fiber.HeaderOrigin, corsHelmetOrigin)
			},
			assertions: func(t *testing.T, resp *fasthttp.Response) {
				t.Helper()
				require.Equal(t, corsHelmetOrigin, string(resp.Header.Peek(fiber.HeaderAccessControlAllowOrigin)))
				require.Equal(t, "nosniff", string(resp.Header.Peek(fiber.HeaderXContentTypeOptions)))
				require.Equal(t, "same-origin", string(resp.Header.Peek("Cross-Origin-Opener-Policy")))
				require.Equal(t, "same-origin", string(resp.Header.Peek("Cross-Origin-Resource-Policy")))
				require.Equal(t, "require-corp", string(resp.Header.Peek("Cross-Origin-Embedder-Policy")))
			},
		},
		{
			name: "cors+requestid",
			setup: func(app *fiber.App) {
				app.Use(cors.New(cors.Config{AllowOrigins: []string{corsRequestIDOrigin}}))
				app.Use(requestid.New())
			},
			configureRequest: func(req *fasthttp.Request) {
				req.Header.Set(fiber.HeaderOrigin, corsRequestIDOrigin)
				req.Header.Set("X-Request-ID", requestIDHeader)
			},
			assertions: func(t *testing.T, resp *fasthttp.Response) {
				t.Helper()
				require.Equal(t, corsRequestIDOrigin, string(resp.Header.Peek(fiber.HeaderAccessControlAllowOrigin)))
				require.Equal(t, requestIDHeader, string(resp.Header.Peek("X-Request-ID")))
			},
		},
		{
			name: "cors+helmet+requestid",
			setup: func(app *fiber.App) {
				app.Use(cors.New(cors.Config{AllowOrigins: []string{corsHelmetRequestID}}))
				app.Use(helmet.New())
				app.Use(requestid.New(requestid.Config{
					Generator: func() string {
						return generatedRequestID
					},
				}))
			},
			configureRequest: func(req *fasthttp.Request) {
				req.Header.Set(fiber.HeaderOrigin, corsHelmetRequestID)
			},
			assertions: func(t *testing.T, resp *fasthttp.Response) {
				t.Helper()
				require.Equal(t, corsHelmetRequestID, string(resp.Header.Peek(fiber.HeaderAccessControlAllowOrigin)))
				require.Equal(t, generatedRequestID, string(resp.Header.Peek("X-Request-ID")))
				require.Equal(t, "nosniff", string(resp.Header.Peek(fiber.HeaderXContentTypeOptions)))
			},
		},
		{
			name: "cors+cache",
			setup: func(app *fiber.App) {
				app.Use(cors.New(cors.Config{AllowOrigins: []string{corsCacheOrigin}}))
				app.Use(cache.New())
				// Cache needs the default error handler to execute so it can emit X-Cache on failures.
				app.Use(func(c fiber.Ctx) error {
					if err := c.Next(); err != nil {
						if handlerErr := app.Config().ErrorHandler(c, err); handlerErr != nil {
							return handlerErr
						}
						c.Set(fiber.HeaderCacheControl, "no-store")
						return nil
					}
					c.Set(fiber.HeaderCacheControl, "no-store")
					return nil
				})
			},
			configureRequest: func(req *fasthttp.Request) {
				req.Header.Set(fiber.HeaderOrigin, corsCacheOrigin)
				req.Header.SetMethod(fiber.MethodGet)
			},
			assertions: func(t *testing.T, resp *fasthttp.Response) {
				t.Helper()
				require.Equal(t, corsCacheOrigin, string(resp.Header.Peek(fiber.HeaderAccessControlAllowOrigin)))
				require.Equal(t, "unreachable", string(resp.Header.Peek("X-Cache")))
				require.Equal(t, "no-store", string(resp.Header.Peek(fiber.HeaderCacheControl)))
			},
		},
		{
			name: "cors+session",
			setup: func(app *fiber.App) {
				app.Use(cors.New(cors.Config{
					AllowOrigins:     []string{corsSessionOrigin},
					AllowCredentials: true,
				}))
				app.Use(session.New())
				app.Use(func(c fiber.Ctx) error {
					if sm := session.FromContext(c); sm != nil {
						sm.Set("cors-session", "enabled")
					}
					return c.Next()
				})
			},
			configureRequest: func(req *fasthttp.Request) {
				req.Header.Set(fiber.HeaderOrigin, corsSessionOrigin)
			},
			assertions: func(t *testing.T, resp *fasthttp.Response) {
				t.Helper()
				require.Equal(t, corsSessionOrigin, string(resp.Header.Peek(fiber.HeaderAccessControlAllowOrigin)))
				require.Equal(t, "true", string(resp.Header.Peek(fiber.HeaderAccessControlAllowCredentials)))
				require.Contains(t, string(resp.Header.Peek(fiber.HeaderSetCookie)), "session_id=")
			},
		},
		{
			name: "helmet+encryptcookie",
			setup: func(app *fiber.App) {
				app.Use(helmet.New())
				app.Use(encryptcookie.New(encryptcookie.Config{Key: integrationEncryptCookieKey}))
				app.Use(func(c fiber.Ctx) error {
					c.Cookie(&fiber.Cookie{Name: encryptedCookieName, Value: encryptedCookieVal})
					return c.Next()
				})
			},
			assertions: func(t *testing.T, resp *fasthttp.Response) {
				t.Helper()
				require.Equal(t, "nosniff", string(resp.Header.Peek(fiber.HeaderXContentTypeOptions)))
				cookieHeader := string(resp.Header.Peek(fiber.HeaderSetCookie))
				require.Contains(t, cookieHeader, encryptedCookieName+"=")
				require.NotContains(t, cookieHeader, encryptedCookieVal)
			},
		},
		// --- Helmet anchored stacks validate security headers across other middleware.
		{
			name: "helmet+limiter",
			setup: func(app *fiber.App) {
				app.Use(helmet.New())
				app.Use(limiter.New(limiter.Config{
					Max:        helmetLimiterMax,
					Expiration: time.Duration(helmetLimiterReset) * time.Second,
					KeyGenerator: func(fiber.Ctx) string {
						return "helmet+limiter"
					},
				}))
			},
			assertions: func(t *testing.T, resp *fasthttp.Response) {
				t.Helper()
				require.Equal(t, "nosniff", string(resp.Header.Peek(fiber.HeaderXContentTypeOptions)))
				require.Equal(t, strconv.Itoa(helmetLimiterMax), string(resp.Header.Peek("X-RateLimit-Limit")))
				require.Equal(t, strconv.Itoa(helmetLimiterMax-1), string(resp.Header.Peek("X-RateLimit-Remaining")))
				require.Equal(t, strconv.Itoa(helmetLimiterReset), string(resp.Header.Peek("X-RateLimit-Reset")))
			},
		},
		{
			name: "cors+csrf",
			setup: func(app *fiber.App) {
				app.Use(cors.New(cors.Config{
					AllowOrigins:     []string{corsCSRForigin},
					AllowCredentials: true,
				}))
				app.Use(csrf.New(csrf.Config{
					CookieName:   csrfCookieName,
					KeyGenerator: func() string { return csrfTokenValue },
				}))
			},
			configureRequest: func(req *fasthttp.Request) {
				req.Header.SetMethod(fiber.MethodGet)
				req.Header.Set(fiber.HeaderOrigin, corsCSRForigin)
			},
			assertions: func(t *testing.T, resp *fasthttp.Response) {
				t.Helper()
				require.Equal(t, corsCSRForigin, string(resp.Header.Peek(fiber.HeaderAccessControlAllowOrigin)))
				require.Equal(t, "true", string(resp.Header.Peek(fiber.HeaderAccessControlAllowCredentials)))
				require.Contains(t, string(resp.Header.Peek(fiber.HeaderSetCookie)), csrfCookieName+"="+csrfTokenValue)
			},
		},
		{
			name: "helmet+session",
			setup: func(app *fiber.App) {
				app.Use(helmet.New())
				app.Use(session.New())
				app.Use(func(c fiber.Ctx) error {
					if sm := session.FromContext(c); sm != nil {
						sm.Set("combo-session", "enabled")
					}
					return c.Next()
				})
			},
			assertions: func(t *testing.T, resp *fasthttp.Response) {
				t.Helper()
				require.Equal(t, "nosniff", string(resp.Header.Peek(fiber.HeaderXContentTypeOptions)))
				require.Contains(t, string(resp.Header.Peek(fiber.HeaderSetCookie)), "session_id=")
			},
		},
		{
			name: "helmet+csrf",
			setup: func(app *fiber.App) {
				app.Use(helmet.New())
				app.Use(csrf.New(csrf.Config{
					CookieName:   csrfCookieName,
					KeyGenerator: func() string { return csrfTokenValue },
				}))
			},
			configureRequest: func(req *fasthttp.Request) {
				req.Header.SetMethod(fiber.MethodGet)
			},
			assertions: func(t *testing.T, resp *fasthttp.Response) {
				t.Helper()
				require.Equal(t, "nosniff", string(resp.Header.Peek(fiber.HeaderXContentTypeOptions)))
				require.Contains(t, string(resp.Header.Peek(fiber.HeaderSetCookie)), csrfCookieName+"="+csrfTokenValue)
			},
		},
		{
			name: "helmet+envvar",
			setup: func(app *fiber.App) {
				app.Use(helmet.New())
				app.Use(envvar.New(envvar.Config{ExportVars: map[string]string{"COMBO_ENV": "configured"}}))
			},
			expectedStatus: fiber.StatusMethodNotAllowed,
			assertions: func(t *testing.T, resp *fasthttp.Response) {
				t.Helper()
				require.Equal(t, "nosniff", string(resp.Header.Peek(fiber.HeaderXContentTypeOptions)))
				require.Equal(t, envvarAllowHeader, string(resp.Header.Peek(fiber.HeaderAllow)))
			},
		},
		{
			name: "helmet+basicauth",
			setup: func(app *fiber.App) {
				app.Use(helmet.New())
				app.Use(basicauth.New(basicauth.Config{
					Realm: basicRealm,
					Unauthorized: func(c fiber.Ctx) error {
						c.Set(fiber.HeaderWWWAuthenticate, "Basic realm=\""+basicRealm+"\", charset=\"UTF-8\"")
						c.Set(fiber.HeaderCacheControl, "no-store")
						c.Set(fiber.HeaderVary, fiber.HeaderAuthorization)
						c.Status(fiber.StatusUnauthorized)
						return fiber.ErrUnauthorized
					},
				}))
			},
			expectedStatus: fiber.StatusUnauthorized,
			assertions: func(t *testing.T, resp *fasthttp.Response) {
				t.Helper()
				require.Equal(t, "nosniff", string(resp.Header.Peek(fiber.HeaderXContentTypeOptions)))
				require.Equal(t, "Basic realm=\""+basicRealm+"\", charset=\"UTF-8\"", string(resp.Header.Peek(fiber.HeaderWWWAuthenticate)))
				require.Equal(t, "no-store", string(resp.Header.Peek(fiber.HeaderCacheControl)))
				require.Equal(t, fiber.HeaderAuthorization, string(resp.Header.Peek(fiber.HeaderVary)))
			},
		},
		{
			name: "helmet+keyauth",
			setup: func(app *fiber.App) {
				app.Use(helmet.New())
				app.Use(keyauth.New(keyauth.Config{
					Realm:            keyAuthRealm,
					Error:            keyauth.ErrorInvalidToken,
					ErrorDescription: keyAuthErrorDesc,
					Validator: func(fiber.Ctx, string) (bool, error) {
						return false, nil
					},
					ErrorHandler: func(c fiber.Ctx, _ error) error {
						c.Status(fiber.StatusUnauthorized)
						return fiber.ErrUnauthorized
					},
				}))
			},
			expectedStatus: fiber.StatusUnauthorized,
			assertions: func(t *testing.T, resp *fasthttp.Response) {
				t.Helper()
				require.Equal(t, "nosniff", string(resp.Header.Peek(fiber.HeaderXContentTypeOptions)))
				authenticate := string(resp.Header.Peek(fiber.HeaderWWWAuthenticate))
				require.Contains(t, authenticate, "Bearer realm=\""+keyAuthRealm+"\"")
				require.Contains(t, authenticate, "error=\""+keyauth.ErrorInvalidToken+"\"")
				require.Contains(t, authenticate, "error_description=\""+keyAuthErrorDesc+"\"")
			},
		},
		{
			name: "helmet+compress",
			setup: func(app *fiber.App) {
				app.Use(helmet.New())
				app.Use(compress.New())
				app.Use(func(c fiber.Ctx) error {
					if err := c.Next(); err != nil {
						if handlerErr := app.Config().ErrorHandler(c, err); handlerErr != nil {
							return handlerErr
						}
						// Inflate the error body so the compress middleware has something to work with.
						if body := c.Response().Body(); len(body) > 0 {
							c.Response().SetBodyString(strings.Repeat(string(body), 32))
						}
					}
					return nil
				})
			},
			configureRequest: func(req *fasthttp.Request) {
				req.Header.Set(fiber.HeaderAcceptEncoding, "gzip")
			},
			assertions: func(t *testing.T, resp *fasthttp.Response) {
				t.Helper()
				require.Equal(t, "nosniff", string(resp.Header.Peek(fiber.HeaderXContentTypeOptions)))
				require.Equal(t, "gzip", string(resp.Header.Peek(fiber.HeaderContentEncoding)))
				require.Equal(t, fiber.HeaderAcceptEncoding, string(resp.Header.Peek(fiber.HeaderVary)))
			},
		},
		{
			name: "helmet+recover",
			setup: func(app *fiber.App) {
				app.Use(helmet.New())
				app.Use(recover.New())
			},
			handler: func(fiber.Ctx) error {
				panic("panic for recover middleware")
			},
			assertions: func(t *testing.T, resp *fasthttp.Response) {
				t.Helper()
				require.Equal(t, "nosniff", string(resp.Header.Peek(fiber.HeaderXContentTypeOptions)))
				// Recover writes a plain-text body; ensure we still return content to clients while
				// keeping Helmet's security headers intact.
				require.Contains(t, string(resp.Body()), "panic for recover middleware")
			},
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			app := fiber.New()
			tc.setup(app)
			// Every stack shares the same route that always hits the default error handler so we
			// can verify which headers survive the error response. A few cases override the
			// handler to exercise panic recovery or other routes that still flow through the
			// default error path.
			app.All("/", tc.handlerOrDefault())

			resp := performOversizedRequest(t, app, tc.configureRequest)

			require.Equal(t, tc.statusOrDefault(), resp.StatusCode())
			tc.assertions(t, resp)
		})
	}
}

func Test_Integration_App_ServerErrorHandler_PreservesCORSHeadersOnBodyLimit(t *testing.T) {
	app := fiber.New(fiber.Config{BodyLimit: 16})
	app.Use(cors.New(cors.Config{
		AllowOrigins:     []string{"https://example.com"},
		AllowCredentials: true,
		ExposeHeaders:    []string{"X-Request-ID"},
	}))
	app.Post("/", func(c fiber.Ctx) error {
		return c.SendStatus(fiber.StatusOK)
	})

	resp := performOversizedRequest(t, app, nil)

	require.Equal(t, fiber.StatusRequestEntityTooLarge, resp.StatusCode())
	require.Equal(t, "https://example.com", string(resp.Header.Peek(fiber.HeaderAccessControlAllowOrigin)))
	require.Equal(t, "true", string(resp.Header.Peek(fiber.HeaderAccessControlAllowCredentials)))
	require.Equal(t, "X-Request-ID", string(resp.Header.Peek(fiber.HeaderAccessControlExposeHeaders)))
	require.Equal(t, "Origin", string(resp.Header.Peek(fiber.HeaderVary)))
}

func Test_Integration_App_ServerErrorHandler_PreservesHelmetHeadersOnBodyLimit(t *testing.T) {
	app := fiber.New(fiber.Config{BodyLimit: 16})
	app.Use(helmet.New())
	app.Post("/", func(c fiber.Ctx) error {
		return c.SendStatus(fiber.StatusOK)
	})

	resp := performOversizedRequest(t, app, nil)

	require.Equal(t, fiber.StatusRequestEntityTooLarge, resp.StatusCode())
	require.Equal(t, "nosniff", string(resp.Header.Peek(fiber.HeaderXContentTypeOptions)))
	require.Equal(t, "same-origin", string(resp.Header.Peek("Cross-Origin-Opener-Policy")))
	require.Equal(t, "same-origin", string(resp.Header.Peek("Cross-Origin-Resource-Policy")))
	require.Equal(t, "require-corp", string(resp.Header.Peek("Cross-Origin-Embedder-Policy")))
}

func Test_Integration_App_ServerErrorHandler_PreservesRequestID(t *testing.T) {
	const expectedRequestID = "integration-request-id"

	app := fiber.New(fiber.Config{BodyLimit: 16})
	app.Use(requestid.New())
	app.Post("/", func(c fiber.Ctx) error {
		return c.SendStatus(fiber.StatusOK)
	})

	resp := performOversizedRequest(t, app, func(req *fasthttp.Request) {
		req.Header.Set("X-Request-ID", expectedRequestID)
	})

	require.Equal(t, fiber.StatusRequestEntityTooLarge, resp.StatusCode())
	require.Equal(t, expectedRequestID, string(resp.Header.Peek("X-Request-ID")))
}

func Test_Integration_App_ServerErrorHandler_GroupMiddlewareChain(t *testing.T) {
	app := fiber.New(fiber.Config{BodyLimit: 16})
	app.Use(helmet.New())

	api := app.Group("/api")
	api.Use(requestid.New())
	api.Use(func(c fiber.Ctx) error {
		c.Set("X-Group-Middleware", "active")
		return c.Next()
	})
	api.Post("/resource", func(c fiber.Ctx) error {
		return c.SendStatus(fiber.StatusOK)
	})

	resp := performOversizedRequest(t, app, func(req *fasthttp.Request) {
		req.SetRequestURI("http://example.com/api/resource")
	})

	require.Equal(t, fiber.StatusRequestEntityTooLarge, resp.StatusCode())
	require.Equal(t, "nosniff", string(resp.Header.Peek(fiber.HeaderXContentTypeOptions)))
	require.NotEmpty(t, resp.Header.Peek("X-Request-ID"))
	require.Equal(t, "active", string(resp.Header.Peek("X-Group-Middleware")))
}

func Test_Integration_App_ServerErrorHandler_RetainsHeadersFromSubsequentMiddleware(t *testing.T) {
	app := fiber.New(fiber.Config{BodyLimit: 8})
	app.Use(func(c fiber.Ctx) error {
		c.Set("X-Custom-Middleware", "ran")
		return c.Next()
	})
	app.Use(cors.New())
	app.Post("/", func(c fiber.Ctx) error {
		return c.SendStatus(fiber.StatusOK)
	})

	resp := performOversizedRequest(t, app, nil)

	require.Equal(t, fiber.StatusRequestEntityTooLarge, resp.StatusCode())
	require.Equal(t, "ran", string(resp.Header.Peek("X-Custom-Middleware")))
	require.Equal(t, "*", string(resp.Header.Peek(fiber.HeaderAccessControlAllowOrigin)))
}

func Test_Integration_App_ServerErrorHandler_WithCustomCtx(t *testing.T) {
	app := fiber.NewWithCustomCtx(newIntegrationCustomCtx, fiber.Config{BodyLimit: 16})
	app.Use(func(c fiber.Ctx) error {
		customCtx, ok := c.(*integrationCustomCtx)
		require.True(t, ok)
		customCtx.Set("X-Custom-Ctx", "true")
		return c.Next()
	})
	app.Use(cors.New(cors.Config{AllowOrigins: []string{"https://example.org"}}))
	app.Post("/", func(c fiber.Ctx) error {
		return c.SendStatus(fiber.StatusOK)
	})

	resp := performOversizedRequest(t, app, func(req *fasthttp.Request) {
		req.Header.Set(fiber.HeaderOrigin, "https://example.org")
	})

	require.Equal(t, fiber.StatusRequestEntityTooLarge, resp.StatusCode())
	require.Equal(t, "true", string(resp.Header.Peek("X-Custom-Ctx")))
	require.Equal(t, "https://example.org", string(resp.Header.Peek(fiber.HeaderAccessControlAllowOrigin)))
}

// Integration tests for domain routing

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

	app := fiber.New()
	app.Use(helmet.New())
	app.Use(requestid.New())

	// Domain-specific route with middleware
	app.Domain("api.example.com").Get("/users", func(c fiber.Ctx) error {
		return c.SendString("api users")
	})

	// Fallback route
	app.Get("/users", func(c fiber.Ctx) error {
		return c.SendString("default users")
	})

	// Test matching domain
	req := httptest.NewRequest(http.MethodGet, "/users", http.NoBody)
	req.Host = "api.example.com"
	resp, err := app.Test(req)
	require.NoError(t, err)
	require.Equal(t, fiber.StatusOK, resp.StatusCode)
	body, err := io.ReadAll(resp.Body)
	require.NoError(t, err)
	require.Equal(t, "api users", string(body))
	require.NotEmpty(t, resp.Header.Get("X-Request-ID"))
	require.Equal(t, "nosniff", resp.Header.Get(fiber.HeaderXContentTypeOptions))

	// Test non-matching domain (fallback)
	req = httptest.NewRequest(http.MethodGet, "/users", http.NoBody)
	req.Host = "www.example.com"
	resp, err = app.Test(req)
	require.NoError(t, err)
	require.Equal(t, fiber.StatusOK, resp.StatusCode)
	body, err = io.ReadAll(resp.Body)
	require.NoError(t, err)
	require.Equal(t, "default users", string(body))
}

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

	const apiOrigin = "https://api.example.com"

	app := fiber.New()
	app.Use(cors.New(cors.Config{
		AllowOrigins:     []string{apiOrigin},
		AllowCredentials: true,
	}))

	app.Domain("api.example.com").Get("/data", func(c fiber.Ctx) error {
		return c.JSON(fiber.Map{"source": "api"})
	})

	req := httptest.NewRequest(http.MethodGet, "/data", http.NoBody)
	req.Host = "api.example.com"
	req.Header.Set(fiber.HeaderOrigin, apiOrigin)
	resp, err := app.Test(req)
	require.NoError(t, err)
	require.Equal(t, fiber.StatusOK, resp.StatusCode)
	require.Equal(t, apiOrigin, resp.Header.Get(fiber.HeaderAccessControlAllowOrigin))
	require.Equal(t, "true", resp.Header.Get(fiber.HeaderAccessControlAllowCredentials))
}

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

	app := fiber.New()
	app.Use(helmet.New())

	// Domain with group
	apiDomain := app.Domain("api.example.com")
	apiDomain.Use(requestid.New(requestid.Config{
		Generator: func() string { return "domain-request-id" },
	}))

	v1 := apiDomain.Group("/v1")
	v1.Get("/posts", func(c fiber.Ctx) error {
		return c.SendString("v1 posts")
	})

	v2 := apiDomain.Group("/v2")
	v2.Get("/posts", func(c fiber.Ctx) error {
		return c.SendString("v2 posts")
	})

	// Test v1
	req := httptest.NewRequest(http.MethodGet, "/v1/posts", http.NoBody)
	req.Host = "api.example.com"
	resp, err := app.Test(req)
	require.NoError(t, err)
	require.Equal(t, fiber.StatusOK, resp.StatusCode)
	body, err := io.ReadAll(resp.Body)
	require.NoError(t, err)
	require.Equal(t, "v1 posts", string(body))
	require.Equal(t, "domain-request-id", resp.Header.Get("X-Request-ID"))
	require.Equal(t, "nosniff", resp.Header.Get(fiber.HeaderXContentTypeOptions))

	// Test v2
	req = httptest.NewRequest(http.MethodGet, "/v2/posts", http.NoBody)
	req.Host = "api.example.com"
	resp, err = app.Test(req)
	require.NoError(t, err)
	require.Equal(t, fiber.StatusOK, resp.StatusCode)
	body, err = io.ReadAll(resp.Body)
	require.NoError(t, err)
	require.Equal(t, "v2 posts", string(body))
}

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

	app := fiber.New()
	app.Use(helmet.New())

	// Create sub-app with routes
	subApp := fiber.New()
	subApp.Use(requestid.New())
	subApp.Get("/users", func(c fiber.Ctx) error {
		return c.JSON(fiber.Map{"users": []string{"alice", "bob"}})
	})
	subApp.Get("/posts", func(c fiber.Ctx) error {
		return c.JSON(fiber.Map{"posts": []string{"post1", "post2"}})
	})

	// Mount sub-app on domain
	app.Domain("api.example.com").Use("/api", subApp)

	// Fallback route
	app.Get("/api/users", func(c fiber.Ctx) error {
		return c.SendString("default users")
	})

	// Test domain-mounted sub-app
	req := httptest.NewRequest(http.MethodGet, "/api/users", http.NoBody)
	req.Host = "api.example.com"
	resp, err := app.Test(req)
	require.NoError(t, err)
	require.Equal(t, fiber.StatusOK, resp.StatusCode)
	body, err := io.ReadAll(resp.Body)
	require.NoError(t, err)
	require.Contains(t, string(body), "alice")
	require.NotEmpty(t, resp.Header.Get("X-Request-ID"))
	require.Equal(t, "nosniff", resp.Header.Get(fiber.HeaderXContentTypeOptions))

	// Test non-matching domain (fallback)
	req = httptest.NewRequest(http.MethodGet, "/api/users", http.NoBody)
	req.Host = "www.example.com"
	resp, err = app.Test(req)
	require.NoError(t, err)
	require.Equal(t, fiber.StatusOK, resp.StatusCode)
	body, err = io.ReadAll(resp.Body)
	require.NoError(t, err)
	require.Equal(t, "default users", string(body))
}

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

	app := fiber.New()
	app.Use(requestid.New())

	// Domain with parameters
	app.Domain(":tenant.app.example.com").Get("/dashboard", func(c fiber.Ctx) error {
		tenant := fiber.DomainParam(c, "tenant")
		return c.JSON(fiber.Map{
			"tenant":    tenant,
			"page":      "dashboard",
			"requestID": c.Get("X-Request-ID"),
		})
	})

	req := httptest.NewRequest(http.MethodGet, "/dashboard", http.NoBody)
	req.Host = "acme.app.example.com"
	resp, err := app.Test(req)
	require.NoError(t, err)
	require.Equal(t, fiber.StatusOK, resp.StatusCode)
	body, err := io.ReadAll(resp.Body)
	require.NoError(t, err)
	require.Contains(t, string(body), "acme")
	require.Contains(t, string(body), "dashboard")
	require.NotEmpty(t, resp.Header.Get("X-Request-ID"))
}

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

	app := fiber.New()

	// Different domains with different responses
	app.Domain("api.example.com").Get("/", func(c fiber.Ctx) error {
		return c.SendString("api domain")
	})

	app.Domain("admin.example.com").Get("/", func(c fiber.Ctx) error {
		return c.SendString("admin domain")
	})

	app.Domain("app.example.com").Get("/", func(c fiber.Ctx) error {
		return c.SendString("app domain")
	})

	// Default fallback
	app.Get("/", func(c fiber.Ctx) error {
		return c.SendString("default domain")
	})

	tests := []struct {
		host     string
		expected string
	}{
		{"api.example.com", "api domain"},
		{"admin.example.com", "admin domain"},
		{"app.example.com", "app domain"},
		{"www.example.com", "default domain"},
		{"unknown.example.com", "default domain"},
	}

	for _, tt := range tests {
		t.Run(tt.host, func(t *testing.T) {
			t.Parallel()
			req := httptest.NewRequest(http.MethodGet, "/", http.NoBody)
			req.Host = tt.host
			resp, err := app.Test(req)
			require.NoError(t, err)
			require.Equal(t, fiber.StatusOK, resp.StatusCode)
			body, err := io.ReadAll(resp.Body)
			require.NoError(t, err)
			require.Equal(t, tt.expected, string(body))
		})
	}
}

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

	app := fiber.New()
	app.Use(session.New())

	app.Domain("api.example.com").Get("/session", func(c fiber.Ctx) error {
		sess := session.FromContext(c)
		if sess == nil {
			return c.Status(fiber.StatusInternalServerError).SendString("no session")
		}
		sess.Set("domain", "api")
		return c.SendString("session set")
	})

	req := httptest.NewRequest(http.MethodGet, "/session", http.NoBody)
	req.Host = "api.example.com"
	resp, err := app.Test(req)
	require.NoError(t, err)
	require.Equal(t, fiber.StatusOK, resp.StatusCode)
	require.Contains(t, resp.Header.Get(fiber.HeaderSetCookie), "session_id=")
}

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

	app := fiber.New()
	app.Use(compress.New())

	app.Domain("api.example.com").Get("/data", func(c fiber.Ctx) error {
		// Return large enough content for compression
		return c.SendString(strings.Repeat("Hello World! ", 100))
	})

	req := httptest.NewRequest(http.MethodGet, "/data", http.NoBody)
	req.Host = "api.example.com"
	req.Header.Set(fiber.HeaderAcceptEncoding, "gzip")
	resp, err := app.Test(req)
	require.NoError(t, err)
	require.Equal(t, fiber.StatusOK, resp.StatusCode)
	require.Equal(t, "gzip", resp.Header.Get(fiber.HeaderContentEncoding))
}

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

	app := fiber.New(fiber.Config{
		ErrorHandler: func(c fiber.Ctx, err error) error {
			c.Set("X-Error-Handled", "true")
			return c.Status(fiber.StatusInternalServerError).SendString("error: " + err.Error())
		},
	})
	app.Use(helmet.New())

	app.Domain("api.example.com").Get("/error", func(_ fiber.Ctx) error {
		return fiber.NewError(fiber.StatusInternalServerError, "domain error")
	})

	req := httptest.NewRequest(http.MethodGet, "/error", http.NoBody)
	req.Host = "api.example.com"
	resp, err := app.Test(req)
	require.NoError(t, err)
	require.Equal(t, fiber.StatusInternalServerError, resp.StatusCode)
	require.Equal(t, "true", resp.Header.Get("X-Error-Handled"))
	require.Equal(t, "nosniff", resp.Header.Get(fiber.HeaderXContentTypeOptions))
	body, err := io.ReadAll(resp.Body)
	require.NoError(t, err)
	require.Contains(t, string(body), "domain error")
}

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

	const maxRequests = 3

	app := fiber.New()
	app.Use(limiter.New(limiter.Config{
		Max:        maxRequests,
		Expiration: 1 * time.Minute,
		KeyGenerator: func(_ fiber.Ctx) string {
			return "domain-limiter"
		},
	}))

	app.Domain("api.example.com").Get("/limited", func(c fiber.Ctx) error {
		return c.SendString("ok")
	})

	// Make requests up to the limit
	for i := 0; i < maxRequests; i++ {
		req := httptest.NewRequest(http.MethodGet, "/limited", http.NoBody)
		req.Host = "api.example.com"
		resp, err := app.Test(req)
		require.NoError(t, err)
		require.Equal(t, fiber.StatusOK, resp.StatusCode)
		require.Equal(t, strconv.Itoa(maxRequests), resp.Header.Get("X-RateLimit-Limit"))
	}

	// Next request should be rate limited
	req := httptest.NewRequest(http.MethodGet, "/limited", http.NoBody)
	req.Host = "api.example.com"
	resp, err := app.Test(req)
	require.NoError(t, err)
	require.Equal(t, fiber.StatusTooManyRequests, resp.StatusCode)
}

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

	const csrfCookie = "domain-csrf"

	app := fiber.New()
	app.Use(csrf.New(csrf.Config{
		CookieName:   csrfCookie,
		KeyGenerator: func() string { return "test-csrf-token" },
	}))

	app.Domain("api.example.com").Get("/form", func(c fiber.Ctx) error {
		return c.SendString("form page")
	})

	req := httptest.NewRequest(http.MethodGet, "/form", http.NoBody)
	req.Host = "api.example.com"
	resp, err := app.Test(req)
	require.NoError(t, err)
	require.Equal(t, fiber.StatusOK, resp.StatusCode)
	require.Contains(t, resp.Header.Get(fiber.HeaderSetCookie), csrfCookie+"=")
}

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

	app := fiber.New()

	app.Domain("API.Example.COM").Get("/", func(c fiber.Ctx) error {
		return c.SendString("matched")
	})

	// Test various case combinations
	hosts := []string{
		"api.example.com",
		"API.EXAMPLE.COM",
		"Api.Example.Com",
		"aPi.eXaMpLe.CoM",
	}

	for _, host := range hosts {
		t.Run(host, func(t *testing.T) {
			t.Parallel()
			req := httptest.NewRequest(http.MethodGet, "/", http.NoBody)
			req.Host = host
			resp, err := app.Test(req)
			require.NoError(t, err)
			require.Equal(t, fiber.StatusOK, resp.StatusCode)
			body, err := io.ReadAll(resp.Body)
			require.NoError(t, err)
			require.Equal(t, "matched", string(body))
		})
	}
}

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

	app := fiber.New()

	// Create a reusable sub-app
	apiApp := fiber.New()
	apiApp.Get("/status", func(c fiber.Ctx) error {
		return c.SendString("ok")
	})

	// Mount the same sub-app on multiple domains
	app.Domain("api1.example.com").Use("/v1", apiApp)
	app.Domain("api2.example.com").Use("/v2", apiApp)

	// Test first domain
	req := httptest.NewRequest(http.MethodGet, "/v1/status", http.NoBody)
	req.Host = "api1.example.com"
	resp, err := app.Test(req)
	require.NoError(t, err)
	require.Equal(t, fiber.StatusOK, resp.StatusCode)
	body, err := io.ReadAll(resp.Body)
	require.NoError(t, err)
	require.Equal(t, "ok", string(body))

	// Test second domain
	req = httptest.NewRequest(http.MethodGet, "/v2/status", http.NoBody)
	req.Host = "api2.example.com"
	resp, err = app.Test(req)
	require.NoError(t, err)
	require.Equal(t, fiber.StatusOK, resp.StatusCode)
	body, err = io.ReadAll(resp.Body)
	require.NoError(t, err)
	require.Equal(t, "ok", string(body))
}
