package fasthttp

import (
	"bufio"
	"bytes"
	"embed"
	"io/fs"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"testing"
	"testing/fstest"
	"time"
)

//go:embed fasthttputil fs.go README.md testdata examples
var fsTestFilesystem embed.FS

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

	var ctx RequestCtx
	var req Request
	req.Header.SetMethod(MethodHead)
	req.SetRequestURI("http://foobar.com/baz")
	ctx.Init(&req, nil, nil)

	ServeFS(&ctx, fsTestFilesystem, "fs.go")

	var resp Response
	resp.SkipBody = true
	s := ctx.Response.String()
	br := bufio.NewReader(bytes.NewBufferString(s))
	if err := resp.Read(br); err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	ce := resp.Header.ContentEncoding()
	if len(ce) > 0 {
		t.Fatalf("Unexpected 'Content-Encoding' %q", ce)
	}

	body := resp.Body()
	if len(body) > 0 {
		t.Fatalf("unexpected response body %q. Expecting empty body", body)
	}

	expectedBody, err := getFileContents("/fs.go")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	contentLength := resp.Header.ContentLength()
	if contentLength != len(expectedBody) {
		t.Fatalf("unexpected Content-Length: %d. expecting %d", contentLength, len(expectedBody))
	}
}

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

	testFS := fstest.MapFS{
		"space name.txt":     {Data: []byte("space")},
		"hash#name.txt":      {Data: []byte("hash")},
		"percent%61name.txt": {Data: []byte("percent")},
		"query?name.txt":     {Data: []byte("query")},
	}

	for name, file := range testFS {
		t.Run(name, func(t *testing.T) {
			var ctx RequestCtx
			var req Request
			req.SetRequestURI("http://foobar.com/original")
			ctx.Init(&req, nil, nil)

			ServeFSLiteral(&ctx, testFS, name)

			resp := readResponseFromCtx(t, &ctx, false)
			if resp.StatusCode() != StatusOK {
				t.Fatalf("unexpected status code %d. expecting %d", resp.StatusCode(), StatusOK)
			}
			if !bytes.Equal(resp.Body(), file.Data) {
				t.Fatalf("unexpected body %q. expecting %q", resp.Body(), file.Data)
			}
		})
	}
}

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

	testFS := fstest.MapFS{
		"hash#name.txt":      {Data: []byte("hash")},
		"percent%61name.txt": {Data: []byte("percent")},
		"query?name.txt":     {Data: []byte("query")},
	}

	for name := range testFS {
		t.Run(name, func(t *testing.T) {
			var ctx RequestCtx
			var req Request
			req.SetRequestURI("http://foobar.com/original")
			ctx.Init(&req, nil, TestLogger{t})

			ServeFS(&ctx, testFS, name)

			resp := readResponseFromCtx(t, &ctx, false)
			if resp.StatusCode() == StatusOK {
				t.Fatalf("ServeFS should fail for special-character path %q without literal mode, but got 200", name)
			}
		})
	}
}

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

	var ctx RequestCtx
	ctx.Init(&Request{}, nil, nil)

	var resp Response

	expectedBody, err := getFileContents("/fs.go")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	// should prefer brotli over zstd, gzip and ignore unknown encoding
	ctx.Request.SetRequestURI("http://foobar.com/baz")
	ctx.Request.Header.Set(HeaderAcceptEncoding, "gzip, zstd, br, wompwomp")
	ServeFS(&ctx, fsTestFilesystem, "fs.go")

	s := ctx.Response.String()
	br := bufio.NewReader(bytes.NewBufferString(s))
	if err = resp.Read(br); err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if resp.StatusCode() != StatusOK {
		t.Fatalf("unexpected status code: %d. Expecting %d", resp.StatusCode(), StatusOK)
	}

	ce := resp.Header.ContentEncoding()
	if string(ce) != "br" {
		t.Fatalf("unexpected 'Content-Encoding' %q. Expecting %q", string(ce), "br")
	}

	vary := resp.Header.PeekBytes(strVary)
	if !bytes.Equal(vary, strAcceptEncoding) {
		t.Fatalf("unexpected 'Vary': %q. Expecting %q", string(vary), HeaderAcceptEncoding)
	}

	body, err := resp.BodyUnbrotli()
	if err != nil {
		t.Fatalf("unexpected error on unbrotli response body: %v", err)
	}
	if !bytes.Equal(body, expectedBody) {
		t.Fatalf("unexpected body: len=%d. Expected len=%d", len(body), len(expectedBody))
	}

	// should prefer zstd over gzip and ignore unknown encoding
	ctx.Request.Reset()
	ctx.Request.SetRequestURI("http://foobar.com/baz")
	ctx.Request.Header.Set(HeaderAcceptEncoding, "gzip, zstd, wompwomp")
	ServeFS(&ctx, fsTestFilesystem, "fs.go")

	s = ctx.Response.String()
	br = bufio.NewReader(bytes.NewBufferString(s))
	if err = resp.Read(br); err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if resp.StatusCode() != StatusOK {
		t.Fatalf("unexpected status code: %d. Expecting %d", resp.StatusCode(), StatusOK)
	}

	ce = resp.Header.ContentEncoding()
	if string(ce) != "zstd" {
		t.Fatalf("unexpected 'Content-Encoding' %q. Expecting %q", string(ce), "zstd")
	}

	vary = resp.Header.PeekBytes(strVary)
	if !bytes.Equal(vary, strAcceptEncoding) {
		t.Fatalf("unexpected 'Vary': %q. Expecting %q", string(vary), HeaderAcceptEncoding)
	}

	body, err = resp.BodyUnzstd()
	if err != nil {
		t.Fatalf("unexpected error on unzstd response body: %v", err)
	}
	if !bytes.Equal(body, expectedBody) {
		t.Fatalf("unexpected body: len=%d. Expected len=%d", len(body), len(expectedBody))
	}

	// should prefer gzip and ignore unknown encoding
	ctx.Request.Reset()
	ctx.Request.SetRequestURI("http://foobar.com/baz")
	ctx.Request.Header.Set(HeaderAcceptEncoding, "gzip, wompwomp")
	ServeFS(&ctx, fsTestFilesystem, "fs.go")

	s = ctx.Response.String()
	br = bufio.NewReader(bytes.NewBufferString(s))
	if err = resp.Read(br); err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if resp.StatusCode() != StatusOK {
		t.Fatalf("unexpected status code: %d. Expecting %d", resp.StatusCode(), StatusOK)
	}

	ce = resp.Header.ContentEncoding()
	if string(ce) != "gzip" {
		t.Fatalf("unexpected 'Content-Encoding' %q. Expecting %q", string(ce), "gzip")
	}

	vary = resp.Header.PeekBytes(strVary)
	if !bytes.Equal(vary, strAcceptEncoding) {
		t.Fatalf("unexpected 'Vary': %q. Expecting %q", string(vary), HeaderAcceptEncoding)
	}

	body, err = resp.BodyGunzip()
	if err != nil {
		t.Fatalf("unexpected error on gunzip response body: %v", err)
	}
	if !bytes.Equal(body, expectedBody) {
		t.Fatalf("unexpected body: len=%d. Expected len=%d", len(body), len(expectedBody))
	}
}

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

	var ctx RequestCtx
	ctx.Init(&Request{}, nil, nil)

	var resp Response

	expectedBody, err := getFileContents("/fs.go")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	ctx.Request.SetRequestURI("http://foobar.com/baz")
	ctx.Request.Header.Set(HeaderAcceptEncoding, "gzip, zstd, br, wompwomp")
	ServeFileUncompressed(&ctx, "fs.go")

	s := ctx.Response.String()
	br := bufio.NewReader(bytes.NewBufferString(s))
	if err = resp.Read(br); err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if resp.StatusCode() != StatusOK {
		t.Fatalf("unexpected status code: %d. Expecting %d", resp.StatusCode(), StatusOK)
	}

	ce := resp.Header.ContentEncoding()
	if len(ce) > 0 {
		t.Fatalf("unexpected 'Content-Encoding': %q. Expecting \"\"", string(ce))
	}

	vary := resp.Header.PeekBytes(strVary)
	if len(vary) > 0 {
		t.Fatalf("unexpected 'Vary': %q. Expecting \"\"", string(vary))
	}

	body := resp.Body()
	if !bytes.Equal(body, expectedBody) {
		t.Fatalf("unexpected body: len=%d. Expecting len=%d", len(body), len(expectedBody))
	}
}

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

	stop := make(chan struct{})
	defer close(stop)

	fs := &FS{
		FS:              fsTestFilesystem,
		Root:            "",
		AcceptByteRange: true,
		CleanStop:       stop,
	}
	h := fs.NewRequestHandler()

	concurrency := 10
	ch := make(chan struct{}, concurrency)
	for range concurrency {
		go func() {
			for range 5 {
				testFSByteRange(t, h, "/fs.go")
				testFSByteRange(t, h, "/README.md")
			}
			ch <- struct{}{}
		}()
	}

	for range concurrency {
		select {
		case <-time.After(time.Second):
			t.Fatalf("timeout")
		case <-ch:
		}
	}
}

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

	stop := make(chan struct{})
	defer close(stop)

	fs := &FS{
		FS:              fsTestFilesystem,
		Root:            ".",
		AcceptByteRange: true,
		CleanStop:       stop,
	}
	h := fs.NewRequestHandler()

	testFSByteRange(t, h, "/fs.go")
	testFSByteRange(t, h, "/README.md")
}

