package compress

import (
	"bytes"
	"compress/gzip"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"os"
	"strings"
	"testing"
	"time"

	"github.com/gofiber/fiber/v3"
	"github.com/gofiber/fiber/v3/middleware/etag"
	"github.com/stretchr/testify/require"
	"github.com/valyala/fasthttp"
)

var filedata []byte

var testConfig = fiber.TestConfig{
	Timeout:       10 * time.Second,
	FailOnTimeout: true,
}

func init() {
	dat, err := os.ReadFile("../../.github/README.md")
	if err != nil {
		panic(err)
	}
	filedata = dat
}

// go test -run Test_Compress_Gzip
func Test_Compress_Gzip(t *testing.T) {
	t.Parallel()
	app := fiber.New()

	app.Use(New())

	app.Get("/", func(c fiber.Ctx) error {
		c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
		return c.Send(filedata)
	})

	req := httptest.NewRequest(fiber.MethodGet, "/", http.NoBody)
	req.Header.Set("Accept-Encoding", "gzip")

	resp, err := app.Test(req, testConfig)
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 200, resp.StatusCode, "Status code")
	require.Equal(t, "gzip", resp.Header.Get(fiber.HeaderContentEncoding))

	// Validate that the file size has shrunk
	body, err := io.ReadAll(resp.Body)
	require.NoError(t, err)
	require.Less(t, len(body), len(filedata))
}

