package static

import (
	"embed"
	"io"
	"io/fs"
	"net/http"
	"net/http/httptest"
	"net/url"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/gofiber/fiber/v3"
)

const (
	winOS      = "windows"
	testCSSDir = "../../.github/testdata/fs/css"
)

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

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

	app.Get("/prefix", New("../../.github/workflows"))

	app.Get("", New("../../.github/"))

	app.Get("test", New("", Config{
		IndexNames: []string{"index.html"},
	}))

	resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", http.NoBody))
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 200, resp.StatusCode, "Status code")
	require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))
	require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))

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

	resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/not-found", http.NoBody))
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 404, resp.StatusCode, "Status code")
	require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))
	require.Equal(t, fiber.MIMETextPlainCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))

	body, err = io.ReadAll(resp.Body)
	require.NoError(t, err)
	require.Equal(t, "Not Found", string(body))
}

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

	app.Get("/*", New("../../.github"))

	resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/index.html", http.NoBody))
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 200, resp.StatusCode, "Status code")
	require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))
	require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))

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

	resp, err = app.Test(httptest.NewRequest(fiber.MethodPost, "/index.html", http.NoBody))
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 405, resp.StatusCode, "Status code")
	require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))
	require.Equal(t, fiber.MIMETextPlainCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))

	resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/testdata/testRoutes.json", http.NoBody))
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 200, resp.StatusCode, "Status code")
	require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))
	require.Equal(t, fiber.MIMEApplicationJSON, resp.Header.Get("Content-Type"))
	require.Empty(t, resp.Header.Get(fiber.HeaderCacheControl), "CacheControl Control")

	body, err = io.ReadAll(resp.Body)
	require.NoError(t, err)
	require.Contains(t, string(body), "test_routes")
}

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

	app.Get("/*", New("../../.github", Config{
		MaxAge: 100,
	}))

	resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/index.html", http.NoBody))
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 200, resp.StatusCode, "Status code")
	require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))
	require.Equal(t, "text/html; charset=utf-8", resp.Header.Get(fiber.HeaderContentType))
	require.Equal(t, "public, max-age=100", resp.Header.Get(fiber.HeaderCacheControl), "CacheControl Control")
}

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

	app.Get("/*", New("../../.github", Config{
		ModifyResponse: func(c fiber.Ctx) error {
			if strings.Contains(c.GetRespHeader("Content-Type"), "text/html") {
				c.Response().Header.Set("Cache-Control", "no-cache, no-store, must-revalidate")
			}
			return nil
		},
	}))

	resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/index.html", http.NoBody))
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, "no-cache, no-store, must-revalidate", resp.Header.Get(fiber.HeaderCacheControl), "CacheControl Control")

	normalResp, normalErr := app.Test(httptest.NewRequest(fiber.MethodGet, "/config.yml", http.NoBody))
	require.NoError(t, normalErr, "app.Test(req)")
	require.Empty(t, normalResp.Header.Get(fiber.HeaderCacheControl), "CacheControl Control")
}

func Test_Static_Disable_Cache(t *testing.T) {
	// Skip on Windows. It's not possible to delete a file that is in use.
	if runtime.GOOS == winOS {
		t.SkipNow()
	}

	t.Parallel()

	app := fiber.New()

	file, err := os.Create("../../.github/test.txt")
	require.NoError(t, err)
	_, err = file.WriteString("Hello, World!")
	require.NoError(t, err)
	require.NoError(t, file.Close())

	// Remove the file even if the test fails
	defer func() {
		_ = os.Remove("../../.github/test.txt") //nolint:errcheck // not needed
	}()

	app.Get("/*", New("../../.github/", Config{
		CacheDuration: -1,
	}))

	resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/test.txt", http.NoBody))
	require.NoError(t, err, "app.Test(req)")
	require.Empty(t, resp.Header.Get(fiber.HeaderCacheControl), "CacheControl Control")

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

	require.NoError(t, os.Remove("../../.github/test.txt"))

	resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/test.txt", http.NoBody))
	require.NoError(t, err, "app.Test(req)")
	require.Empty(t, resp.Header.Get(fiber.HeaderCacheControl), "CacheControl Control")

	body, err = io.ReadAll(resp.Body)
	require.NoError(t, err)
	require.Equal(t, "Not Found", string(body))
}

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

	app.Get("/*", New("../../.github", Config{
		NotFoundHandler: func(c fiber.Ctx) error {
			return c.SendString("Custom 404")
		},
	}))

	resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/not-found", http.NoBody))
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 404, resp.StatusCode, "Status code")
	require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))
	require.Equal(t, fiber.MIMETextPlainCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))

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

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

	app.Get("/fiber.png", New("../../.github/testdata/fs/img/fiber.png", Config{
		Download: true,
	}))

	resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/fiber.png", http.NoBody))
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 200, resp.StatusCode, "Status code")
	require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))
	require.Equal(t, "image/png", resp.Header.Get(fiber.HeaderContentType))
	require.Equal(t, `attachment; filename="fiber.png"`, resp.Header.Get(fiber.HeaderContentDisposition))
}