func TestFSFSCompressConcurrent(t *testing.T) {
	t.Parallel()
	// go 1.16 timeout may occur
	if strings.HasPrefix(runtime.Version(), "go1.16") {
		t.SkipNow()
	}

	stop := make(chan struct{})
	defer close(stop)

	fs := &FS{
		FS:                 fsTestFilesystem,
		Root:               ".",
		GenerateIndexPages: true,
		Compress:           true,
		CompressBrotli:     true,
		CompressZstd:       true,
		CleanStop:          stop,
	}
	h := fs.NewRequestHandler()

	concurrency := 4
	ch := make(chan struct{}, concurrency)
	for range concurrency {
		go func() {
			for range 5 {
				testFSFSCompress(t, h, "/fs.go")
				testFSFSCompress(t, h, "/examples/")
				testFSFSCompress(t, h, "/README.md")
			}
			ch <- struct{}{}
		}()
	}

	for range concurrency {
		select {
		case <-ch:
		case <-time.After(time.Second * 4):
			t.Fatalf("timeout")
		}
	}
}

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

	stop := make(chan struct{})
	defer close(stop)

	fs := &FS{
		FS:                 fsTestFilesystem,
		Root:               ".",
		GenerateIndexPages: true,
		Compress:           true,
		CompressBrotli:     true,
		CompressZstd:       true,
		CleanStop:          stop,
	}
	h := fs.NewRequestHandler()

	testFSFSCompress(t, h, "/fs.go")
	testFSFSCompress(t, h, "/examples/")
	testFSFSCompress(t, h, "/README.md")
}