// go test -run Test_Compress_Different_Level
func Test_Compress_Different_Level(t *testing.T) {
	t.Parallel()
	levels := []Level{LevelDefault, LevelBestSpeed, LevelBestCompression}
	algorithms := []string{"gzip", "deflate", "br", "zstd"}

	for _, algo := range algorithms {
		for _, level := range levels {
			t.Run(fmt.Sprintf("%s_level %d", algo, level), func(t *testing.T) {
				t.Parallel()
				app := fiber.New()

				app.Use(New(Config{Level: level}))

				app.Get("/", func(c fiber.Ctx) error {
					c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
					return c.Send(filedata)
				})

				req := httptest.NewRequest(fiber.MethodGet, "/", http.NoBody)
				req.Header.Set("Accept-Encoding", algo)

				resp, err := app.Test(req, testConfig)
				require.NoError(t, err, "app.Test(req)")
				require.Equal(t, 200, resp.StatusCode, "Status code")
				require.Equal(t, algo, resp.Header.Get(fiber.HeaderContentEncoding))

				// Validate that the file size has shrunk
				body, err := io.ReadAll(resp.Body)
				require.NoError(t, err)
				require.Less(t, len(body), len(filedata))
			})
		}
	}
}

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

	app.Use(New())

	app.Get("/", func(c fiber.Ctx) error {
		return c.Send(filedata)
	})

	req := httptest.NewRequest(fiber.MethodGet, "/", http.NoBody)
	req.Header.Set("Accept-Encoding", "deflate")

	resp, err := app.Test(req, testConfig)
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 200, resp.StatusCode, "Status code")
	require.Equal(t, "deflate", resp.Header.Get(fiber.HeaderContentEncoding))

	// Validate that the file size has shrunk
	body, err := io.ReadAll(resp.Body)
	require.NoError(t, err)
	require.Less(t, len(body), len(filedata))
}

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

	app.Use(New())

	app.Get("/", func(c fiber.Ctx) error {
		return c.Send(filedata)
	})

	req := httptest.NewRequest(fiber.MethodGet, "/", http.NoBody)
	req.Header.Set("Accept-Encoding", "br")

	resp, err := app.Test(req, testConfig)
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 200, resp.StatusCode, "Status code")
	require.Equal(t, "br", resp.Header.Get(fiber.HeaderContentEncoding))

	// Validate that the file size has shrunk
	body, err := io.ReadAll(resp.Body)
	require.NoError(t, err)
	require.Less(t, len(body), len(filedata))
}

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

	app.Use(New())

	app.Get("/", func(c fiber.Ctx) error {
		return c.Send(filedata)
	})

	req := httptest.NewRequest(fiber.MethodGet, "/", http.NoBody)
	req.Header.Set("Accept-Encoding", "zstd")

	resp, err := app.Test(req, testConfig)
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 200, resp.StatusCode, "Status code")
	require.Equal(t, "zstd", resp.Header.Get(fiber.HeaderContentEncoding))

	// Validate that the file size has shrunk
	body, err := io.ReadAll(resp.Body)
	require.NoError(t, err)
	require.Less(t, len(body), len(filedata))
}

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

	app.Use(New(Config{Level: LevelDisabled}))

	app.Get("/", func(c fiber.Ctx) error {
		return c.Send(filedata)
	})

	req := httptest.NewRequest(fiber.MethodGet, "/", http.NoBody)
	req.Header.Set("Accept-Encoding", "br")

	resp, err := app.Test(req, testConfig)
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 200, resp.StatusCode, "Status code")
	require.Empty(t, resp.Header.Get(fiber.HeaderContentEncoding))

	// Validate the file size is not shrunk
	body, err := io.ReadAll(resp.Body)
	require.NoError(t, err)
	require.Len(t, body, len(filedata))
}

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

	app.Use(New())

	app.Get("/", func(c fiber.Ctx) error {
		return c.SendString("hello")
	})

	req := httptest.NewRequest(fiber.MethodGet, "/", http.NoBody)
	req.Header.Set("Accept-Encoding", "gzip")

	resp, err := app.Test(req, testConfig)
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, "Accept-Encoding", resp.Header.Get(fiber.HeaderVary))
}

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

	app.Use(New())

	app.Get("/", func(c fiber.Ctx) error {
		c.Set(fiber.HeaderVary, "*")
		return c.SendString("hello")
	})

	req := httptest.NewRequest(fiber.MethodGet, "/", http.NoBody)
	req.Header.Set("Accept-Encoding", "gzip")

	resp, err := app.Test(req, testConfig)
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, "*", resp.Header.Get(fiber.HeaderVary))
}

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

	app.Use(New())

	app.Get("/", func(c fiber.Ctx) error {
		c.Set(fiber.HeaderVary, "User-Agent, *")
		return c.SendString("hello")
	})

	req := httptest.NewRequest(fiber.MethodGet, "/", http.NoBody)
	req.Header.Set("Accept-Encoding", "gzip")

	resp, err := app.Test(req, testConfig)
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, "User-Agent, *", resp.Header.Get(fiber.HeaderVary))
}

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

	app := fiber.New(fiber.Config{
		BodyLimit: 40,
	})

	app.Use(New())

	app.Post("/", func(c fiber.Ctx) error {
		return c.Send(c.Body())
	})

	var b bytes.Buffer
	gz := gzip.NewWriter(&b)
	_, err := gz.Write([]byte(strings.Repeat("a", 256)))
	require.NoError(t, err)
	require.NoError(t, gz.Flush())
	require.NoError(t, gz.Close())

	req := httptest.NewRequest(fiber.MethodPost, "/", bytes.NewReader(b.Bytes()))
	req.Header.Set(fiber.HeaderContentEncoding, fiber.StrGzip)
	req.Header.Set(fiber.HeaderContentType, fiber.MIMETextPlain)
	req.Header.Set(fiber.HeaderAcceptEncoding, fiber.StrIdentity)

	resp, err := app.Test(req, testConfig)
	require.NoError(t, err)
	require.Equal(t, fiber.StatusRequestEntityTooLarge, resp.StatusCode)

	body, err := io.ReadAll(resp.Body)
	require.NoError(t, err)
	require.Equal(t, fasthttp.ErrBodyTooLarge.Error(), string(body))
}

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

	app.Use(New())

	app.Get("/", func(c fiber.Ctx) error {
		c.Set(fiber.HeaderVary, "Accept-Encoding2")
		return c.SendString("hello")
	})

	req := httptest.NewRequest(fiber.MethodGet, "/", http.NoBody)
	req.Header.Set("Accept-Encoding", "gzip")

	resp, err := app.Test(req, testConfig)
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, "Accept-Encoding2, Accept-Encoding", resp.Header.Get(fiber.HeaderVary))
}

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

	app.Use(New())

	app.Get("/", func(c fiber.Ctx) error {
		c.Set(fiber.HeaderContentEncoding, "gzip")
		c.Set(fiber.HeaderETag, "\"abc\"")
		return c.SendString("hello")
	})

	req := httptest.NewRequest(fiber.MethodGet, "/", http.NoBody)
	req.Header.Set("Accept-Encoding", "gzip")

	resp, err := app.Test(req, testConfig)
	require.NoError(t, err, "app.Test(req)")
	body, err := io.ReadAll(resp.Body)
	require.NoError(t, err)
	require.Equal(t, "hello", string(body))
	require.Equal(t, "gzip", resp.Header.Get(fiber.HeaderContentEncoding))
	require.Equal(t, "\"abc\"", resp.Header.Get(fiber.HeaderETag))
	require.Equal(t, "Accept-Encoding", resp.Header.Get(fiber.HeaderVary))
}

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

	app.Use(New())

	app.Get("/", func(c fiber.Ctx) error {
		c.Set(fiber.HeaderContentEncoding, "gzip")
		c.Set(fiber.HeaderVary, "*")
		return c.SendString("hello")
	})

	req := httptest.NewRequest(fiber.MethodGet, "/", http.NoBody)
	req.Header.Set("Accept-Encoding", "gzip")

	resp, err := app.Test(req, testConfig)
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, "*", resp.Header.Get(fiber.HeaderVary))
}

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

	app.Use(New())

	app.Get("/", func(c fiber.Ctx) error {
		c.Set(fiber.HeaderContentEncoding, "gzip")
		c.Set(fiber.HeaderVary, "User-Agent, *")
		return c.SendString("hello")
	})

	req := httptest.NewRequest(fiber.MethodGet, "/", http.NoBody)
	req.Header.Set("Accept-Encoding", "gzip")

	resp, err := app.Test(req, testConfig)
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, "User-Agent, *", resp.Header.Get(fiber.HeaderVary))
}

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

	app.Use(New())

	app.Get("/", func(c fiber.Ctx) error {
		c.Set(fiber.HeaderContentEncoding, "gzip")
		c.Set(fiber.HeaderVary, "Accept-Encoding2")
		return c.SendString("hello")
	})

	req := httptest.NewRequest(fiber.MethodGet, "/", http.NoBody)
	req.Header.Set("Accept-Encoding", "gzip")

	resp, err := app.Test(req, testConfig)
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, "Accept-Encoding2, Accept-Encoding", resp.Header.Get(fiber.HeaderVary))
}

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

	app.Use(New())

	app.Get("/", func(c fiber.Ctx) error {
		c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
		c.Set(fiber.HeaderETag, "\"abc\"")
		return c.Send(filedata)
	})

	req := httptest.NewRequest(fiber.MethodGet, "/", http.NoBody)
	req.Header.Set("Accept-Encoding", "gzip")

	resp, err := app.Test(req, testConfig)
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, "gzip", resp.Header.Get(fiber.HeaderContentEncoding))
	body, err := io.ReadAll(resp.Body)
	require.NoError(t, err)
	expected := string(etag.Generate(body))
	require.Equal(t, expected, resp.Header.Get(fiber.HeaderETag))
}

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

	app.Use(New())

	app.Get("/", func(c fiber.Ctx) error {
		c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
		c.Set(fiber.HeaderETag, "W/\"abc\"")
		return c.Send(filedata)
	})

	req := httptest.NewRequest(fiber.MethodGet, "/", http.NoBody)
	req.Header.Set("Accept-Encoding", "gzip")

	resp, err := app.Test(req, testConfig)
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, "gzip", resp.Header.Get(fiber.HeaderContentEncoding))
	require.Equal(t, "W/\"abc\"", resp.Header.Get(fiber.HeaderETag))
}

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

	app.Use(New())

	app.Get("/", func(c fiber.Ctx) error {
		c.Set(fiber.HeaderETag, "\"abc\"")
		return c.SendString("tiny")
	})

	req := httptest.NewRequest(fiber.MethodGet, "/", http.NoBody)
	req.Header.Set("Accept-Encoding", "gzip")

	resp, err := app.Test(req, testConfig)
	require.NoError(t, err)
	require.Empty(t, resp.Header.Get(fiber.HeaderContentEncoding))
	require.Equal(t, "\"abc\"", resp.Header.Get(fiber.HeaderETag))
	require.Equal(t, "Accept-Encoding", resp.Header.Get(fiber.HeaderVary))
}

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

	app.Use(New())

	handler := func(c fiber.Ctx) error {
		c.Set(fiber.HeaderETag, "\"abc\"")
		return c.Send(filedata)
	}
	app.Get("/", handler)
	app.Head("/", handler)

	getReq := httptest.NewRequest(fiber.MethodGet, "/", http.NoBody)
	getReq.Header.Set("Accept-Encoding", "gzip")

	getResp, err := app.Test(getReq, testConfig)
	require.NoError(t, err, "app.Test(getReq)")
	getBody, err := io.ReadAll(getResp.Body)
	require.NoError(t, err)
	require.NotEmpty(t, getBody)
	require.Equal(t, "gzip", getResp.Header.Get(fiber.HeaderContentEncoding))

	headReq := httptest.NewRequest(fiber.MethodHead, "/", http.NoBody)
	headReq.Header.Set("Accept-Encoding", "gzip")

	headResp, err := app.Test(headReq, testConfig)
	require.NoError(t, err, "app.Test(headReq)")
	headBody, err := io.ReadAll(headResp.Body)
	require.NoError(t, err)
	require.Empty(t, headBody)

	require.Empty(t, headResp.Header.Get(fiber.HeaderContentEncoding))
	require.Equal(t, "Accept-Encoding", headResp.Header.Get(fiber.HeaderVary))
	require.Equal(t, "\"abc\"", headResp.Header.Get(fiber.HeaderETag))
}

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

	app.Use(New())

	app.Get("/", func(c fiber.Ctx) error {
		c.Set(fiber.HeaderETag, "\"abc\"")
		return c.SendStatus(fiber.StatusNoContent)
	})

	req := httptest.NewRequest(fiber.MethodGet, "/", http.NoBody)
	req.Header.Set("Accept-Encoding", "gzip")

	resp, err := app.Test(req, testConfig)
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, fiber.StatusNoContent, resp.StatusCode)
	require.Empty(t, resp.Header.Get(fiber.HeaderContentEncoding))
	require.Equal(t, "\"abc\"", resp.Header.Get(fiber.HeaderETag))
}

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

	app.Use(New())

	app.Get("/", func(c fiber.Ctx) error {
		c.Set(fiber.HeaderETag, "\"abc\"")
		c.Status(fiber.StatusNotModified)
		return nil
	})

	req := httptest.NewRequest(fiber.MethodGet, "/", http.NoBody)
	req.Header.Set("Accept-Encoding", "gzip")

	resp, err := app.Test(req, testConfig)
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, fiber.StatusNotModified, resp.StatusCode)
	require.Empty(t, resp.Header.Get(fiber.HeaderContentEncoding))
	require.Equal(t, "\"abc\"", resp.Header.Get(fiber.HeaderETag))
}

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

	app.Use(New())

	app.Get("/", func(c fiber.Ctx) error {
		return c.SendString("hello")
	})

	req := httptest.NewRequest(fiber.MethodGet, "/", http.NoBody)
	req.Header.Set("Accept-Encoding", "gzip")
	req.Header.Set("Range", "bytes=0-1")

	resp, err := app.Test(req, testConfig)
	require.NoError(t, err, "app.Test(req)")
	require.Empty(t, resp.Header.Get(fiber.HeaderContentEncoding))
	require.Equal(t, "Accept-Encoding", resp.Header.Get(fiber.HeaderVary))
}

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

	app.Use(New())

	app.Get("/", func(c fiber.Ctx) error {
		return c.SendString("hello")
	})

	req := httptest.NewRequest(fiber.MethodGet, "/", http.NoBody)
	req.Header.Set("Range", "bytes=0-1")

	resp, err := app.Test(req, testConfig)
	require.NoError(t, err, "app.Test(req)")
	require.Empty(t, resp.Header.Get(fiber.HeaderContentEncoding))
	require.Equal(t, "Accept-Encoding", resp.Header.Get(fiber.HeaderVary))
}

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

	app.Use(New())

	app.Get("/", func(c fiber.Ctx) error {
		c.Set(fiber.HeaderVary, "*")
		return c.SendString("hello")
	})

	req := httptest.NewRequest(fiber.MethodGet, "/", http.NoBody)
	req.Header.Set("Accept-Encoding", "gzip")
	req.Header.Set("Range", "bytes=0-1")

	resp, err := app.Test(req, testConfig)
	require.NoError(t, err, "app.Test(req)")
	require.Empty(t, resp.Header.Get(fiber.HeaderContentEncoding))
	require.Equal(t, "*", resp.Header.Get(fiber.HeaderVary))
}

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

	app.Use(New())

	app.Get("/", func(c fiber.Ctx) error {
		c.Set(fiber.HeaderVary, "Accept-Encoding2")
		return c.SendString("hello")
	})

	req := httptest.NewRequest(fiber.MethodGet, "/", http.NoBody)
	req.Header.Set("Accept-Encoding", "gzip")
	req.Header.Set("Range", "bytes=0-1")

	resp, err := app.Test(req, testConfig)
	require.NoError(t, err, "app.Test(req)")
	require.Empty(t, resp.Header.Get(fiber.HeaderContentEncoding))
	require.Equal(t, "Accept-Encoding2, Accept-Encoding", resp.Header.Get(fiber.HeaderVary))
}

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

	app.Use(New())

	app.Get("/", func(c fiber.Ctx) error {
		c.Status(fiber.StatusPartialContent)
		return c.SendString("hello")
	})

	req := httptest.NewRequest(fiber.MethodGet, "/", http.NoBody)
	req.Header.Set("Accept-Encoding", "gzip")

	resp, err := app.Test(req, testConfig)
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, fiber.StatusPartialContent, resp.StatusCode)
	require.Empty(t, resp.Header.Get(fiber.HeaderContentEncoding))
}