func Test_Static_Download_NonASCII(t *testing.T) {
	// Skip on Windows. It's not possible to delete a file that is in use.
	if runtime.GOOS == "windows" {
		t.SkipNow()
	}

	t.Parallel()

	dir := t.TempDir()
	fname := "файл.txt"
	path := filepath.Join(dir, fname)
	require.NoError(t, os.WriteFile(path, []byte("x"), 0o644)) //nolint:gosec // Not a concern

	app := fiber.New()
	app.Get("/file", New(path, Config{Download: true}))

	resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/file", http.NoBody))
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 200, resp.StatusCode, "Status code")
	expect := "attachment; filename=\"" + fname + "\"; filename*=UTF-8''" + url.PathEscape(fname)
	require.Equal(t, expect, resp.Header.Get(fiber.HeaderContentDisposition))
}

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

	grp := app.Group("/v1", func(c fiber.Ctx) error {
		c.Set("Test-Header", "123")
		return c.Next()
	})

	grp.Get("/v2*", New("../../.github/index.html"))

	req := httptest.NewRequest(fiber.MethodGet, "/v1/v2", http.NoBody)
	resp, err := app.Test(req)
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 200, resp.StatusCode, "Status code")
	require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))
	require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))
	require.Equal(t, "123", resp.Header.Get("Test-Header"))

	grp = app.Group("/v2")
	grp.Get("/v3*", New("../../.github/index.html"))

	req = httptest.NewRequest(fiber.MethodGet, "/v2/v3/john/doe", http.NoBody)
	resp, err = app.Test(req)
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 200, resp.StatusCode, "Status code")
	require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))
	require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))
}

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

	app.Get("*", New("../../.github/index.html"))

	req := httptest.NewRequest(fiber.MethodGet, "/yesyes/john/doe", http.NoBody)
	resp, err := app.Test(req)
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 200, resp.StatusCode, "Status code")
	require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))
	require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))

	body, err := io.ReadAll(resp.Body)
	require.NoError(t, err)
	require.Contains(t, string(body), "Test file")
}

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

	app.Get("/test*", New("../../.github/index.html"))

	req := httptest.NewRequest(fiber.MethodGet, "/test/john/doe", http.NoBody)
	resp, err := app.Test(req)
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 200, resp.StatusCode, "Status code")
	require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))
	require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))

	app.Get("/my/nameisjohn*", New("../../.github/index.html"))

	resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/my/nameisjohn/no/its/not", http.NoBody))
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 200, resp.StatusCode, "Status code")
	require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))
	require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))

	body, err := io.ReadAll(resp.Body)
	require.NoError(t, err)
	require.Contains(t, string(body), "Test file")
}

func Test_Static_Prefix(t *testing.T) {
	t.Parallel()
	app := fiber.New()
	app.Get("/john*", New("../../.github"))

	req := httptest.NewRequest(fiber.MethodGet, "/john/index.html", http.NoBody)
	resp, err := app.Test(req)
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 200, resp.StatusCode, "Status code")
	require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))
	require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))

	app.Get("/prefix*", New("../../.github/testdata"))

	req = httptest.NewRequest(fiber.MethodGet, "/prefix/index.html", http.NoBody)
	resp, err = app.Test(req)
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 200, resp.StatusCode, "Status code")
	require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))
	require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))

	app.Get("/single*", New("../../.github/testdata/testRoutes.json"))

	req = httptest.NewRequest(fiber.MethodGet, "/single", http.NoBody)
	resp, err = app.Test(req)
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 200, resp.StatusCode, "Status code")
	require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))
	require.Equal(t, fiber.MIMEApplicationJSON, resp.Header.Get(fiber.HeaderContentType))
}