func testFSFSCompress(t *testing.T, h RequestHandler, filePath string) {
	var ctx RequestCtx
	ctx.Init(&Request{}, nil, nil)

	var resp Response

	// get uncompressed
	ctx.Request.SetRequestURI(filePath)
	h(&ctx)

	s := ctx.Response.String()
	br := bufio.NewReader(bytes.NewBufferString(s))
	if err := resp.Read(br); err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if resp.StatusCode() != StatusOK {
		t.Fatalf("unexpected status code: %d. Expecting %d", resp.StatusCode(), StatusOK)
	}

	ce := resp.Header.ContentEncoding()
	if len(ce) > 0 {
		t.Fatalf("unexpected 'Content-Encoding': %q. Expecting \"\"", string(ce))
	}

	vary := resp.Header.PeekBytes(strVary)
	if len(vary) > 0 {
		t.Fatalf("unexpected 'Vary': %q. Expecting \"\"", string(vary))
	}

	expectedBody := bytes.Clone(resp.Body())

	// should prefer brotli over zstd, gzip and ignore unknown encoding
	ctx.Request.Reset()
	ctx.Request.SetRequestURI(filePath)
	ctx.Request.Header.Set(HeaderAcceptEncoding, "gzip, zstd, br, wompwomp")
	h(&ctx)

	s = ctx.Response.String()
	br = bufio.NewReader(bytes.NewBufferString(s))
	if err := resp.Read(br); err != nil {
		t.Fatalf("unexpected error: %v. filePath=%q", err, filePath)
	}
	if resp.StatusCode() != StatusOK {
		t.Fatalf("unexpected status code: %d. Expecting %d. filePath=%q", resp.StatusCode(), StatusOK, filePath)
	}

	ce = resp.Header.ContentEncoding()
	if string(ce) != "br" {
		t.Fatalf("unexpected 'Content-Encoding' %q. Expecting %q. filePath=%q", string(ce), "br", filePath)
	}

	vary = resp.Header.PeekBytes(strVary)
	if !bytes.Equal(vary, strAcceptEncoding) {
		t.Fatalf("unexpected 'Vary': %q. Expecting %q", string(vary), HeaderAcceptEncoding)
	}

	body, err := resp.BodyUnbrotli()
	if err != nil {
		t.Fatalf("unexpected error on unbrotli response body: %v. filePath=%q", err, filePath)
	}
	if !bytes.Equal(body, expectedBody) {
		t.Fatalf("unexpected body: len=%d. Expecting len=%d. filePath=%q", len(body), len(expectedBody), filePath)
	}

	// should prefer zstd over gzip and ignore unknown encoding
	ctx.Request.Reset()
	ctx.Request.SetRequestURI(filePath)
	ctx.Request.Header.Set(HeaderAcceptEncoding, "gzip, zstd, wompwomp")
	h(&ctx)

	s = ctx.Response.String()
	br = bufio.NewReader(bytes.NewBufferString(s))
	if err = resp.Read(br); err != nil {
		t.Fatalf("unexpected error: %v. filePath=%q", err, filePath)
	}
	if resp.StatusCode() != StatusOK {
		t.Fatalf("unexpected status code: %d. Expecting %d. filePath=%q", resp.StatusCode(), StatusOK, filePath)
	}

	ce = resp.Header.ContentEncoding()
	if string(ce) != "zstd" {
		t.Fatalf("unexpected 'Content-Encoding' %q. Expecting %q. filePath=%q", string(ce), "zstd", filePath)
	}

	vary = resp.Header.PeekBytes(strVary)
	if !bytes.Equal(vary, strAcceptEncoding) {
		t.Fatalf("unexpected 'Vary': %q. Expecting %q", string(vary), HeaderAcceptEncoding)
	}

	body, err = resp.BodyUnzstd()
	if err != nil {
		t.Fatalf("unexpected error on unzstd response body: %v. filePath=%q", err, filePath)
	}
	if !bytes.Equal(body, expectedBody) {
		t.Fatalf("unexpected body: len=%d. Expecting len=%d. filePath=%q", len(body), len(expectedBody), filePath)
	}

	// should prefer gzip and ignore unknown encoding
	ctx.Request.Reset()
	ctx.Request.SetRequestURI(filePath)
	ctx.Request.Header.Set(HeaderAcceptEncoding, "gzip, wompwomp")
	h(&ctx)

	s = ctx.Response.String()
	br = bufio.NewReader(bytes.NewBufferString(s))
	if err = resp.Read(br); err != nil {
		t.Fatalf("unexpected error: %v. filePath=%q", err, filePath)
	}
	if resp.StatusCode() != StatusOK {
		t.Fatalf("unexpected status code: %d. Expecting %d. filePath=%q", resp.StatusCode(), StatusOK, filePath)
	}

	ce = resp.Header.ContentEncoding()
	if string(ce) != "gzip" {
		t.Fatalf("unexpected 'Content-Encoding' %q. Expecting %q. filePath=%q", string(ce), "gzip", filePath)
	}

	vary = resp.Header.PeekBytes(strVary)
	if !bytes.Equal(vary, strAcceptEncoding) {
		t.Fatalf("unexpected 'Vary': %q. Expecting %q", string(vary), HeaderAcceptEncoding)
	}

	body, err = resp.BodyGunzip()
	if err != nil {
		t.Fatalf("unexpected error on gunzip response body: %v. filePath=%q", err, filePath)
	}
	if !bytes.Equal(body, expectedBody) {
		t.Fatalf("unexpected body: len=%d. Expecting len=%d. filePath=%q", len(body), len(expectedBody), filePath)
	}
}

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

	var ctx RequestCtx
	var req Request
	req.Header.SetMethod(MethodGet)
	req.SetRequestURI("http://foobar.com/baz")
	ctx.Init(&req, nil, nil)

	ServeFS(&ctx, fsTestFilesystem, "testdata/test.png")

	var resp Response
	s := ctx.Response.String()
	br := bufio.NewReader(bytes.NewBufferString(s))
	if err := resp.Read(br); err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	expected := []byte("image/png")
	if !bytes.Equal(resp.Header.ContentType(), expected) {
		t.Fatalf("Unexpected Content-Type, expected: %q got %q", expected, resp.Header.ContentType())
	}
}

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

	var ctx RequestCtx
	var req Request
	req.SetRequestURI("http://foobar.com")
	ctx.Init(&req, nil, nil)

	ctx.Request.Reset()
	ctx.Response.Reset()
	ServeFS(&ctx, fsTestFilesystem, "fasthttputil")
	if ctx.Response.StatusCode() != StatusFound {
		t.Fatalf("Unexpected status code %d for directory '/fasthttputil' without trailing slash. Expecting %d.", ctx.Response.StatusCode(), StatusFound)
	}

	ctx.Request.Reset()
	ctx.Response.Reset()
	ServeFS(&ctx, fsTestFilesystem, "fasthttputil/")
	if ctx.Response.StatusCode() != StatusOK {
		t.Fatalf("Unexpected status code %d for directory '/fasthttputil/' with trailing slash. Expecting %d.", ctx.Response.StatusCode(), StatusOK)
	}

	ctx.Request.Reset()
	ctx.Response.Reset()
	ServeFS(&ctx, fsTestFilesystem, "fs.go")
	if ctx.Response.StatusCode() != StatusOK {
		t.Fatalf("Unexpected status code %d for file '/fs.go'. Expecting %d.", ctx.Response.StatusCode(), StatusOK)
	}
}