func Test_Compress_Skip_NoTransform(t *testing.T) {
	t.Parallel()
	tests := []struct {
		name       string
		setRequest bool
	}{
		{name: "request", setRequest: true},
		{name: "response", setRequest: false},
	}

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

			app.Use(New())

			app.Get("/", func(c fiber.Ctx) error {
				if !tt.setRequest {
					c.Set(fiber.HeaderCacheControl, "no-transform")
				}
				return c.SendString("hello")
			})

			req := httptest.NewRequest(fiber.MethodGet, "/", http.NoBody)
			req.Header.Set("Accept-Encoding", "gzip")
			if tt.setRequest {
				req.Header.Set(fiber.HeaderCacheControl, "no-transform")
			}

			resp, err := app.Test(req, testConfig)
			require.NoError(t, err, "app.Test(req)")
			require.Empty(t, resp.Header.Get(fiber.HeaderContentEncoding))
		})
	}
}

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

	app.Use(New())

	app.Get("/", func(_ fiber.Ctx) error {
		return errors.New("next error")
	})

	req := httptest.NewRequest(fiber.MethodGet, "/", http.NoBody)
	req.Header.Set("Accept-Encoding", "gzip")

	resp, err := app.Test(req)
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 500, resp.StatusCode, "Status code")
	require.Empty(t, resp.Header.Get(fiber.HeaderContentEncoding))

	body, err := io.ReadAll(resp.Body)
	require.NoError(t, err)
	require.Equal(t, "next error", string(body))
}