func Test_Static_Trailing_Slash(t *testing.T) {
	t.Parallel()
	app := fiber.New()
	app.Get("/john*", New("../../.github"))

	req := httptest.NewRequest(fiber.MethodGet, "/john/", http.NoBody)
	resp, err := app.Test(req)
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 200, resp.StatusCode, "Status code")
	require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))
	require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))

	app.Get("/john_without_index*", New(testCSSDir))

	req = httptest.NewRequest(fiber.MethodGet, "/john_without_index/", http.NoBody)
	resp, err = app.Test(req)
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 404, resp.StatusCode, "Status code")
	require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))
	require.Equal(t, fiber.MIMETextPlainCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))

	app.Use("/john", New("../../.github"))

	req = httptest.NewRequest(fiber.MethodGet, "/john/", http.NoBody)
	resp, err = app.Test(req)
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 200, resp.StatusCode, "Status code")
	require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))
	require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))

	req = httptest.NewRequest(fiber.MethodGet, "/john", http.NoBody)
	resp, err = app.Test(req)
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 200, resp.StatusCode, "Status code")
	require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))
	require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))

	app.Use("/john_without_index/", New(testCSSDir))

	req = httptest.NewRequest(fiber.MethodGet, "/john_without_index/", http.NoBody)
	resp, err = app.Test(req)
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 404, resp.StatusCode, "Status code")
	require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))
	require.Equal(t, fiber.MIMETextPlainCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))
}

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

	app.Get("/*", New("../../.github", Config{
		Next: func(c fiber.Ctx) bool {
			return c.Get("X-Custom-Header") == "skip"
		},
	}))

	app.Get("/*", func(c fiber.Ctx) error {
		return c.SendString("You've skipped app.Static")
	})

	t.Run("app.Static is skipped: invoking Get handler", func(t *testing.T) {
		t.Parallel()
		req := httptest.NewRequest(fiber.MethodGet, "/", http.NoBody)
		req.Header.Set("X-Custom-Header", "skip")
		resp, err := app.Test(req)
		require.NoError(t, err)
		require.Equal(t, 200, resp.StatusCode)
		require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))
		require.Equal(t, fiber.MIMETextPlainCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))

		body, err := io.ReadAll(resp.Body)
		require.NoError(t, err)
		require.Contains(t, string(body), "You've skipped app.Static")
	})

	t.Run("app.Static is not skipped: serving index.html", func(t *testing.T) {
		t.Parallel()
		req := httptest.NewRequest(fiber.MethodGet, "/", http.NoBody)
		req.Header.Set("X-Custom-Header", "don't skip")
		resp, err := app.Test(req)
		require.NoError(t, err)
		require.Equal(t, 200, resp.StatusCode)
		require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))
		require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))

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

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

	dir := testCSSDir
	app := fiber.New()
	app.Get("/*", New(dir, Config{
		Browse: true,
	}))

	resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", http.NoBody))
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 200, resp.StatusCode, "Status code")

	resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/style.css", http.NoBody))
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 200, resp.StatusCode, "Status code")

	body, err := io.ReadAll(resp.Body)
	require.NoError(t, err, "app.Test(req)")
	require.Contains(t, string(body), "color")

	app = fiber.New()
	app.Get("/*", New(dir))

	resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/", http.NoBody))
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 404, resp.StatusCode, "Status code")

	resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/style.css", http.NoBody))
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 200, resp.StatusCode, "Status code")

	body, err = io.ReadAll(resp.Body)
	require.NoError(t, err, "app.Test(req)")
	require.Contains(t, string(body), "color")
}

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

	dir := testCSSDir
	app := fiber.New()
	app.Get("/static*", New(dir, Config{
		Browse: true,
	}))

	resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/static", http.NoBody))
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 200, resp.StatusCode, "Status code")

	resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/static/", http.NoBody))
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 200, resp.StatusCode, "Status code")

	resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/static/style.css", http.NoBody))
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 200, resp.StatusCode, "Status code")

	body, err := io.ReadAll(resp.Body)
	require.NoError(t, err, "app.Test(req)")
	require.Contains(t, string(body), "color")

	app = fiber.New()
	app.Get("/static/*", New(dir, Config{
		Browse: true,
	}))

	resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/static", http.NoBody))
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 200, resp.StatusCode, "Status code")

	resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/static/", http.NoBody))
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 200, resp.StatusCode, "Status code")

	resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/static/style.css", http.NoBody))
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 200, resp.StatusCode, "Status code")

	body, err = io.ReadAll(resp.Body)
	require.NoError(t, err, "app.Test(req)")
	require.Contains(t, string(body), "color")

	app = fiber.New()
	app.Get("/static*", New(dir))

	resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/static", http.NoBody))
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 404, resp.StatusCode, "Status code")

	resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/static/", http.NoBody))
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 404, resp.StatusCode, "Status code")

	resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/static/style.css", http.NoBody))
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 200, resp.StatusCode, "Status code")

	body, err = io.ReadAll(resp.Body)
	require.NoError(t, err, "app.Test(req)")
	require.Contains(t, string(body), "color")

	app = fiber.New()
	app.Get("/static*", New(dir))

	resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/static", http.NoBody))
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 404, resp.StatusCode, "Status code")

	resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/static/", http.NoBody))
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 404, resp.StatusCode, "Status code")

	resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/static/style.css", http.NoBody))
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 200, resp.StatusCode, "Status code")

	body, err = io.ReadAll(resp.Body)
	require.NoError(t, err, "app.Test(req)")
	require.Contains(t, string(body), "color")
}

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

	app := fiber.New()
	app.Get("/*", New("", Config{
		FS:     os.DirFS("../../.github/testdata/fs"),
		Browse: true,
	}))

	resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", http.NoBody))
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 200, resp.StatusCode, "Status code")
	require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))

	resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/css/style.css", http.NoBody))
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 200, resp.StatusCode, "Status code")
	require.Equal(t, fiber.MIMETextCSSCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))

	body, err := io.ReadAll(resp.Body)
	require.NoError(t, err, "app.Test(req)")
	require.Contains(t, string(body), "color")
}