var dirTestFilesystem = os.DirFS(".")

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

	var ctx RequestCtx
	var req Request
	req.Header.SetMethod(MethodHead)
	req.SetRequestURI("http://foobar.com/baz")
	ctx.Init(&req, nil, nil)

	ServeFS(&ctx, dirTestFilesystem, "fs.go")

	var resp Response
	resp.SkipBody = true
	s := ctx.Response.String()
	br := bufio.NewReader(bytes.NewBufferString(s))
	if err := resp.Read(br); err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	ce := resp.Header.ContentEncoding()
	if len(ce) > 0 {
		t.Fatalf("Unexpected 'Content-Encoding' %q", ce)
	}

	body := resp.Body()
	if len(body) > 0 {
		t.Fatalf("unexpected response body %q. Expecting empty body", body)
	}

	expectedBody, err := getFileContents("/fs.go")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	contentLength := resp.Header.ContentLength()
	if contentLength != len(expectedBody) {
		t.Fatalf("unexpected Content-Length: %d. expecting %d", contentLength, len(expectedBody))
	}
}

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

	var ctx RequestCtx
	ctx.Init(&Request{}, nil, nil)

	var resp Response

	expectedBody, err := getFileContents("/fs.go")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	// should prefer brotli over zstd, gzip and ignore unknown encoding
	ctx.Request.SetRequestURI("http://foobar.com/baz")
	ctx.Request.Header.Set(HeaderAcceptEncoding, "gzip, zstd, br, wompwomp")
	ServeFS(&ctx, dirTestFilesystem, "fs.go")

	s := ctx.Response.String()
	br := bufio.NewReader(bytes.NewBufferString(s))
	if err = resp.Read(br); err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if resp.StatusCode() != StatusOK {
		t.Fatalf("unexpected status code: %d. Expecting %d", resp.StatusCode(), StatusOK)
	}

	ce := resp.Header.ContentEncoding()
	if string(ce) != "br" {
		t.Fatalf("unexpected 'Content-Encoding' %q. Expecting %q", string(ce), "br")
	}

	vary := resp.Header.PeekBytes(strVary)
	if !bytes.Equal(vary, strAcceptEncoding) {
		t.Fatalf("unexpected 'Vary': %q. Expecting %q", string(vary), HeaderAcceptEncoding)
	}

	body, err := resp.BodyUnbrotli()
	if err != nil {
		t.Fatalf("unexpected error on unbrotli response body: %v", err)
	}
	if !bytes.Equal(body, expectedBody) {
		t.Fatalf("unexpected body: len=%d. Expecting len=%d", len(body), len(expectedBody))
	}

	// should prefer zstd over gzip and ignore unknown encoding
	ctx.Request.Reset()
	ctx.Request.SetRequestURI("http://foobar.com/baz")
	ctx.Request.Header.Set(HeaderAcceptEncoding, "gzip, zstd, wompwomp")
	ServeFS(&ctx, dirTestFilesystem, "fs.go")

	s = ctx.Response.String()
	br = bufio.NewReader(bytes.NewBufferString(s))
	if err = resp.Read(br); err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if resp.StatusCode() != StatusOK {
		t.Fatalf("unexpected status code: %d. Expecting %d", resp.StatusCode(), StatusOK)
	}

	ce = resp.Header.ContentEncoding()
	if string(ce) != "zstd" {
		t.Fatalf("unexpected 'Content-Encoding' %q. Expecting %q", string(ce), "zstd")
	}

	vary = resp.Header.PeekBytes(strVary)
	if !bytes.Equal(vary, strAcceptEncoding) {
		t.Fatalf("unexpected 'Vary': %q. Expecting %q", string(vary), HeaderAcceptEncoding)
	}

	body, err = resp.BodyUnzstd()
	if err != nil {
		t.Fatalf("unexpected error on unzstd response body: %v", err)
	}
	if !bytes.Equal(body, expectedBody) {
		t.Fatalf("unexpected body: len=%d. Expecting len=%d", len(body), len(expectedBody))
	}

	// should prefer gzip and ignore unknown encoding
	ctx.Request.Reset()
	ctx.Request.SetRequestURI("http://foobar.com/baz")
	ctx.Request.Header.Set(HeaderAcceptEncoding, "gzip, wompwomp")
	ServeFS(&ctx, dirTestFilesystem, "fs.go")

	s = ctx.Response.String()
	br = bufio.NewReader(bytes.NewBufferString(s))
	if err = resp.Read(br); err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if resp.StatusCode() != StatusOK {
		t.Fatalf("unexpected status code: %d. Expecting %d", resp.StatusCode(), StatusOK)
	}

	ce = resp.Header.ContentEncoding()
	if string(ce) != "gzip" {
		t.Fatalf("unexpected 'Content-Encoding' %q. Expecting %q", string(ce), "gzip")
	}

	vary = resp.Header.PeekBytes(strVary)
	if !bytes.Equal(vary, strAcceptEncoding) {
		t.Fatalf("unexpected 'Vary': %q. Expecting %q", string(vary), HeaderAcceptEncoding)
	}

	body, err = resp.BodyGunzip()
	if err != nil {
		t.Fatalf("unexpected error on gunzip response body: %v", err)
	}
	if !bytes.Equal(body, expectedBody) {
		t.Fatalf("unexpected body: len=%d. Expecting len=%d", len(body), len(expectedBody))
	}
}