// go test -run Test_Compress_Next
func Test_Compress_Next(t *testing.T) {
	t.Parallel()
	app := fiber.New()
	app.Use(New(Config{
		Next: func(_ fiber.Ctx) bool {
			return true
		},
	}))

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

// go test -bench=Benchmark_Compress
func Benchmark_Compress(b *testing.B) {
	tests := []struct {
		name           string
		acceptEncoding string
	}{
		{name: "Gzip", acceptEncoding: "gzip"},
		{name: "Deflate", acceptEncoding: "deflate"},
		{name: "Brotli", acceptEncoding: "br"},
		{name: "Zstd", acceptEncoding: "zstd"},
	}

	for _, tt := range tests {
		b.Run(tt.name, func(b *testing.B) {
			app := fiber.New()
			app.Use(New())
			app.Get("/", func(c fiber.Ctx) error {
				c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
				return c.Send(filedata)
			})

			h := app.Handler()
			fctx := &fasthttp.RequestCtx{}
			fctx.Request.Header.SetMethod(fiber.MethodGet)
			fctx.Request.SetRequestURI("/")

			if tt.acceptEncoding != "" {
				fctx.Request.Header.Set("Accept-Encoding", tt.acceptEncoding)
			}

			b.ReportAllocs()

			for b.Loop() {
				h(fctx)
			}
		})
	}
}

// go test -bench=Benchmark_Compress_Levels
func Benchmark_Compress_Levels(b *testing.B) {
	tests := []struct {
		name           string
		acceptEncoding string
	}{
		{name: "Gzip", acceptEncoding: "gzip"},
		{name: "Deflate", acceptEncoding: "deflate"},
		{name: "Brotli", acceptEncoding: "br"},
		{name: "Zstd", acceptEncoding: "zstd"},
	}

	levels := []struct {
		name  string
		level Level
	}{
		{name: "LevelDisabled", level: LevelDisabled},
		{name: "LevelDefault", level: LevelDefault},
		{name: "LevelBestSpeed", level: LevelBestSpeed},
		{name: "LevelBestCompression", level: LevelBestCompression},
	}

	for _, tt := range tests {
		for _, lvl := range levels {
			b.Run(tt.name+"_"+lvl.name, func(b *testing.B) {
				app := fiber.New()
				app.Use(New(Config{Level: lvl.level}))
				app.Get("/", func(c fiber.Ctx) error {
					c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
					return c.Send(filedata)
				})

				h := app.Handler()
				fctx := &fasthttp.RequestCtx{}
				fctx.Request.Header.SetMethod(fiber.MethodGet)
				fctx.Request.SetRequestURI("/")

				if tt.acceptEncoding != "" {
					fctx.Request.Header.Set("Accept-Encoding", tt.acceptEncoding)
				}

				b.ReportAllocs()

				for b.Loop() {
					h(fctx)
				}
			})
		}
	}
}

// go test -bench=Benchmark_Compress_Parallel
func Benchmark_Compress_Parallel(b *testing.B) {
	tests := []struct {
		name           string
		acceptEncoding string
	}{
		{name: "Gzip", acceptEncoding: "gzip"},
		{name: "Deflate", acceptEncoding: "deflate"},
		{name: "Brotli", acceptEncoding: "br"},
		{name: "Zstd", acceptEncoding: "zstd"},
	}

	for _, tt := range tests {
		b.Run(tt.name, func(b *testing.B) {
			app := fiber.New()
			app.Use(New())
			app.Get("/", func(c fiber.Ctx) error {
				c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
				return c.Send(filedata)
			})

			h := app.Handler()

			b.ReportAllocs()
			b.ResetTimer()

			b.RunParallel(func(pb *testing.PB) {
				fctx := &fasthttp.RequestCtx{}
				fctx.Request.Header.SetMethod(fiber.MethodGet)
				fctx.Request.SetRequestURI("/")

				if tt.acceptEncoding != "" {
					fctx.Request.Header.Set("Accept-Encoding", tt.acceptEncoding)
				}

				for pb.Next() {
					h(fctx)
				}
			})
		})
	}
}

// go test -bench=Benchmark_Compress_Levels_Parallel
func Benchmark_Compress_Levels_Parallel(b *testing.B) {
	tests := []struct {
		name           string
		acceptEncoding string
	}{
		{name: "Gzip", acceptEncoding: "gzip"},
		{name: "Deflate", acceptEncoding: "deflate"},
		{name: "Brotli", acceptEncoding: "br"},
		{name: "Zstd", acceptEncoding: "zstd"},
	}

	levels := []struct {
		name  string
		level Level
	}{
		{name: "LevelDisabled", level: LevelDisabled},
		{name: "LevelDefault", level: LevelDefault},
		{name: "LevelBestSpeed", level: LevelBestSpeed},
		{name: "LevelBestCompression", level: LevelBestCompression},
	}

	for _, tt := range tests {
		for _, lvl := range levels {
			b.Run(tt.name+"_"+lvl.name, func(b *testing.B) {
				app := fiber.New()
				app.Use(New(Config{Level: lvl.level}))
				app.Get("/", func(c fiber.Ctx) error {
					c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
					return c.Send(filedata)
				})

				h := app.Handler()

				b.ReportAllocs()
				b.ResetTimer()

				b.RunParallel(func(pb *testing.PB) {
					fctx := &fasthttp.RequestCtx{}
					fctx.Request.Header.SetMethod(fiber.MethodGet)
					fctx.Request.SetRequestURI("/")

					if tt.acceptEncoding != "" {
						fctx.Request.Header.Set("Accept-Encoding", tt.acceptEncoding)
					}

					for pb.Next() {
						h(fctx)
					}
				})
			})
		}
	}
}

type failOnReadStream struct {
	readCalls  int
	closeCalls int
}

func (s *failOnReadStream) Read(_ []byte) (int, error) {
	s.readCalls++
	return 0, errors.New("stream should not be read")
}

func (s *failOnReadStream) Close() error {
	s.closeCalls++
	return nil
}

func Test_Compress_ShouldSkip_DoesNotMaterialize_BodyStream(t *testing.T) {
	t.Parallel()
	app := fiber.New()
	fctx := &fasthttp.RequestCtx{}
	fctx.Request.Header.SetMethod(fiber.MethodGet)

	c := app.AcquireCtx(fctx)
	t.Cleanup(func() {
		app.ReleaseCtx(c)
	})

	stream := &failOnReadStream{}
	c.Response().SetStatusCode(fiber.StatusOK)
	c.Response().SetBodyStream(stream, -1)

	require.False(t, shouldSkip(c))
	require.True(t, c.Response().IsBodyStream())
	require.Zero(t, stream.readCalls)
	require.Zero(t, stream.closeCalls)
}

// go test -run Test_Compress_Streaming_With_Compression
func Test_Compress_Streaming_With_Compression(t *testing.T) {
	t.Parallel()
	app := fiber.New()

	app.Use(New())

	app.Get("/stream", func(c fiber.Ctx) error {
		// Send a stream when the client explicitly requests compression.
		return c.SendStream(bytes.NewReader(filedata))
	})

	req := httptest.NewRequest(fiber.MethodGet, "/stream", http.NoBody)
	req.Header.Set("Accept-Encoding", "gzip")

	resp, err := app.Test(req, testConfig)
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 200, resp.StatusCode, "Status code")
	require.Equal(t, "gzip", resp.Header.Get(fiber.HeaderContentEncoding))

	// Validate that the response is compressed
	body, err := io.ReadAll(resp.Body)
	require.NoError(t, err)
	require.Less(t, len(body), len(filedata), "Compressed size should be smaller than original")
}