/*func Test_Static_FS_DifferentRoot(t *testing.T) {
	t.Parallel()

	app := fiber.New()
	app.Get("/*", New("fs", Config{
		FS:         os.DirFS("../../.github/testdata"),
		IndexNames: []string{"index2.html"},
		Browse:     true,
	}))

	resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 200, resp.StatusCode, "Status code")
	require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))

	body, err := io.ReadAll(resp.Body)
	require.NoError(t, err, "app.Test(req)")
	require.Contains(t, string(body), "<h1>Hello, World!</h1>")

	resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/css/style.css", nil))
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 200, resp.StatusCode, "Status code")
	require.Equal(t, fiber.MIMETextCSSCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))

	body, err = io.ReadAll(resp.Body)
	require.NoError(t, err, "app.Test(req)")
	require.Contains(t, string(body), "color")
}*/

//go:embed static.go config.go
var fsTestFilesystem embed.FS

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

	app := fiber.New()

	app.Get("/embed*", New("", Config{
		FS:     fsTestFilesystem,
		Browse: true,
	}))

	app.Get("/dirfs*", New("", Config{
		FS:     os.DirFS(testCSSDir),
		Browse: true,
	}))

	resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/dirfs", http.NoBody))
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 200, resp.StatusCode, "Status code")
	require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))

	body, err := io.ReadAll(resp.Body)
	require.NoError(t, err, "app.Test(req)")
	require.Contains(t, string(body), "style.css")

	resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/dirfs/style.css", http.NoBody))
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 200, resp.StatusCode, "Status code")
	require.Equal(t, fiber.MIMETextCSSCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))

	body, err = io.ReadAll(resp.Body)
	require.NoError(t, err, "app.Test(req)")
	require.Contains(t, string(body), "color")

	resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/dirfs/test", http.NoBody))
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 200, resp.StatusCode, "Status code")
	require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))

	resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/dirfs/test/style2.css", http.NoBody))
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 200, resp.StatusCode, "Status code")
	require.Equal(t, fiber.MIMETextCSSCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))

	body, err = io.ReadAll(resp.Body)
	require.NoError(t, err, "app.Test(req)")
	require.Contains(t, string(body), "color")

	resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/embed", http.NoBody))
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 200, resp.StatusCode, "Status code")
	require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))

	body, err = io.ReadAll(resp.Body)
	require.NoError(t, err, "app.Test(req)")
	require.Contains(t, string(body), "static.go")
}

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

	app.Get("/test*", New("index.html", Config{
		FS:         os.DirFS("../../.github"),
		IndexNames: []string{"not_index.html"},
	}))

	req := httptest.NewRequest(fiber.MethodGet, "/test/john/doe", http.NoBody)
	resp, err := app.Test(req)
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 200, resp.StatusCode, "Status code")
	require.NotEmpty(t, resp.Header.Get(fiber.HeaderContentLength))
	require.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))

	body, err := io.ReadAll(resp.Body)
	require.NoError(t, err)
	require.Contains(t, string(body), "Test file")
}

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

	cases := []struct {
		filesystem fs.FS
		gotError   error
		name       string
		path       string
		expected   bool
	}{
		{
			name:       "file",
			path:       "index.html",
			filesystem: os.DirFS("../../.github"),
			expected:   true,
		},
		{
			name:       "file",
			path:       "index2.html",
			filesystem: os.DirFS("../../.github"),
			expected:   false,
			gotError:   fs.ErrNotExist,
		},
		{
			name:       "directory",
			path:       ".",
			filesystem: os.DirFS("../../.github"),
			expected:   false,
		},
		{
			name:       "directory",
			path:       "not_exists",
			filesystem: os.DirFS("../../.github"),
			expected:   false,
			gotError:   fs.ErrNotExist,
		},
		{
			name:       "directory",
			path:       ".",
			filesystem: os.DirFS(testCSSDir),
			expected:   false,
		},
		{
			name:       "file",
			path:       testCSSDir + "/style.css",
			filesystem: nil,
			expected:   true,
		},
		{
			name:       "file",
			path:       testCSSDir + "/style2.css",
			filesystem: nil,
			expected:   false,
			gotError:   fs.ErrNotExist,
		},
		{
			name:       "directory",
			path:       testCSSDir,
			filesystem: nil,
			expected:   false,
		},
	}

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

			actual, err := isFile(c.path, c.filesystem)
			require.ErrorIs(t, err, c.gotError)
			require.Equal(t, c.expected, actual)
		})
	}
}