func TestDirFSFSByteRangeConcurrent(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip()
	}

	t.Parallel()

	stop := make(chan struct{})
	defer close(stop)

	fs := &FS{
		FS:              dirTestFilesystem,
		Root:            "",
		AcceptByteRange: true,
		CleanStop:       stop,
	}
	h := fs.NewRequestHandler()

	concurrency := 10
	ch := make(chan struct{}, concurrency)
	for range concurrency {
		go func() {
			for range 5 {
				testFSByteRange(t, h, "/fs.go")
				testFSByteRange(t, h, "/README.md")
			}
			ch <- struct{}{}
		}()
	}

	for range concurrency {
		select {
		case <-time.After(time.Second):
			t.Fatalf("timeout")
		case <-ch:
		}
	}
}

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

	stop := make(chan struct{})
	defer close(stop)

	fs := &FS{
		FS:              dirTestFilesystem,
		Root:            ".",
		AcceptByteRange: true,
		CleanStop:       stop,
	}
	h := fs.NewRequestHandler()

	testFSByteRange(t, h, "/fs.go")
	testFSByteRange(t, h, "/README.md")
}

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

	stop := make(chan struct{})
	defer close(stop)

	fs := &FS{
		FS:                 dirTestFilesystem,
		Root:               ".",
		GenerateIndexPages: true,
		Compress:           true,
		CompressBrotli:     true,
		CompressZstd:       true,
		CleanStop:          stop,
	}
	h := fs.NewRequestHandler()

	concurrency := 4
	ch := make(chan struct{}, concurrency)
	for range concurrency {
		go func() {
			for range 5 {
				testFSFSCompress(t, h, "/fs.go")
				testFSFSCompress(t, h, "/examples/")
				testFSFSCompress(t, h, "/README.md")
			}
			ch <- struct{}{}
		}()
	}

	for range concurrency {
		select {
		case <-ch:
		case <-time.After(time.Second * 2):
			t.Fatalf("timeout")
		}
	}
}

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

	stop := make(chan struct{})
	defer close(stop)

	fs := &FS{
		FS:                 dirTestFilesystem,
		Root:               ".",
		GenerateIndexPages: true,
		Compress:           true,
		CompressBrotli:     true,
		CompressZstd:       true,
		CleanStop:          stop,
	}
	h := fs.NewRequestHandler()

	testFSFSCompress(t, h, "/fs.go")
	testFSFSCompress(t, h, "/examples/")
	testFSFSCompress(t, h, "/README.md")
}

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

	var ctx RequestCtx
	var req Request
	req.Header.SetMethod(MethodGet)
	req.SetRequestURI("http://foobar.com/baz")
	ctx.Init(&req, nil, nil)

	ServeFS(&ctx, dirTestFilesystem, "testdata/test.png")

	var resp Response
	s := ctx.Response.String()
	br := bufio.NewReader(bytes.NewBufferString(s))
	if err := resp.Read(br); err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	expected := []byte("image/png")
	if !bytes.Equal(resp.Header.ContentType(), expected) {
		t.Fatalf("Unexpected Content-Type, expected: %q got %q", expected, resp.Header.ContentType())
	}
}

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

	var ctx RequestCtx
	var req Request
	req.SetRequestURI("http://foobar.com")
	ctx.Init(&req, nil, nil)

	ctx.Request.Reset()
	ctx.Response.Reset()
	ServeFS(&ctx, dirTestFilesystem, "fasthttputil")
	if ctx.Response.StatusCode() != StatusFound {
		t.Fatalf("Unexpected status code %d for directory '/fasthttputil' without trailing slash. Expecting %d.", ctx.Response.StatusCode(), StatusFound)
	}

	ctx.Request.Reset()
	ctx.Response.Reset()
	ServeFS(&ctx, dirTestFilesystem, "fasthttputil/")
	if ctx.Response.StatusCode() != StatusOK {
		t.Fatalf("Unexpected status code %d for directory '/fasthttputil/' with trailing slash. Expecting %d.", ctx.Response.StatusCode(), StatusOK)
	}

	ctx.Request.Reset()
	ctx.Response.Reset()
	ServeFS(&ctx, dirTestFilesystem, "fs.go")
	if ctx.Response.StatusCode() != StatusOK {
		t.Fatalf("Unexpected status code %d for file '/fs.go'. Expecting %d.", ctx.Response.StatusCode(), StatusOK)
	}
}

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

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

		fs := &FS{
			FS:                 dirTestFilesystem,
			Root:               ".",
			GenerateIndexPages: true,
		}
		h := fs.NewRequestHandler()

		var ctx RequestCtx
		var req Request
		ctx.Init(&req, nil, nil)

		h(&ctx)

		cases := []string{"/", "//", ""}
		for _, c := range cases {
			ctx.Request.Reset()
			ctx.Response.Reset()

			req.Header.SetMethod(MethodGet)
			req.SetRequestURI("http://foobar.com" + c)
			h(&ctx)

			if ctx.Response.StatusCode() != StatusOK {
				t.Fatalf("unexpected status code %d for path %q. Expecting %d", ctx.Response.StatusCode(), ctx.Response.StatusCode(), StatusOK)
			}

			if !bytes.Contains(ctx.Response.Body(), []byte("fasthttputil")) {
				t.Fatalf("unexpected body %q. Expecting to contain %q", ctx.Response.Body(), "fasthttputil")
			}

			if !bytes.Contains(ctx.Response.Body(), []byte("fs.go")) {
				t.Fatalf("unexpected body %q. Expecting to contain %q", ctx.Response.Body(), "fs.go")
			}
		}
	})

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

		fs := &FS{
			FS:                 fsTestFilesystem,
			Root:               ".",
			GenerateIndexPages: true,
		}
		h := fs.NewRequestHandler()

		var ctx RequestCtx
		var req Request
		ctx.Init(&req, nil, nil)

		h(&ctx)

		cases := []string{"/", "//", ""}
		for _, c := range cases {
			ctx.Request.Reset()
			ctx.Response.Reset()

			req.Header.SetMethod(MethodGet)
			req.SetRequestURI("http://foobar.com" + c)
			h(&ctx)

			if ctx.Response.StatusCode() != StatusOK {
				t.Fatalf("unexpected status code %d for path %q. Expecting %d", ctx.Response.StatusCode(), ctx.Response.StatusCode(), StatusOK)
			}

			if !bytes.Contains(ctx.Response.Body(), []byte("fasthttputil")) {
				t.Fatalf("unexpected body %q. Expecting to contain %q", ctx.Response.Body(), "fasthttputil")
			}

			if !bytes.Contains(ctx.Response.Body(), []byte("fs.go")) {
				t.Fatalf("unexpected body %q. Expecting to contain %q", ctx.Response.Body(), "fs.go")
			}
		}
	})
}

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

	memFS := fstest.MapFS{
		"public/index.html":  {Data: []byte("<h1>Public</h1>")},
		"secret/admin.json":  {Data: []byte(`{"admin": true, "key": "s3cret"}`)},
		"public/nested/info": {Data: []byte("nested")},
	}

	// Skip dirfs subtests on Windows: the FS handler pools open file handles
	// (via bigFileReader) which prevents t.TempDir cleanup.
	// The mapfs subtests exercise the same root enforcement logic.
	var tmpDir string
	if runtime.GOOS != "windows" {
		tmpDir = t.TempDir()
		if err := os.MkdirAll(filepath.Join(tmpDir, "public"), 0o755); err != nil {
			t.Fatalf("cannot create public dir: %v", err)
		}
		if err := os.MkdirAll(filepath.Join(tmpDir, "secret"), 0o755); err != nil {
			t.Fatalf("cannot create secret dir: %v", err)
		}
		if err := os.WriteFile(filepath.Join(tmpDir, "public", "index.html"), []byte("<h1>Public</h1>"), 0o644); err != nil {
			t.Fatalf("cannot create public index: %v", err)
		}
		if err := os.WriteFile(filepath.Join(tmpDir, "secret", "admin.json"), []byte(`{"admin": true, "key": "s3cret"}`), 0o644); err != nil {
			t.Fatalf("cannot create secret admin file: %v", err)
		}
	}

	type testCase struct {
		name        string
		root        string
		filesystem  fs.FS
		pathRewrite PathRewriteFunc
	}

	cases := make([]testCase, 0, 9)
	for _, root := range []string{"public", "public/", "./public", "/public"} {
		cases = append(cases, testCase{
			name:       "mapfs/" + root,
			root:       root,
			filesystem: memFS,
		})
		if tmpDir != "" {
			cases = append(cases, testCase{
				name:       "dirfs/" + root,
				root:       root,
				filesystem: os.DirFS(tmpDir),
			})
		}
	}

	cases = append(cases, testCase{
		name:       "mapfs/pathrewrite-no-leading-slash",
		root:       "./public/",
		filesystem: memFS,
		pathRewrite: func(ctx *RequestCtx) []byte {
			return bytes.TrimPrefix(ctx.Path(), []byte("/"))
		},
	})

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			stop := make(chan struct{})
			defer close(stop)

			fs := &FS{
				Root:           tc.root,
				FS:             tc.filesystem,
				AllowEmptyRoot: true,
				CleanStop:      stop,
				PathRewrite:    tc.pathRewrite,
			}
			h := fs.NewRequestHandler()

			var ctx RequestCtx
			ctx.Init(&Request{}, nil, TestLogger{t: t})

			checkStatus := func(uri string, expected int) {
				ctx.Request.Reset()
				ctx.Response.Reset()
				ctx.Request.SetRequestURI(uri)
				h(&ctx)
				if ctx.Response.StatusCode() != expected {
					t.Fatalf("unexpected status code for %s: %d. Expecting %d", uri, ctx.Response.StatusCode(), expected)
				}
			}

			checkStatus("http://localhost/index.html", StatusOK)
			checkStatus("http://localhost/secret/admin.json", StatusNotFound)
		})
	}
}

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

	testCases := []struct {
		path string
		want bool
	}{
		{path: "", want: false},
		{path: ".", want: false},
		{path: "..", want: true},
		{path: "../secret.txt", want: true},
		{path: "/../secret.txt", want: true},
		{path: "nested/../info", want: true},
		{path: "nested/..", want: true},
		{path: "nested/..hidden/info", want: false},
		{path: "nested..", want: false},
		{path: "/index.html", want: false},
	}

	if filepath.Separator == '\\' {
		testCases = append(testCases,
			struct {
				path string
				want bool
			}{path: `..\secret.txt`, want: true},
			struct {
				path string
				want bool
			}{path: `nested\..\info`, want: true},
		)
	}

	for _, tc := range testCases {
		t.Run(tc.path, func(t *testing.T) {
			t.Parallel()

			if got := hasDotDotPathSegment([]byte(tc.path)); got != tc.want {
				t.Fatalf("unexpected result for %q: got %v want %v", tc.path, got, tc.want)
			}
		})
	}
}

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

	tmpDir := t.TempDir()
	publicDir := filepath.Join(tmpDir, "public")
	if err := os.MkdirAll(publicDir, 0o755); err != nil {
		t.Fatalf("cannot create public dir: %v", err)
	}
	if err := os.WriteFile(filepath.Join(publicDir, "index.html"), []byte("<h1>Public</h1>"), 0o644); err != nil {
		t.Fatalf("cannot create public index: %v", err)
	}
	secretPath := filepath.Join(tmpDir, "secret.txt")
	if err := os.WriteFile(secretPath, []byte("TOP_SECRET"), 0o644); err != nil {
		t.Fatalf("cannot create secret file: %v", err)
	}

	type testCase struct {
		name        string
		pathRewrite PathRewriteFunc
		requestURI  string
	}

	testCases := []testCase{
		{
			name:        "prefix-stripper-leading-dotdot",
			pathRewrite: NewPathPrefixStripper(len("/static/")),
			requestURI:  "http://localhost/aaaaaaa../secret.txt",
		},
		{
			name: "custom-leading-dotdot",
			pathRewrite: func(ctx *RequestCtx) []byte {
				return []byte("../secret.txt")
			},
			requestURI: "http://localhost/ignored",
		},
		{
			name: "custom-trailing-dotdot",
			pathRewrite: func(ctx *RequestCtx) []byte {
				return []byte("nested/..")
			},
			requestURI: "http://localhost/ignored",
		},
		{
			name: "custom-exact-dotdot",
			pathRewrite: func(ctx *RequestCtx) []byte {
				return []byte("..")
			},
			requestURI: "http://localhost/ignored",
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			stop := make(chan struct{})
			defer close(stop)

			fs := &FS{
				Root:           publicDir,
				AllowEmptyRoot: true,
				CleanStop:      stop,
				PathRewrite:    tc.pathRewrite,
			}
			h := fs.NewRequestHandler()

			var ctx RequestCtx
			ctx.Init(&Request{}, nil, TestLogger{t: t})
			ctx.Request.SetRequestURI(tc.requestURI)

			h(&ctx)

			if ctx.Response.StatusCode() != StatusInternalServerError {
				t.Fatalf("unexpected status code for %s: %d. Expecting %d", tc.name, ctx.Response.StatusCode(), StatusInternalServerError)
			}
			if bytes.Contains(ctx.Response.Body(), []byte("TOP_SECRET")) {
				t.Fatalf("unexpected secret disclosure for %s", tc.name)
			}
		})
	}
}