func Test_Static_Compress(t *testing.T) {
	t.Parallel()
	dir := "../../.github/testdata/fs"
	app := fiber.New()
	app.Get("/*", New(dir, Config{
		Compress: true,
	}))

	// Note: deflate is not supported by fasthttp.FS
	algorithms := []string{"zstd", "gzip", "br"}

	for _, algo := range algorithms {
		t.Run(algo+"_compression", func(t *testing.T) {
			t.Parallel()
			// request non-compressible file (less than 200 bytes), Content Length will remain the same
			req := httptest.NewRequest(fiber.MethodGet, "/css/style.css", 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.Empty(t, resp.Header.Get(fiber.HeaderContentEncoding))
			require.Equal(t, "46", resp.Header.Get(fiber.HeaderContentLength))

			// request compressible file, ContentLength will change
			req = httptest.NewRequest(fiber.MethodGet, "/index.html", 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))
			require.Greater(t, "299", resp.Header.Get(fiber.HeaderContentLength))
		})
	}
}

func Test_Static_Compress_WithoutEncoding(t *testing.T) {
	t.Parallel()
	dir := "../../.github/testdata/fs"
	app := fiber.New()
	app.Get("/*", New(dir, Config{
		Compress:      true,
		CacheDuration: 1 * time.Second,
	}))

	// request compressible file without encoding
	req := httptest.NewRequest(fiber.MethodGet, "/index.html", http.NoBody)
	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))
	require.Equal(t, "299", resp.Header.Get(fiber.HeaderContentLength))

	// request compressible file with different encodings
	algorithms := []string{"zstd", "gzip", "br"}
	fileSuffixes := map[string]string{
		"gzip": ".fiber.gz",
		"br":   ".fiber.br",
		"zstd": ".fiber.zst",
	}

	for _, algo := range algorithms {
		// Wait for cache to expire
		time.Sleep(2 * time.Second)
		fileName := "index.html"
		compressedFileName := dir + "/index.html" + fileSuffixes[algo]

		req = httptest.NewRequest(fiber.MethodGet, "/"+fileName, 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))
		require.Greater(t, "299", resp.Header.Get(fiber.HeaderContentLength))

		// verify suffixed file was created
		_, err := os.Stat(compressedFileName)
		require.NoError(t, err, "File should exist")
	}
}

func Test_Static_Compress_WithFileSuffixes(t *testing.T) {
	t.Parallel()
	dir := "../../.github/testdata/fs"
	fileSuffixes := map[string]string{
		"gzip": ".test.gz",
		"br":   ".test.br",
		"zstd": ".test.zst",
	}

	app := fiber.New(fiber.Config{
		CompressedFileSuffixes: fileSuffixes,
	})
	app.Get("/*", New(dir, Config{
		Compress:      true,
		CacheDuration: 1 * time.Second,
	}))

	// request compressible file with different encodings
	algorithms := []string{"zstd", "gzip", "br"}

	for _, algo := range algorithms {
		// Wait for cache to expire
		time.Sleep(2 * time.Second)
		fileName := "index.html"
		compressedFileName := dir + "/index.html" + fileSuffixes[algo]

		req := httptest.NewRequest(fiber.MethodGet, "/"+fileName, 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))
		require.Greater(t, "299", resp.Header.Get(fiber.HeaderContentLength))

		// verify suffixed file was created
		_, err = os.Stat(compressedFileName)
		require.NoError(t, err, "File should exist")
	}
}

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

	app := fiber.New()

	app.Use("/static", New(testCSSDir, Config{Browse: true}))
	app.Get("/", func(c fiber.Ctx) error {
		return c.SendString("Home")
	})

	subApp := fiber.New()
	app.Use("/mount", subApp)
	subApp.Get("/test", func(c fiber.Ctx) error {
		return c.SendString("Hello from /test")
	})

	app.Use(func(c fiber.Ctx) error {
		return c.Status(fiber.StatusNotFound).SendString("Not Found")
	})

	resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/static/style.css", http.NoBody))
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 200, resp.StatusCode, "Status code")
}

func Test_Static_PathTraversal(t *testing.T) {
	// Skip this test if running on Windows
	if runtime.GOOS == winOS {
		t.Skip("Skipping Windows-specific tests")
	}

	t.Parallel()
	app := fiber.New()

	// Serve only from testCSSDir
	// This directory should contain `style.css` but not `index.html` or anything above it.
	rootDir := testCSSDir
	app.Get("/*", New(rootDir))

	// A valid request: should succeed
	validReq := httptest.NewRequest(fiber.MethodGet, "/style.css", http.NoBody)
	validResp, err := app.Test(validReq)
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 200, validResp.StatusCode, "Status code")
	require.Equal(t, fiber.MIMETextCSSCharsetUTF8, validResp.Header.Get(fiber.HeaderContentType))
	validBody, err := io.ReadAll(validResp.Body)
	require.NoError(t, err, "app.Test(req)")
	require.Contains(t, string(validBody), "color")

	// Helper function to assert that a given path is blocked.
	// Blocked can mean different status codes depending on what triggered the block.
	// We'll accept 400 or 404 as "blocked" statuses:
	// - 404 is the expected blocked response in most cases.
	// - 400 might occur if fasthttp rejects the request before it's even processed (e.g., null bytes).
	assertTraversalBlocked := func(path string) {
		req := httptest.NewRequest(fiber.MethodGet, path, http.NoBody)
		resp, err := app.Test(req)
		require.NoError(t, err, "app.Test(req)")

		status := resp.StatusCode
		require.Truef(t, status == 400 || status == 404,
			"Status code for path traversal %s should be 400 or 404, got %d", path, status)

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

		// If we got a 404, we expect the "Not Found" message because that's how fiber handles NotFound by default.
		if status == 404 {
			require.Contains(t, string(body), "Not Found",
				"Blocked traversal should have a \"Not Found\" message for %s", path)
		} else {
			require.Contains(t, string(body), "Are you a hacker?",
				"Blocked traversal should have a \"Not Found\" message for %s", path)
		}
	}

	// Basic attempts to escape the directory
	assertTraversalBlocked("/index.html..")
	assertTraversalBlocked("/style.css..")
	assertTraversalBlocked("/../index.html")
	assertTraversalBlocked("/../../index.html")
	assertTraversalBlocked("/../../../index.html")

	// Attempts with double slashes
	assertTraversalBlocked("//../index.html")
	assertTraversalBlocked("/..//index.html")

	// Encoded attempts: `%2e` is '.' and `%2f` is '/'
	assertTraversalBlocked("/..%2findex.html")        // ../index.html
	assertTraversalBlocked("/%2e%2e/index.html")      // ../index.html
	assertTraversalBlocked("/%2e%2e%2f%2e%2e/secret") // ../../../secret

	// Mixed encoded and normal attempts
	assertTraversalBlocked("/%2e%2e/../index.html")  // ../../index.html
	assertTraversalBlocked("/..%2f..%2fsecret.json") // ../../../secret.json

	// Attempts with current directory references
	assertTraversalBlocked("/./../index.html")
	assertTraversalBlocked("/././../index.html")

	// Trailing slashes
	assertTraversalBlocked("/../")
	assertTraversalBlocked("/../../")

	// Attempts to load files from an absolute path outside the root
	assertTraversalBlocked("/" + rootDir + "/../../index.html")

	// Additional edge cases:

	// Double-encoded `..`
	assertTraversalBlocked("/%252e%252e/index.html") // double-encoded .. -> ../index.html after double decoding

	// Multiple levels of encoding and traversal
	assertTraversalBlocked("/%2e%2e%2F..%2f%2e%2e%2fWINDOWS")       // multiple ups and unusual pattern
	assertTraversalBlocked("/%2e%2e%2F..%2f%2e%2e%2f%2e%2e/secret") // more complex chain of ../

	// Null byte attempts
	assertTraversalBlocked("/index.html%00.jpg")
	assertTraversalBlocked("/%00index.html")
	assertTraversalBlocked("/somefolder%00/something")
	assertTraversalBlocked("/%00/index.html")

	// Attempts to access known system files
	assertTraversalBlocked("/etc/passwd")
	assertTraversalBlocked("/etc/")

	// Complex mixed attempts with encoded slashes and dots
	assertTraversalBlocked("/..%2F..%2F..%2F..%2Fetc%2Fpasswd")

	// Attempts inside subdirectories with encoded traversal
	assertTraversalBlocked("/somefolder/%2e%2e%2findex.html")
	assertTraversalBlocked("/somefolder/%2e%2e%2f%2e%2e%2findex.html")

	// Backslash encoded attempts
	assertTraversalBlocked("/%5C..%5Cindex.html")
	assertTraversalBlocked("/%5c..%5c..%5cetc%5cpasswd")
	assertTraversalBlocked("/%255c..%255c..%255cetc%255cpasswd")
	assertTraversalBlocked("/..%5c..%5cetc%5cpasswd")
	assertTraversalBlocked("/%2e%2e%5c%2e%2e%5cetc%5cpasswd")
	assertTraversalBlocked("/%2e%2e%2f%2e%2e%5cetc%5cpasswd")
	assertTraversalBlocked("/%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd")
	assertTraversalBlocked("/.%2e/.%2e/etc/passwd")
	assertTraversalBlocked("/..%2f..%2f..%2f..%2fetc%2fpasswd")
	assertTraversalBlocked("/%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd")
	assertTraversalBlocked("/%2e%2e%2f%2e%2e%2fetc%2fshadow")
	assertTraversalBlocked("/%2e%2e/%2e%2e/var/log/auth.log")
	assertTraversalBlocked("/..%2f..%2fvar%2flog%2fauth.log")
	assertTraversalBlocked("/%2e%2e//%2e%2e//etc/passwd")
	assertTraversalBlocked("/..//..//etc/passwd")
	assertTraversalBlocked("/%2e%2e%2f%2e%2e%2f%2e%2e%2fproc%2fself%2fenviron")
	assertTraversalBlocked("/%2e%2e%2f%2e%2e%2f%2e%2e%2froot%2f.ssh%2fauthorized_keys")
}

func Test_Static_PathTraversal_WindowsOnly(t *testing.T) {
	// Skip this test if not running on Windows
	if runtime.GOOS != winOS {
		t.Skip("Skipping Windows-specific tests")
	}

	t.Parallel()
	app := fiber.New()

	// Serve only from testCSSDir
	rootDir := testCSSDir
	app.Get("/*", New(rootDir))

	// A valid request (relative path without backslash):
	validReq := httptest.NewRequest(fiber.MethodGet, "/style.css", http.NoBody)
	validResp, err := app.Test(validReq)
	require.NoError(t, err, "app.Test(req)")
	require.Equal(t, 200, validResp.StatusCode, "Status code for valid file on Windows")
	body, err := io.ReadAll(validResp.Body)
	require.NoError(t, err, "app.Test(req)")
	require.Contains(t, string(body), "color")

	// Helper to test blocked responses
	assertTraversalBlocked := func(path string) {
		req := httptest.NewRequest(fiber.MethodGet, path, http.NoBody)
		resp, err := app.Test(req)
		require.NoError(t, err, "app.Test(req)")

		// We expect a blocked request to return either 400 or 404
		status := resp.StatusCode
		require.Containsf(t, []int{400, 404}, status,
			"Status code for path traversal %s should be 400 or 404, got %d", path, status)

		// If it's a 404, we expect a "Not Found" message
		if status == 404 {
			respBody, err := io.ReadAll(resp.Body)
			require.NoError(t, err)
			require.Contains(t, string(respBody), "Not Found",
				"Blocked traversal should have a \"Not Found\" message for %s", path)
		} else {
			require.Contains(t, string(body), "Are you a hacker?",
				"Blocked traversal should have a \"Not Found\" message for %s", path)
		}
	}

	// Windows-specific traversal attempts
	// Backslashes are treated as directory separators on Windows.
	assertTraversalBlocked("/..\\index.html")
	assertTraversalBlocked("/..\\..\\index.html")
	assertTraversalBlocked("/..\\..\\..\\Windows\\win.ini")
	assertTraversalBlocked("/..\\..\\..\\Windows\\System32\\drivers\\etc\\hosts")
	assertTraversalBlocked("/%5C..%5C..%5CWindows%5Cwin.ini")
	assertTraversalBlocked("/%255C..%255C..%255CWindows%255Cwin.ini")
	assertTraversalBlocked("/%5c..%5c..%5cWindows%5cSystem32%5cdrivers%5cetc%5chosts")
	assertTraversalBlocked("/C:\\Windows\\System32\\cmd.exe")
	assertTraversalBlocked("/C:%5CWindows%5CSystem32%5Ccmd.exe")
	assertTraversalBlocked("/%43:%5CWindows%5CSystem32%5Ccmd.exe")
	assertTraversalBlocked("/%5c%5cserver%5cshare%5csecret.txt")
	assertTraversalBlocked("//server\\share\\secret.txt")
	assertTraversalBlocked("//server/share/secret.txt")
	assertTraversalBlocked("/%2F%2Fserver%2Fshare%2Fsecret.txt")

	// Attempt with a path that might try to reference Windows drives or absolute paths
	// Note: These are artificial tests to ensure no drive-letter escapes are allowed.
	assertTraversalBlocked("/C:\\Windows\\System32\\cmd.exe")
	assertTraversalBlocked("/C:/Windows/System32/cmd.exe")

	// Attempt with UNC-like paths (though unlikely in a web context, good to test)
	assertTraversalBlocked("//server\\share\\secret.txt")

	// Attempt using a mixture of forward and backward slashes
	assertTraversalBlocked("/..\\..\\/index.html")

	// Attempt that includes a null-byte on Windows
	assertTraversalBlocked("/index.html%00.txt")

	// Check behavior on an obviously nonexistent and suspicious file
	assertTraversalBlocked("/\\this\\path\\does\\not\\exist\\..")

	// Attempts involving relative traversal and current directory reference
	assertTraversalBlocked("/.\\../index.html")
	assertTraversalBlocked("/./..\\index.html")
}

func Benchmark_SanitizePath(b *testing.B) {
	bench := func(name string, filesystem fs.FS, path []byte) {
		b.Run(name, func(b *testing.B) {
			b.ReportAllocs()
			for b.Loop() {
				if _, err := sanitizePath(path, filesystem); err != nil {
					b.Fatal(err)
				}
			}
		})
	}

	bench("nilFS - urlencoded chars", nil, []byte("/foo%2Fbar/../baz%20qux/index.html"))
	bench("dirFS - urlencoded chars", os.DirFS("."), []byte("/foo%2Fbar/../baz%20qux/index.html"))
	bench("nilFS - slashes", nil, []byte("\\foo%2Fbar\\baz%20qux\\index.html"))
}

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

	type testCase struct {
		filesystem fs.FS
		name       string
		expectPath string
		input      []byte
	}

	testCases := []testCase{
		{name: "simple path", input: []byte("/foo/bar.txt"), expectPath: "/foo/bar.txt"},
		{name: "traversal attempt", input: []byte("/foo/../../bar.txt"), expectPath: "/bar.txt"},
		{name: "encoded traversal", input: []byte("/foo/%2e%2e/bar.txt"), expectPath: "/bar.txt"},
		{name: "double encoded traversal", input: []byte("/%252e%252e/bar.txt"), expectPath: "/bar.txt"},
		{name: "current dir reference", input: []byte("/foo/./bar.txt"), expectPath: "/foo/bar.txt"},
		{name: "encoded slash", input: []byte("/foo%2Fbar.txt"), expectPath: "/foo/bar.txt"},
		{name: "empty path", input: []byte(""), expectPath: "/"},
		{name: "dot segments", input: []byte("/foo/./bar/../baz.txt"), expectPath: "/foo/baz.txt"},
		{name: "leading dot segment", input: []byte("/./foo/bar.txt"), expectPath: "/foo/bar.txt"},
		{name: "encoded space", input: []byte("/foo%20bar/baz.txt"), expectPath: "/foo bar/baz.txt"},
		{name: "encoded plus literal", input: []byte("/foo+bar/baz.txt"), expectPath: "/foo+bar/baz.txt"},
		// windows-specific paths
		{name: "backslash path", input: []byte("\\foo\\bar.txt"), expectPath: "/foo/bar.txt"},
		{name: "backslash traversal", input: []byte("\\foo\\..\\..\\bar.txt"), expectPath: "/bar.txt"},
		{name: "mixed slashes", input: []byte("/foo\\bar.txt"), expectPath: "/foo/bar.txt"},
		{name: "trailing slash preserved", input: []byte("/foo/bar/"), expectPath: "/foo/bar/"},
		{name: "encoded trailing slash", input: []byte("/foo/bar%2F"), expectPath: "/foo/bar"},
		{filesystem: os.DirFS("."), name: "filesystem empty path", input: []byte(""), expectPath: "/"},
		{filesystem: os.DirFS("."), name: "filesystem trailing slash", input: []byte("/foo/"), expectPath: "/foo/"},
		{filesystem: os.DirFS("."), name: "filesystem traversal clean", input: []byte("/foo/../bar.txt"), expectPath: "/bar.txt"},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			got, err := sanitizePath(tc.input, tc.filesystem)
			require.NoError(t, err)
			require.Equal(t, tc.expectPath, string(got))
		})
	}
}

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

	type testCase struct {
		filesystem fs.FS
		name       string
		input      []byte
	}

	testCases := []testCase{
		{name: "null byte", input: []byte("/foo/bar.txt%00")},
		{name: "encoded backslash traversal", input: []byte("/foo%5C..%5Cbar.txt")},
		{name: "double encoded backslash traversal", input: []byte("/%255C..%255C..%255CWindows%255Cwin.ini")},
		{name: "encoded backslash absolute", input: []byte("/%5CWindows%5CSystem32%5Cdrivers%5Cetc%5Chosts")},
		{name: "double encoded backslash absolute", input: []byte("/%255CWindows%255CSystem32%255Cdrivers%255Cetc%255Chosts")},
		{name: "encoded backslash mixed slashes", input: []byte("/..%5C..%5Cetc%5Cpasswd")},
		{name: "encoded backslash mixed encoding", input: []byte("/%2e%2e%5c%2e%2e%5cetc%5cpasswd")},
		{name: "encoded backslash with encoded slash", input: []byte("/%2e%2e%2f%2e%2e%5cetc%5cpasswd")},
		{name: "encoded backslash unc path", input: []byte("//server%5Cshare%5Csecret.txt")},
		{name: "encoded backslash drive letter", input: []byte("/C:%5CWindows%5CSystem32%5Ccmd.exe")},
		{name: "double slash path", input: []byte("//foo//bar.txt")},
		{name: "drive letter", input: []byte("C:/Windows/System32/cmd.exe")},
		{name: "drive letter with leading slash", input: []byte("/C:/Windows/System32/cmd.exe")},
		{name: "encoded drive letter", input: []byte("/%43:%5CWindows%5CSystem32%5Ccmd.exe")},
		{name: "unc path", input: []byte("//server/share/secret.txt")},
		{name: "encoded unc path", input: []byte("/%2F%2Fserver%2Fshare%2Fsecret.txt")},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			_, err := sanitizePath(tc.input, tc.filesystem)
			require.ErrorIs(t, err, ErrInvalidPath, "Expected ErrInvalidPath for input: %s", tc.input)
		})
	}
}
