// Package handler 提供静态文件处理器功能的测试。
//
// 该文件测试静态文件处理器模块的各项功能，包括：
//   - 正常文件访问
//   - 嵌套路径文件
//   - 目录索引文件
//   - 路径遍历安全检查
//   - 索引文件优先级
//   - 构造函数
//   - 文件缓存设置
//   - Gzip 静态文件设置
//   - HEAD 请求处理
//   - 预压缩文件支持
//   - 大文件处理
//   - 符号链接处理
//
// 作者：xfy
package handler

import (
	"os"
	"path/filepath"
	"sync"
	"testing"
	"time"

	"github.com/valyala/fasthttp"
	"rua.plus/lolly/internal/cache"
	"rua.plus/lolly/internal/testutil"
)

// newTestHandler 创建测试用的静态文件处理器。
//
// 参数：
//   - t: 测试上下文，用于标记 helper
//   - root: 静态文件根目录
//
// 返回值：
//   - *StaticHandler: 配置好索引文件的静态文件处理器（禁用 sendfile）
func newTestHandler(t *testing.T, root string) *StaticHandler {
	t.Helper()
	return NewStaticHandler(root, "/", []string{"index.html", "index.htm"}, false) // 测试时禁用 sendfile
}

// newTestContext 创建测试用的 fasthttp 请求上下文。
//
// 参数：
//   - t: 测试上下文，用于标记 helper
//   - path: 请求路径
//
// 返回值：
//   - *fasthttp.RequestCtx: 初始化好的请求上下文
func newTestContext(t *testing.T, path string) *fasthttp.RequestCtx {
	t.Helper()
	return testutil.NewRequestCtx("GET", path)
}

// TestStaticHandlerHandle 测试静态文件处理器
func TestStaticHandlerHandle(t *testing.T) {
	tests := []struct {
		setup       func(t *testing.T, root string)
		name        string
		path        string
		wantContent string
		wantStatus  int
		skipContent bool
	}{
		{
			name: "正常文件访问",
			setup: func(t *testing.T, root string) {
				t.Helper()
				content := "hello world"
				if err := os.WriteFile(filepath.Join(root, "test.txt"), []byte(content), 0o644); err != nil {
					t.Fatalf("创建测试文件失败: %v", err)
				}
			},
			path:        "/test.txt",
			wantStatus:  fasthttp.StatusOK,
			wantContent: "hello world",
		},
		{
			name: "嵌套路径文件",
			setup: func(t *testing.T, root string) {
				t.Helper()
				subDir := filepath.Join(root, "sub", "dir")
				if err := os.MkdirAll(subDir, 0o755); err != nil {
					t.Fatalf("创建子目录失败: %v", err)
				}
				content := "nested file content"
				if err := os.WriteFile(filepath.Join(subDir, "nested.txt"), []byte(content), 0o644); err != nil {
					t.Fatalf("创建嵌套文件失败: %v", err)
				}
			},
			path:        "/sub/dir/nested.txt",
			wantStatus:  fasthttp.StatusOK,
			wantContent: "nested file content",
		},
		{
			name: "目录带索引文件",
			setup: func(t *testing.T, root string) {
				t.Helper()
				dir := filepath.Join(root, "withindex")
				if err := os.MkdirAll(dir, 0o755); err != nil {
					t.Fatalf("创建目录失败: %v", err)
				}
				content := "index page"
				if err := os.WriteFile(filepath.Join(dir, "index.html"), []byte(content), 0o644); err != nil {
					t.Fatalf("创建索引文件失败: %v", err)
				}
			},
			path:        "/withindex/",
			wantStatus:  fasthttp.StatusOK,
			wantContent: "index page",
		},
		{
			name: "目录无索引文件",
			setup: func(t *testing.T, root string) {
				t.Helper()
				dir := filepath.Join(root, "noindex")
				if err := os.MkdirAll(dir, 0o755); err != nil {
					t.Fatalf("创建目录失败: %v", err)
				}
			},
			path:        "/noindex/",
			wantStatus:  fasthttp.StatusForbidden,
			skipContent: true,
		},
		{
			name: "文件不存在",
			setup: func(_ *testing.T, _ string) {
				t.Helper()
				// 不创建任何文件
			},
			path:        "/nonexistent.txt",
			wantStatus:  fasthttp.StatusNotFound,
			skipContent: true,
		},
		{
			name: "空路径访问根目录无索引",
			setup: func(_ *testing.T, _ string) {
				t.Helper()
				// root 目录没有索引文件
			},
			path:        "/",
			wantStatus:  fasthttp.StatusForbidden,
			skipContent: true,
		},
		{
			name: "根目录有索引文件",
			setup: func(t *testing.T, root string) {
				t.Helper()
				content := "root index"
				if err := os.WriteFile(filepath.Join(root, "index.html"), []byte(content), 0o644); err != nil {
					t.Fatalf("创建根索引文件失败: %v", err)
				}
			},
			path:        "/",
			wantStatus:  fasthttp.StatusOK,
			wantContent: "root index",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// 创建临时目录
			tmpDir := t.TempDir()

			// 设置测试文件
			tt.setup(t, tmpDir)

			// 创建处理器和上下文
			handler := newTestHandler(t, tmpDir)
			ctx := newTestContext(t, tt.path)

			// 执行请求
			handler.Handle(ctx)

			// 验证状态码
			if got := ctx.Response.StatusCode(); got != tt.wantStatus {
				t.Errorf("Handle() 状态码 = %d, want %d", got, tt.wantStatus)
			}

			// 验证内容（如果需要）
			if !tt.skipContent && tt.wantContent != "" {
				got := string(ctx.Response.Body())
				if got != tt.wantContent {
					t.Errorf("Handle() 内容 = %q, want %q", got, tt.wantContent)
				}
			}
		})
	}
}

// TestStaticHandlerHandle_PathTraversalSecurity 测试路径遍历安全检查
// 注意：fasthttp 会自动规范化路径，移除 ../ 组件
// 安全检查 strings.Contains(path, "..") 检测文件名中包含 ".." 的情况
func TestStaticHandlerHandle_PathTraversalSecurity(t *testing.T) {
	tests := []struct {
		setup       func(t *testing.T, root string)
		name        string
		path        string
		description string
		wantStatus  int
	}{
		{
			name: "文件名包含双点 - 安全检查拦截",
			setup: func(_ *testing.T, _ string) {
				t.Helper()
				// 不创建任何文件
			},
			path:        "/file..txt",
			wantStatus:  fasthttp.StatusForbidden,
			description: "路径包含 '..' 字符串，触发安全检查返回 403",
		},
		{
			name: "路径末尾双点 - 安全检查拦截",
			setup: func(_ *testing.T, _ string) {
				t.Helper()
			},
			path:        "/foo/..",
			wantStatus:  fasthttp.StatusForbidden,
			description: "路径末尾包含 '..'，触发安全检查返回 403",
		},
		{
			name: "隐藏文件 .hidden - 文件不存在",
			setup: func(_ *testing.T, _ string) {
				t.Helper()
			},
			path:        "/.hidden",
			wantStatus:  fasthttp.StatusNotFound,
			description: "单点开头的隐藏文件不触发安全检查，文件不存在返回 404",
		},
		{
			name: "文件名包含多点 ...txt - 安全检查拦截",
			setup: func(_ *testing.T, _ string) {
				t.Helper()
			},
			path:        "/file...txt",
			wantStatus:  fasthttp.StatusForbidden,
			description: "包含连续多点（含 '..'）触发安全检查返回 403",
		},
		{
			name: "fasthttp 规范化后的路径 - 文件不存在",
			setup: func(_ *testing.T, _ string) {
				t.Helper()
				// fasthttp 将 /../secret.txt 规范化为 /secret.txt
			},
			path:        "/../secret.txt",
			wantStatus:  fasthttp.StatusNotFound,
			description: "fasthttp 自动规范化路径移除 ../，结果路径文件不存在返回 404",
		},
		{
			name: "URL 编码路径遍历 - fasthttp 规范化",
			setup: func(_ *testing.T, _ string) {
				t.Helper()
				// fasthttp 解码 %2e%2e 为 .. 并规范化路径
			},
			path:        "/%2e%2e/secret.txt",
			wantStatus:  fasthttp.StatusNotFound,
			description: "fasthttp 解码 URL 编码后规范化路径，文件不存在返回 404",
		},
		{
			name: "混合 URL 编码 - fasthttp 规范化",
			setup: func(_ *testing.T, _ string) {
				t.Helper()
			},
			path:        "/%2e%2e%2fsecret.txt",
			wantStatus:  fasthttp.StatusNotFound,
			description: "fasthttp 解码并规范化路径，文件不存在返回 404",
		},
		{
			name: "路径中含 ../ - fasthttp 规范化",
			setup: func(t *testing.T, root string) {
				t.Helper()
				// 创建目标文件供测试
				if err := os.WriteFile(filepath.Join(root, "bar.txt"), []byte("bar"), 0o644); err != nil {
					t.Fatalf("创建文件失败: %v", err)
				}
			},
			path:        "/foo/../bar.txt",
			wantStatus:  fasthttp.StatusOK,
			description: "fasthttp 规范化 /foo/../bar.txt 为 /bar.txt，文件存在返回 200",
		},
	}

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

			handler := newTestHandler(t, tmpDir)
			ctx := newTestContext(t, tt.path)

			handler.Handle(ctx)

			if got := ctx.Response.StatusCode(); got != tt.wantStatus {
				t.Errorf("Handle() 状态码 = %d, want %d\n说明: %s", got, tt.wantStatus, tt.description)
			}
		})
	}
}

// TestStaticHandlerHandle_IndexFallback 测试索引文件优先级
func TestStaticHandlerHandle_IndexFallback(t *testing.T) {
	t.Run("优先 index.html", func(t *testing.T) {
		tmpDir := t.TempDir()
		dir := filepath.Join(tmpDir, "testdir")
		if err := os.MkdirAll(dir, 0o755); err != nil {
			t.Fatalf("创建目录失败: %v", err)
		}

		// 创建两个索引文件
		if err := os.WriteFile(filepath.Join(dir, "index.html"), []byte("html content"), 0o644); err != nil {
			t.Fatalf("创建 index.html 失败: %v", err)
		}
		if err := os.WriteFile(filepath.Join(dir, "index.htm"), []byte("htm content"), 0o644); err != nil {
			t.Fatalf("创建 index.htm 失败: %v", err)
		}

		handler := newTestHandler(t, tmpDir)
		ctx := newTestContext(t, "/testdir/")
		handler.Handle(ctx)

		if got := ctx.Response.StatusCode(); got != fasthttp.StatusOK {
			t.Errorf("状态码 = %d, want %d", got, fasthttp.StatusOK)
		}

		// 应返回 index.html 而非 index.htm
		got := string(ctx.Response.Body())
		if got != "html content" {
			t.Errorf("内容 = %q, want %q", got, "html content")
		}
	})

	t.Run("无 index.html 时使用 index.htm", func(t *testing.T) {
		tmpDir := t.TempDir()
		dir := filepath.Join(tmpDir, "testdir")
		if err := os.MkdirAll(dir, 0o755); err != nil {
			t.Fatalf("创建目录失败: %v", err)
		}

		// 仅创建 index.htm
		if err := os.WriteFile(filepath.Join(dir, "index.htm"), []byte("htm content"), 0o644); err != nil {
			t.Fatalf("创建 index.htm 失败: %v", err)
		}

		handler := newTestHandler(t, tmpDir)
		ctx := newTestContext(t, "/testdir/")
		handler.Handle(ctx)

		if got := ctx.Response.StatusCode(); got != fasthttp.StatusOK {
			t.Errorf("状态码 = %d, want %d", got, fasthttp.StatusOK)
		}

		got := string(ctx.Response.Body())
		if got != "htm content" {
			t.Errorf("内容 = %q, want %q", got, "htm content")
		}
	})

	t.Run("无索引文件时返回 403", func(t *testing.T) {
		tmpDir := t.TempDir()
		dir := filepath.Join(tmpDir, "testdir")
		if err := os.MkdirAll(dir, 0o755); err != nil {
			t.Fatalf("创建目录失败: %v", err)
		}

		// 创建一个非索引文件
		if err := os.WriteFile(filepath.Join(dir, "other.txt"), []byte("other content"), 0o644); err != nil {
			t.Fatalf("创建 other.txt 失败: %v", err)
		}

		handler := newTestHandler(t, tmpDir)
		ctx := newTestContext(t, "/testdir/")
		handler.Handle(ctx)

		if got := ctx.Response.StatusCode(); got != fasthttp.StatusForbidden {
			t.Errorf("状态码 = %d, want %d", got, fasthttp.StatusForbidden)
		}
	})

	t.Run("目录不带斜杠结尾", func(t *testing.T) {
		tmpDir := t.TempDir()
		dir := filepath.Join(tmpDir, "testdir")
		if err := os.MkdirAll(dir, 0o755); err != nil {
			t.Fatalf("创建目录失败: %v", err)
		}

		// 创建索引文件
		if err := os.WriteFile(filepath.Join(dir, "index.html"), []byte("index"), 0o644); err != nil {
			t.Fatalf("创建 index.html 失败: %v", err)
		}

		handler := newTestHandler(t, tmpDir)
		ctx := newTestContext(t, "/testdir") // 不带斜杠
		handler.Handle(ctx)

		// 目录不带斜杠也应该能访问索引文件
		if got := ctx.Response.StatusCode(); got != fasthttp.StatusOK {
			t.Errorf("状态码 = %d, want %d", got, fasthttp.StatusOK)
		}
	})
}

// TestNewStaticHandler 测试静态文件处理器构造函数
func TestNewStaticHandler(t *testing.T) {
	t.Run("正常创建", func(t *testing.T) {
		root := "/var/www"
		index := []string{"index.html", "index.htm"}
		handler := NewStaticHandler(root, "/", index, true)

		if handler == nil {
			t.Fatal("NewStaticHandler() 返回 nil")
		}
		// root 被规范化为带尾部斜杠的形式
		expectedRoot := "/var/www/"
		if handler.root != expectedRoot {
			t.Errorf("handler.root = %q, want %q", handler.root, expectedRoot)
		}
		if len(handler.index) != len(index) {
			t.Errorf("len(handler.index) = %d, want %d", len(handler.index), len(index))
		}
	})

	t.Run("空索引列表", func(t *testing.T) {
		handler := NewStaticHandler("/var/www", "/", nil, false)
		if handler == nil {
			t.Fatal("NewStaticHandler() 返回 nil")
		}
		if handler.index != nil {
			t.Errorf("handler.index 应为 nil")
		}
	})
}

// TestStaticHandler_SetFileCache 测试设置文件缓存
func TestStaticHandler_SetFileCache(t *testing.T) {
	handler := NewStaticHandler("/var/www", "/", nil, false)

	// 设置 nil 缓存
	handler.SetFileCache(nil)
	if handler.fileCache != nil {
		t.Error("Expected nil fileCache")
	}

	// 设置非 nil 缓存（使用 mock 或简单验证）
	// 由于 FileCache 接口需要实现，这里主要验证不会 panic
}

// TestStaticHandler_SetGzipStatic 测试设置 Gzip 静态文件
func TestStaticHandler_SetGzipStatic(t *testing.T) {
	handler := NewStaticHandler("/var/www", "/", nil, false)

	// 启用 gzip
	handler.SetGzipStatic(true, nil, []string{".gz", ".gzip"})
	if handler.gzipStatic == nil {
		t.Error("Expected gzipStatic to be non-nil")
	}

	// 禁用 gzip
	handler.SetGzipStatic(false, nil, nil)
	// gzipStatic 保持不变（SetGzipStatic 只在 enabled=true 时设置）
}

// TestStaticHandler_Handle_HeadRequest 测试 HEAD 请求
func TestStaticHandler_Handle_HeadRequest(t *testing.T) {
	tmpDir := t.TempDir()
	content := "head request test"
	if err := os.WriteFile(filepath.Join(tmpDir, "test.txt"), []byte(content), 0o644); err != nil {
		t.Fatalf("创建测试文件失败: %v", err)
	}

	handler := newTestHandler(t, tmpDir)

	ctx := &fasthttp.RequestCtx{}
	ctx.Request.SetRequestURI("/test.txt")
	ctx.Request.Header.SetMethod("HEAD")

	handler.Handle(ctx)

	if got := ctx.Response.StatusCode(); got != fasthttp.StatusOK {
		t.Errorf("状态码 = %d, want %d", got, fasthttp.StatusOK)
	}

	// 验证 Content-Type 设置正确
	ct := string(ctx.Response.Header.ContentType())
	if ct == "" {
		t.Error("Expected Content-Type to be set")
	}
}

// TestStaticHandler_Handle_WithCache 测试带缓存的文件处理
func TestStaticHandler_Handle_WithCache(t *testing.T) {
	tmpDir := t.TempDir()
	content := "cached content test"
	if err := os.WriteFile(filepath.Join(tmpDir, "test.txt"), []byte(content), 0o644); err != nil {
		t.Fatalf("创建测试文件失败: %v", err)
	}

	handler := newTestHandler(t, tmpDir)
	// 设置 nil 缓存，验证不会 panic
	handler.SetFileCache(nil)

	ctx := &fasthttp.RequestCtx{}
	ctx.Request.SetRequestURI("/test.txt")

	handler.Handle(ctx)

	if got := ctx.Response.StatusCode(); got != fasthttp.StatusOK {
		t.Errorf("状态码 = %d, want %d", got, fasthttp.StatusOK)
	}
}

// TestStaticHandler_Handle_Precompressed 测试预压缩文件
func TestStaticHandler_Handle_Precompressed(t *testing.T) {
	tmpDir := t.TempDir()

	// 创建原始文件和 gzip 压缩版本
	content := "test content for gzip"
	if err := os.WriteFile(filepath.Join(tmpDir, "test.txt"), []byte(content), 0o644); err != nil {
		t.Fatalf("创建测试文件失败: %v", err)
	}

	// 创建 .gz 文件（模拟预压缩）
	gzContent := []byte("gzipped content")
	if err := os.WriteFile(filepath.Join(tmpDir, "test.txt.gz"), gzContent, 0o644); err != nil {
		t.Fatalf("创建 gzip 文件失败: %v", err)
	}

	handler := NewStaticHandler(tmpDir, "/", nil, false)
	handler.SetGzipStatic(true, nil, []string{".gz"})

	ctx := &fasthttp.RequestCtx{}
	ctx.Request.SetRequestURI("/test.txt")
	ctx.Request.Header.Set("Accept-Encoding", "gzip")

	handler.Handle(ctx)

	// 应返回预压缩内容
	if got := ctx.Response.StatusCode(); got != fasthttp.StatusOK {
		t.Errorf("状态码 = %d, want %d", got, fasthttp.StatusOK)
	}
}

// TestStaticHandler_Handle_LargeFile 测试大文件处理
func TestStaticHandler_Handle_LargeFile(t *testing.T) {
	tmpDir := t.TempDir()

	// 创建一个较大的文件 (> 8KB 触发 sendfile 路径)
	largeContent := make([]byte, 16*1024)
	for i := range largeContent {
		largeContent[i] = byte(i % 256)
	}

	tmpFile := filepath.Join(tmpDir, "large.bin")
	if err := os.WriteFile(tmpFile, largeContent, 0o644); err != nil {
		t.Fatalf("创建大文件失败: %v", err)
	}

	// 使用 sendfile 启用的处理器
	handler := NewStaticHandler(tmpDir, "/", []string{"index.html"}, true)

	ctx := &fasthttp.RequestCtx{}
	ctx.Request.SetRequestURI("/large.bin")

	handler.Handle(ctx)

	if got := ctx.Response.StatusCode(); got != fasthttp.StatusOK {
		t.Errorf("状态码 = %d, want %d", got, fasthttp.StatusOK)
	}
}

// TestStaticHandler_Handle_Symlink 测试符号链接处理
func TestStaticHandler_Handle_Symlink(t *testing.T) {
	tmpDir := t.TempDir()

	// 创建目标文件
	targetContent := "symlink target"
	targetFile := filepath.Join(tmpDir, "target.txt")
	if err := os.WriteFile(targetFile, []byte(targetContent), 0o644); err != nil {
		t.Fatalf("创建目标文件失败: %v", err)
	}

	// 创建符号链接
	linkFile := filepath.Join(tmpDir, "link.txt")
	if err := os.Symlink(targetFile, linkFile); err != nil {
		t.Fatalf("创建符号链接失败: %v", err)
	}

	handler := newTestHandler(t, tmpDir)

	ctx := &fasthttp.RequestCtx{}
	ctx.Request.SetRequestURI("/link.txt")

	handler.Handle(ctx)

	// 符号链接应该能正常访问
	if got := ctx.Response.StatusCode(); got != fasthttp.StatusOK {
		t.Errorf("状态码 = %d, want %d", got, fasthttp.StatusOK)
	}
}

// TestStaticHandler_SetTryFiles 测试 SetTryFiles 配置设置
func TestStaticHandler_SetTryFiles(t *testing.T) {
	tests := []struct {
		name         string
		tryFiles     []string
		wantTryFiles []string
		tryFilesPass bool
		wantPass     bool
	}{
		{
			name:         "基本配置",
			tryFiles:     []string{"$uri", "$uri/", "/index.html"},
			tryFilesPass: false,
			wantTryFiles: []string{"$uri", "$uri/", "/index.html"},
			wantPass:     false,
		},
		{
			name:         "启用 tryFilesPass",
			tryFiles:     []string{"$uri", "/fallback.html"},
			tryFilesPass: true,
			wantTryFiles: []string{"$uri", "/fallback.html"},
			wantPass:     true,
		},
		{
			name:         "空配置",
			tryFiles:     []string{},
			tryFilesPass: false,
			wantTryFiles: []string{},
			wantPass:     false,
		},
		{
			name:         "单一项配置",
			tryFiles:     []string{"/app.html"},
			tryFilesPass: false,
			wantTryFiles: []string{"/app.html"},
			wantPass:     false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			handler := NewStaticHandler("/var/www", "/", []string{"index.html"}, false)
			router := NewRouter()

			handler.SetTryFiles(tt.tryFiles, tt.tryFilesPass, router)

			// 验证配置
			if len(handler.tryFiles) != len(tt.wantTryFiles) {
				t.Errorf("tryFiles length = %d, want %d", len(handler.tryFiles), len(tt.wantTryFiles))
			}
			for i, v := range tt.wantTryFiles {
				if handler.tryFiles[i] != v {
					t.Errorf("tryFiles[%d] = %q, want %q", i, handler.tryFiles[i], v)
				}
			}
			if handler.tryFilesPass != tt.wantPass {
				t.Errorf("tryFilesPass = %v, want %v", handler.tryFilesPass, tt.wantPass)
			}
			if handler.router != router {
				t.Error("router 未正确设置")
			}
		})
	}
}

// TestStaticHandler_resolveTryFilePath 测试 resolveTryFilePath 占位符解析
func TestStaticHandler_resolveTryFilePath(t *testing.T) {
	handler := NewStaticHandler("/var/www", "/", []string{"index.html"}, false)

	tests := []struct {
		name       string
		tryFile    string
		relPath    string
		wantResult string
	}{
		{
			name:       "$uri 占位符",
			tryFile:    "$uri",
			relPath:    "/api/user",
			wantResult: "/api/user",
		},
		{
			name:       "$uri/ 占位符",
			tryFile:    "$uri/",
			relPath:    "/api/user",
			wantResult: "/api/user/",
		},
		{
			name:       "绝对路径",
			tryFile:    "/index.html",
			relPath:    "/api/user",
			wantResult: "index.html",
		},
		{
			name:       "普通文件名",
			tryFile:    "fallback.html",
			relPath:    "/api/user",
			wantResult: "fallback.html",
		},
		{
			name:       "根路径 $uri",
			tryFile:    "$uri",
			relPath:    "/",
			wantResult: "/",
		},
		{
			name:       "嵌套路径 $uri",
			tryFile:    "$uri",
			relPath:    "/assets/js/app.js",
			wantResult: "/assets/js/app.js",
		},
		{
			name:       "带查询风格路径",
			tryFile:    "$uri",
			relPath:    "/path/to/file.txt",
			wantResult: "/path/to/file.txt",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := handler.resolveTryFilePath(tt.tryFile, tt.relPath)
			if got != tt.wantResult {
				t.Errorf("resolveTryFilePath(%q, %q) = %q, want %q", tt.tryFile, tt.relPath, got, tt.wantResult)
			}
		})
	}
}

// TestStaticHandler_handleTryFiles 测试 handleTryFiles 功能
func TestStaticHandler_handleTryFiles(t *testing.T) {
	tests := []struct {
		setup       func(t *testing.T, root string)
		name        string
		path        string
		wantContent string
		tryFiles    []string
		wantStatus  int
		skipContent bool
	}{
		{
			name: "$uri 找到文件",
			setup: func(t *testing.T, root string) {
				if err := os.WriteFile(filepath.Join(root, "app.js"), []byte("app content"), 0o644); err != nil {
					t.Fatalf("创建文件失败: %v", err)
				}
			},
			tryFiles:    []string{"$uri", "$uri/", "/index.html"},
			path:        "/app.js",
			wantStatus:  fasthttp.StatusOK,
			wantContent: "app content",
		},
		{
			name: "$uri 未找到回退到 $uri/",
			setup: func(t *testing.T, root string) {
				dir := filepath.Join(root, "assets")
				if err := os.MkdirAll(dir, 0o755); err != nil {
					t.Fatalf("创建目录失败: %v", err)
				}
				if err := os.WriteFile(filepath.Join(dir, "index.html"), []byte("assets index"), 0o644); err != nil {
					t.Fatalf("创建索引文件失败: %v", err)
				}
			},
			tryFiles:    []string{"$uri", "$uri/", "/index.html"},
			path:        "/assets",
			wantStatus:  fasthttp.StatusOK,
			wantContent: "assets index",
		},
		{
			name: "回退到 fallback 文件",
			setup: func(t *testing.T, root string) {
				if err := os.WriteFile(filepath.Join(root, "index.html"), []byte("spa fallback"), 0o644); err != nil {
					t.Fatalf("创建 fallback 文件失败: %v", err)
				}
			},
			tryFiles:    []string{"$uri", "$uri/", "/index.html"},
			path:        "/nonexistent",
			wantStatus:  fasthttp.StatusOK,
			wantContent: "spa fallback",
		},
		{
			name: "所有 try_files 都未找到",
			setup: func(_ *testing.T, _ string) {
				// 不创建任何文件
			},
			tryFiles:    []string{"$uri", "$uri/", "/index.html"},
			path:        "/nonexistent",
			wantStatus:  fasthttp.StatusNotFound,
			skipContent: true,
		},
		{
			name: "嵌套目录回退",
			setup: func(t *testing.T, root string) {
				if err := os.WriteFile(filepath.Join(root, "app.html"), []byte("app shell"), 0o644); err != nil {
					t.Fatalf("创建 fallback 文件失败: %v", err)
				}
			},
			tryFiles:    []string{"$uri", "/app.html"},
			path:        "/user/profile",
			wantStatus:  fasthttp.StatusOK,
			wantContent: "app shell",
		},
		{
			name: "路径前缀剥离",
			setup: func(t *testing.T, root string) {
				apiDir := filepath.Join(root, "api")
				if err := os.MkdirAll(apiDir, 0o755); err != nil {
					t.Fatalf("创建目录失败: %v", err)
				}
				if err := os.WriteFile(filepath.Join(apiDir, "data.json"), []byte("json data"), 0o644); err != nil {
					t.Fatalf("创建文件失败: %v", err)
				}
			},
			tryFiles:    []string{"$uri"},
			path:        "/static/api/data.json",
			wantStatus:  fasthttp.StatusNotFound, // 路径前缀剥离后找不到
			skipContent: true,
		},
		{
			name: "空 try_files 数组",
			setup: func(t *testing.T, root string) {
				if err := os.WriteFile(filepath.Join(root, "test.txt"), []byte("test"), 0o644); err != nil {
					t.Fatalf("创建文件失败: %v", err)
				}
			},
			tryFiles:    []string{},
			path:        "/test.txt",
			wantStatus:  fasthttp.StatusOK, // 空 try_files 走标准处理流程
			wantContent: "test",
		},
	}

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

			handler := NewStaticHandler(tmpDir, "/", []string{"index.html"}, false)
			handler.SetTryFiles(tt.tryFiles, false, nil)

			ctx := newTestContext(t, tt.path)
			handler.Handle(ctx)

			if got := ctx.Response.StatusCode(); got != tt.wantStatus {
				t.Errorf("状态码 = %d, want %d", got, tt.wantStatus)
			}

			if !tt.skipContent && tt.wantContent != "" {
				got := string(ctx.Response.Body())
				if got != tt.wantContent {
					t.Errorf("内容 = %q, want %q", got, tt.wantContent)
				}
			}
		})
	}
}

// TestStaticHandler_handleInternalRedirect 测试内部重定向功能
func TestStaticHandler_handleInternalRedirect(t *testing.T) {
	tests := []struct {
		setup        func(t *testing.T, root string)
		name         string
		path         string
		wantContent  string
		tryFiles     []string
		wantStatus   int
		tryFilesPass bool
		skipContent  bool
	}{
		{
			name: "tryFilesPass false 直接服务文件",
			setup: func(t *testing.T, root string) {
				if err := os.WriteFile(filepath.Join(root, "index.html"), []byte("index content"), 0o644); err != nil {
					t.Fatalf("创建文件失败: %v", err)
				}
			},
			tryFiles:     []string{"$uri", "/index.html"},
			tryFilesPass: false,
			path:         "/nonexistent",
			wantStatus:   fasthttp.StatusOK,
			wantContent:  "index content",
		},
		{
			name: "tryFilesPass true 触发重定向",
			setup: func(t *testing.T, root string) {
				if err := os.WriteFile(filepath.Join(root, "fallback.txt"), []byte("fallback content"), 0o644); err != nil {
					t.Fatalf("创建文件失败: %v", err)
				}
			},
			tryFiles:     []string{"$uri", "/fallback.txt"},
			tryFilesPass: true,
			path:         "/nonexistent",
			wantStatus:   fasthttp.StatusOK,
			wantContent:  "fallback content",
		},
		{
			name: "内部重定向目标不存在",
			setup: func(_ *testing.T, _ string) {
				// 不创建 fallback 文件
			},
			tryFiles:     []string{"$uri", "/fallback.html"},
			tryFilesPass: false,
			path:         "/nonexistent",
			wantStatus:   fasthttp.StatusNotFound,
			skipContent:  true,
		},
		{
			name: "内部重定向目标是目录",
			setup: func(t *testing.T, root string) {
				dir := filepath.Join(root, "fallback")
				if err := os.MkdirAll(dir, 0o755); err != nil {
					t.Fatalf("创建目录失败: %v", err)
				}
				// 在 fallback 目录中创建一个 index.html
				if err := os.WriteFile(filepath.Join(dir, "index.html"), []byte("fallback index"), 0o644); err != nil {
					t.Fatalf("创建 index.html 失败: %v", err)
				}
			},
			tryFiles:     []string{"$uri", "$uri/", "/fallback"},
			tryFilesPass: false,
			path:         "/nonexistent",
			wantStatus:   fasthttp.StatusOK,
			wantContent:  "fallback index",
		},
	}

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

			handler := NewStaticHandler(tmpDir, "/", []string{"index.html"}, false)
			router := NewRouter()
			handler.SetTryFiles(tt.tryFiles, tt.tryFilesPass, router)

			// 注册路由处理器用于测试 tryFilesPass 重定向
			if tt.tryFilesPass {
				router.GET("/{filepath:*}", func(ctx *fasthttp.RequestCtx) {
					// 通配符路由，可以匹配任何路径
					path := string(ctx.Path())
					// 从 root 读取文件
					filePath := filepath.Join(tmpDir, path[1:]) // 去掉开头的 /
					data, err := os.ReadFile(filePath)
					if err != nil {
						ctx.SetStatusCode(fasthttp.StatusNotFound)
						ctx.SetBodyString("Not Found")
						return
					}
					ctx.SetStatusCode(fasthttp.StatusOK)
					ctx.SetBody(data)
				})
			}

			ctx := newTestContext(t, tt.path)
			handler.Handle(ctx)

			if got := ctx.Response.StatusCode(); got != tt.wantStatus {
				t.Errorf("状态码 = %d, want %d", got, tt.wantStatus)
			}

			if !tt.skipContent && tt.wantContent != "" {
				got := string(ctx.Response.Body())
				if got != tt.wantContent {
					t.Errorf("内容 = %q, want %q", got, tt.wantContent)
				}
			}
		})
	}
}

// TestStaticHandler_TryFilesSPA 测试 SPA 场景下的 try_files
func TestStaticHandler_TryFilesSPA(t *testing.T) {
	tmpDir := t.TempDir()

	// 创建 SPA 文件结构
	// index.html - 主应用入口
	if err := os.WriteFile(filepath.Join(tmpDir, "index.html"), []byte("<!DOCTYPE html><html><body>SPA App</body></html>"), 0o644); err != nil {
		t.Fatalf("创建 index.html 失败: %v", err)
	}

	// 静态资源文件
	assetsDir := filepath.Join(tmpDir, "assets")
	if err := os.MkdirAll(assetsDir, 0o755); err != nil {
		t.Fatalf("创建 assets 目录失败: %v", err)
	}
	if err := os.WriteFile(filepath.Join(assetsDir, "app.js"), []byte("console.log('app')"), 0o644); err != nil {
		t.Fatalf("创建 app.js 失败: %v", err)
	}
	if err := os.WriteFile(filepath.Join(assetsDir, "style.css"), []byte("body { margin: 0 }"), 0o644); err != nil {
		t.Fatalf("创建 style.css 失败: %v", err)
	}

	handler := NewStaticHandler(tmpDir, "/", []string{"index.html"}, false)
	handler.SetTryFiles([]string{"$uri", "$uri/", "/index.html"}, false, nil)

	tests := []struct {
		name        string
		path        string
		wantContent string
		wantStatus  int
	}{
		{
			name:        "访问存在的静态资源",
			path:        "/assets/app.js",
			wantStatus:  fasthttp.StatusOK,
			wantContent: "console.log('app')",
		},
		{
			name:        "访问存在的 CSS 文件",
			path:        "/assets/style.css",
			wantStatus:  fasthttp.StatusOK,
			wantContent: "body { margin: 0 }",
		},
		{
			name:        "访问前端路由回退到 index.html",
			path:        "/dashboard",
			wantStatus:  fasthttp.StatusOK,
			wantContent: "<!DOCTYPE html><html><body>SPA App</body></html>",
		},
		{
			name:        "访问嵌套前端路由",
			path:        "/user/profile/settings",
			wantStatus:  fasthttp.StatusOK,
			wantContent: "<!DOCTYPE html><html><body>SPA App</body></html>",
		},
		{
			name:        "访问根路径",
			path:        "/",
			wantStatus:  fasthttp.StatusOK,
			wantContent: "<!DOCTYPE html><html><body>SPA App</body></html>",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctx := newTestContext(t, tt.path)
			handler.Handle(ctx)

			if got := ctx.Response.StatusCode(); got != tt.wantStatus {
				t.Errorf("状态码 = %d, want %d", got, tt.wantStatus)
			}

			got := string(ctx.Response.Body())
			if got != tt.wantContent {
				t.Errorf("内容 = %q, want %q", got, tt.wantContent)
			}
		})
	}
}

// TestStaticHandler_TryFilesWithPathPrefix 测试带路径前缀的 try_files
func TestStaticHandler_TryFilesWithPathPrefix(t *testing.T) {
	tmpDir := t.TempDir()

	// 创建 API 模拟文件
	apiDir := filepath.Join(tmpDir, "api")
	if err := os.MkdirAll(apiDir, 0o755); err != nil {
		t.Fatalf("创建 api 目录失败: %v", err)
	}
	if err := os.WriteFile(filepath.Join(apiDir, "users.json"), []byte("[]"), 0o644); err != nil {
		t.Fatalf("创建 users.json 失败: %v", err)
	}

	// 创建静态文件
	if err := os.WriteFile(filepath.Join(tmpDir, "index.html"), []byte("static index"), 0o644); err != nil {
		t.Fatalf("创建 index.html 失败: %v", err)
	}

	handler := NewStaticHandler(tmpDir, "/static", []string{"index.html"}, false)
	handler.SetTryFiles([]string{"$uri", "$uri/", "/index.html"}, false, nil)

	tests := []struct {
		name        string
		path        string
		wantContent string
		wantStatus  int
		skipContent bool
	}{
		{
			name:        "带前缀访问文件",
			path:        "/static/api/users.json",
			wantStatus:  fasthttp.StatusOK,
			wantContent: "[]",
		},
		{
			name:        "带前缀访问目录",
			path:        "/static/api/",
			wantStatus:  fasthttp.StatusOK, // 目录无索引文件，但会回退到 /index.html
			wantContent: "static index",
		},
		{
			name:        "前缀剥离后回退",
			path:        "/static/unknown",
			wantStatus:  fasthttp.StatusOK,
			wantContent: "static index",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctx := newTestContext(t, tt.path)
			handler.Handle(ctx)

			if got := ctx.Response.StatusCode(); got != tt.wantStatus {
				t.Errorf("状态码 = %d, want %d", got, tt.wantStatus)
			}

			if !tt.skipContent {
				got := string(ctx.Response.Body())
				if got != tt.wantContent {
					t.Errorf("内容 = %q, want %q", got, tt.wantContent)
				}
			}
		})
	}
}

// TestStaticHandler_Alias 测试 alias 路径替换功能
func TestStaticHandler_Alias(t *testing.T) {
	tests := []struct {
		setup       func(t *testing.T, aliasDir string)
		name        string
		alias       string
		pathPrefix  string
		path        string
		wantContent string
		wantStatus  int
		skipContent bool
	}{
		{
			name: "alias 基础替换",
			setup: func(t *testing.T, aliasDir string) {
				if err := os.WriteFile(filepath.Join(aliasDir, "logo.png"), []byte("png content"), 0o644); err != nil {
					t.Fatalf("创建文件失败: %v", err)
				}
			},
			alias:       "/alias/images/",
			pathPrefix:  "/images/",
			path:        "/images/logo.png",
			wantStatus:  fasthttp.StatusOK,
			wantContent: "png content",
		},
		{
			name: "alias 嵌套路径",
			setup: func(t *testing.T, aliasDir string) {
				subDir := filepath.Join(aliasDir, "icons")
				if err := os.MkdirAll(subDir, 0o755); err != nil {
					t.Fatalf("创建目录失败: %v", err)
				}
				if err := os.WriteFile(filepath.Join(subDir, "app.png"), []byte("app icon"), 0o644); err != nil {
					t.Fatalf("创建文件失败: %v", err)
				}
			},
			alias:       "/alias/img/",
			pathPrefix:  "/images/",
			path:        "/images/icons/app.png",
			wantStatus:  fasthttp.StatusOK,
			wantContent: "app icon",
		},
		{
			name: "alias 目录索引",
			setup: func(t *testing.T, aliasDir string) {
				if err := os.WriteFile(filepath.Join(aliasDir, "index.html"), []byte("alias index"), 0o644); err != nil {
					t.Fatalf("创建文件失败: %v", err)
				}
			},
			alias:       "/alias/images/",
			pathPrefix:  "/images/",
			path:        "/images/",
			wantStatus:  fasthttp.StatusOK,
			wantContent: "alias index",
		},
		{
			name: "alias 文件不存在",
			setup: func(_ *testing.T, _ string) {
				// 不创建任何文件
			},
			alias:       "/alias/images/",
			pathPrefix:  "/images/",
			path:        "/images/notfound.png",
			wantStatus:  fasthttp.StatusNotFound,
			skipContent: true,
		},
		{
			name: "root 与 alias 互斥 - 使用 alias",
			setup: func(t *testing.T, aliasDir string) {
				if err := os.WriteFile(filepath.Join(aliasDir, "file.txt"), []byte("from alias"), 0o644); err != nil {
					t.Fatalf("创建文件失败: %v", err)
				}
			},
			alias:       "/alias/images/",
			pathPrefix:  "/images/",
			path:        "/images/file.txt",
			wantStatus:  fasthttp.StatusOK,
			wantContent: "from alias",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// 创建临时目录作为 alias 目录
			aliasDir := t.TempDir()
			tt.setup(t, aliasDir)

			// 创建处理器（使用 alias）
			handler := &StaticHandler{
				alias:         aliasDir,
				pathPrefix:    tt.pathPrefix,
				pathPrefixLen: len(tt.pathPrefix),
				index:         []string{"index.html"},
			}

			ctx := newTestContext(t, tt.path)
			handler.Handle(ctx)

			if got := ctx.Response.StatusCode(); got != tt.wantStatus {
				t.Errorf("状态码 = %d, want %d", got, tt.wantStatus)
			}

			if !tt.skipContent && tt.wantContent != "" {
				got := string(ctx.Response.Body())
				if got != tt.wantContent {
					t.Errorf("内容 = %q, want %q", got, tt.wantContent)
				}
			}
		})
	}
}

// TestStaticHandler_AliasVsRoot 测试 alias 和 root 的行为区别
func TestStaticHandler_AliasVsRoot(t *testing.T) {
	// root 目录
	rootDir := t.TempDir()
	// alias 目录
	aliasDir := t.TempDir()

	// 在 root 创建子目录和文件
	imagesDir := filepath.Join(rootDir, "images")
	if err := os.MkdirAll(imagesDir, 0o755); err != nil {
		t.Fatalf("创建 images 目录失败: %v", err)
	}
	if err := os.WriteFile(filepath.Join(imagesDir, "logo.png"), []byte("from root"), 0o644); err != nil {
		t.Fatalf("创建 root 文件失败: %v", err)
	}
	if err := os.WriteFile(filepath.Join(aliasDir, "logo.png"), []byte("from alias"), 0o644); err != nil {
		t.Fatalf("创建 alias 文件失败: %v", err)
	}

	tests := []struct {
		name        string
		handler     *StaticHandler
		path        string
		wantContent string
	}{
		{
			name:        "root 模式：请求路径附加到 root",
			handler:     NewStaticHandler(rootDir, "/", []string{"index.html"}, false),
			path:        "/images/logo.png",
			wantContent: "from root", // /var/www/images/logo.png
		},
		{
			name: "alias 模式：请求路径替换匹配部分",
			handler: &StaticHandler{
				alias:         aliasDir,
				pathPrefix:    "/images/",
				pathPrefixLen: len("/images/"),
				index:         []string{"index.html"},
			},
			path:        "/images/logo.png",
			wantContent: "from alias", // /alias/logo.png（images/被替换）
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctx := newTestContext(t, tt.path)
			tt.handler.Handle(ctx)

			got := string(ctx.Response.Body())
			if got != tt.wantContent {
				t.Errorf("内容 = %q, want %q", got, tt.wantContent)
			}
		})
	}
}

// TestStaticHandler_SetAlias 测试 SetAlias 方法
func TestStaticHandler_SetAlias(t *testing.T) {
	t.Run("设置 alias", func(t *testing.T) {
		handler := NewStaticHandler("/root", "/", nil, false)

		if handler.alias != "" {
			t.Error("初始 alias 应为空")
		}

		handler.SetAlias("/alias")

		if handler.alias != "/alias" {
			t.Errorf("GetAlias() = %q, want %q", handler.alias, "/alias")
		}
		if handler.root != "" {
			t.Error("设置 alias 后 root 应被清空")
		}
	})

	t.Run("设置 root 清除 alias", func(t *testing.T) {
		handler := NewStaticHandler("/root", "/", nil, false)

		// root 被规范化为带尾部斜杠的形式
		expectedRoot := "/root/"
		if handler.root != expectedRoot {
			t.Errorf("root = %q, want %q", handler.root, expectedRoot)
		}
		if handler.alias != "" {
			t.Error("初始 alias 应为空")
		}
	})
}

// TestStaticHandler_AliasWithTryFiles 测试 alias 与 try_files 组合
func TestStaticHandler_AliasWithTryFiles(t *testing.T) {
	aliasDir := t.TempDir()

	// 创建测试文件
	if err := os.WriteFile(filepath.Join(aliasDir, "app.js"), []byte("app content"), 0o644); err != nil {
		t.Fatalf("创建文件失败: %v", err)
	}
	if err := os.WriteFile(filepath.Join(aliasDir, "index.html"), []byte("fallback"), 0o644); err != nil {
		t.Fatalf("创建文件失败: %v", err)
	}

	handler := &StaticHandler{
		alias:         aliasDir,
		pathPrefix:    "/static/",
		pathPrefixLen: len("/static/"),
		index:         []string{"index.html"},
	}
	handler.SetTryFiles([]string{"$uri", "/index.html"}, false, nil)

	tests := []struct {
		name        string
		path        string
		wantContent string
		wantStatus  int
	}{
		{
			name:        "找到文件",
			path:        "/static/app.js",
			wantStatus:  fasthttp.StatusOK,
			wantContent: "app content",
		},
		{
			name:        "回退到 index.html",
			path:        "/static/nonexistent",
			wantStatus:  fasthttp.StatusOK,
			wantContent: "fallback",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctx := newTestContext(t, tt.path)
			handler.Handle(ctx)

			if got := ctx.Response.StatusCode(); got != tt.wantStatus {
				t.Errorf("状态码 = %d, want %d", got, tt.wantStatus)
			}

			got := string(ctx.Response.Body())
			if got != tt.wantContent {
				t.Errorf("内容 = %q, want %q", got, tt.wantContent)
			}
		})
	}
}

// TestStaticHandler_TryFilesEdgeCases 测试 try_files 边界情况
func TestStaticHandler_TryFilesEdgeCases(t *testing.T) {
	tmpDir := t.TempDir()

	// 创建测试文件
	if err := os.WriteFile(filepath.Join(tmpDir, "file with spaces.txt"), []byte("spaces"), 0o644); err != nil {
		t.Fatalf("创建带空格文件失败: %v", err)
	}

	handler := NewStaticHandler(tmpDir, "/", []string{"index.html"}, false)
	handler.SetTryFiles([]string{"$uri", "/index.html"}, false, nil)

	tests := []struct {
		name       string
		path       string
		wantStatus int
	}{
		{
			name:       "路径遍历攻击被阻止 - fasthttp 规范化",
			path:       "/../secret",
			wantStatus: fasthttp.StatusNotFound, // fasthttp 规范化为 /secret，文件不存在返回 404
		},
		{
			name:       "双点号在路径中被阻止",
			path:       "/file..name",
			wantStatus: fasthttp.StatusForbidden,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctx := newTestContext(t, tt.path)
			handler.Handle(ctx)

			if got := ctx.Response.StatusCode(); got != tt.wantStatus {
				t.Errorf("状态码 = %d, want %d", got, tt.wantStatus)
			}
		})
	}
}

// TestStaticHandler_LargeFileContentType 测试大文件 sendfile 路径的 Content-Type
func TestStaticHandler_LargeFileContentType(t *testing.T) {
	tmpDir := t.TempDir()

	// 创建大于 8KB 的文件
	largeContent := make([]byte, 16*1024)
	for i := range largeContent {
		largeContent[i] = byte(i % 256)
	}

	tests := []struct {
		ext      string
		expected string
	}{
		{".css", "text/css; charset=utf-8"},
		{".js", "text/javascript; charset=utf-8"},
		{".webmanifest", "application/manifest+json"},
		{".webm", "video/webm"},
		{".otf", "font/otf"},
	}

	for _, tc := range tests {
		t.Run(tc.ext, func(t *testing.T) {
			filePath := filepath.Join(tmpDir, "large"+tc.ext)
			if err := os.WriteFile(filePath, largeContent, 0o644); err != nil {
				t.Fatalf("创建文件失败: %v", err)
			}

			handler := NewStaticHandler(tmpDir, "/", nil, true) // 启用 sendfile
			ctx := &fasthttp.RequestCtx{}
			ctx.Request.SetRequestURI("/large" + tc.ext)

			handler.Handle(ctx)

			if got := ctx.Response.StatusCode(); got != fasthttp.StatusOK {
				t.Errorf("状态码 = %d, want %d", got, fasthttp.StatusOK)
			}

			ct := string(ctx.Response.Header.ContentType())
			if ct != tc.expected {
				t.Errorf("Content-Type = %q, want %q", ct, tc.expected)
			}
		})
	}
}

// TestResolveTryFilePathWithDynamicSuffix 测试动态后缀解析
func TestResolveTryFilePathWithDynamicSuffix(t *testing.T) {
	handler := NewStaticHandler("/var/www", "/", []string{"index.html"}, false)

	tests := []struct {
		name       string
		tryFile    string
		relPath    string
		wantResult string
	}{
		// 基本占位符
		{name: "$uri", tryFile: "$uri", relPath: "/api/user", wantResult: "/api/user"},
		{name: "$uri/", tryFile: "$uri/", relPath: "/api/user", wantResult: "/api/user/"},

		// 动态后缀 - 正常路径
		{name: "$uri.html 正常", tryFile: "$uri.html", relPath: "/about", wantResult: "/about.html"},
		{name: "$uri.json 正常", tryFile: "$uri.json", relPath: "/api/data", wantResult: "/api/data.json"},
		{name: "$uri.css 正常", tryFile: "$uri.css", relPath: "/styles/main", wantResult: "/styles/main.css"},

		// 动态后缀 - 根路径边界（返回空字符串）
		{name: "$uri.html 根路径", tryFile: "$uri.html", relPath: "/", wantResult: ""},
		{name: "$uri.json 根路径", tryFile: "$uri.json", relPath: "/", wantResult: ""},

		// 动态后缀 - 子目录路径（正常处理）
		{name: "$uri.html 子目录", tryFile: "$uri.html", relPath: "/api/", wantResult: "/api/.html"},
		{name: "$uri.json 子目录", tryFile: "$uri.json", relPath: "/v1/", wantResult: "/v1/.json"},

		// 绝对路径
		{name: "绝对路径", tryFile: "/index.html", relPath: "/api/user", wantResult: "index.html"},
		{name: "绝对路径嵌套", tryFile: "/fallback/app.html", relPath: "/any", wantResult: "fallback/app.html"},

		// 相对路径
		{name: "相对路径", tryFile: "fallback.html", relPath: "/api/user", wantResult: "fallback.html"},
		{name: "相对路径带连字符", tryFile: "app-shell.html", relPath: "/any", wantResult: "app-shell.html"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := handler.resolveTryFilePath(tt.tryFile, tt.relPath)
			if got != tt.wantResult {
				t.Errorf("resolveTryFilePath(%q, %q) = %q, want %q",
					tt.tryFile, tt.relPath, got, tt.wantResult)
			}
		})
	}
}

// TestStaticHandler_TryFilesWithDynamicSuffix 测试动态后缀集成
func TestStaticHandler_TryFilesWithDynamicSuffix(t *testing.T) {
	tmpDir := t.TempDir()

	// 创建测试文件
	if err := os.WriteFile(filepath.Join(tmpDir, "about.html"), []byte("about page"), 0o644); err != nil {
		t.Fatalf("创建文件失败: %v", err)
	}
	if err := os.MkdirAll(filepath.Join(tmpDir, "api"), 0o755); err != nil {
		t.Fatalf("创建目录失败: %v", err)
	}
	if err := os.WriteFile(filepath.Join(tmpDir, "api", "data.json"), []byte("{\"data\":true}"), 0o644); err != nil {
		t.Fatalf("创建文件失败: %v", err)
	}
	if err := os.WriteFile(filepath.Join(tmpDir, "index.html"), []byte("fallback"), 0o644); err != nil {
		t.Fatalf("创建文件失败: %v", err)
	}

	handler := NewStaticHandler(tmpDir, "/", []string{"index.html"}, false)
	handler.SetTryFiles([]string{"$uri", "$uri.html", "/index.html"}, false, nil)

	tests := []struct {
		name        string
		path        string
		wantContent string
		wantStatus  int
	}{
		{
			name:        "找到 $uri.html",
			path:        "/about",
			wantStatus:  fasthttp.StatusOK,
			wantContent: "about page",
		},
		{
			name:        "回退到 /index.html",
			path:        "/nonexistent",
			wantStatus:  fasthttp.StatusOK,
			wantContent: "fallback",
		},
		{
			name:        "根路径回退到 /index.html",
			path:        "/",
			wantStatus:  fasthttp.StatusOK,
			wantContent: "fallback",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctx := newTestContext(t, tt.path)
			handler.Handle(ctx)

			if got := ctx.Response.StatusCode(); got != tt.wantStatus {
				t.Errorf("状态码 = %d, want %d", got, tt.wantStatus)
			}

			if got := string(ctx.Response.Body()); got != tt.wantContent {
				t.Errorf("内容 = %q, want %q", got, tt.wantContent)
			}
		})
	}
}

// TestStaticHandler_TryFilesRootPathFallback 测试根路径回退
func TestStaticHandler_TryFilesRootPathFallback(t *testing.T) {
	tmpDir := t.TempDir()

	// 创建 index.html 作为根路径回退
	if err := os.WriteFile(filepath.Join(tmpDir, "index.html"), []byte("root fallback"), 0o644); err != nil {
		t.Fatalf("创建文件失败: %v", err)
	}

	// 注意：不创建 /.html 文件，测试根路径边界情况
	handler := NewStaticHandler(tmpDir, "/", []string{"index.html"}, false)
	handler.SetTryFiles([]string{"$uri", "$uri.html", "/index.html"}, false, nil)

	ctx := newTestContext(t, "/")
	handler.Handle(ctx)

	// 验证根路径请求正确回退到 /index.html
	if got := ctx.Response.StatusCode(); got != fasthttp.StatusOK {
		t.Errorf("状态码 = %d, want %d", got, fasthttp.StatusOK)
	}

	if got := string(ctx.Response.Body()); got != "root fallback" {
		t.Errorf("内容 = %q, want %q", got, "root fallback")
	}
}

// TestStaticHandler_SetSymlinkCheck 测试 SetSymlinkCheck 方法
func TestStaticHandler_SetSymlinkCheck(t *testing.T) {
	handler := NewStaticHandler("/var/www", "/", nil, false)

	if handler.symlinkCheck {
		t.Error("初始 symlinkCheck 应为 false")
	}

	handler.SetSymlinkCheck(true)
	if !handler.symlinkCheck {
		t.Error("SetSymlinkCheck(true) 后 symlinkCheck 应为 true")
	}

	handler.SetSymlinkCheck(false)
	if handler.symlinkCheck {
		t.Error("SetSymlinkCheck(false) 后 symlinkCheck 应为 false")
	}
}

// TestStaticHandler_SetInternal 测试 SetInternal 方法
func TestStaticHandler_SetInternal(t *testing.T) {
	handler := NewStaticHandler("/var/www", "/", nil, false)

	if handler.internal {
		t.Error("初始 internal 应为 false")
	}

	handler.SetInternal(true)
	if !handler.internal {
		t.Error("SetInternal(true) 后 internal 应为 true")
	}

	handler.SetInternal(false)
	if handler.internal {
		t.Error("SetInternal(false) 后 internal 应为 false")
	}
}

// TestStaticHandler_SetCacheTTL 测试 SetCacheTTL 方法
func TestStaticHandler_SetCacheTTL(t *testing.T) {
	handler := NewStaticHandler("/var/www", "/", nil, false)

	if handler.cacheTTL != 0 {
		t.Error("初始 cacheTTL 应为 0")
	}

	handler.SetCacheTTL(5 * time.Second)
	if handler.cacheTTL != 5*time.Second {
		t.Errorf("SetCacheTTL 后 cacheTTL = %v, want %v", handler.cacheTTL, 5*time.Second)
	}

	handler.SetCacheTTL(0)
	if handler.cacheTTL != 0 {
		t.Error("SetCacheTTL(0) 后 cacheTTL 应为 0")
	}
}

// TestStaticHandler_InternalRestriction 测试 internal 访问限制
func TestStaticHandler_InternalRestriction(t *testing.T) {
	tmpDir := t.TempDir()
	content := "internal content"
	if err := os.WriteFile(filepath.Join(tmpDir, "test.txt"), []byte(content), 0o644); err != nil {
		t.Fatalf("创建测试文件失败: %v", err)
	}

	handler := newTestHandler(t, tmpDir)
	handler.SetInternal(true)

	t.Run("外部请求返回 404", func(t *testing.T) {
		ctx := newTestContext(t, "/test.txt")
		handler.Handle(ctx)

		if got := ctx.Response.StatusCode(); got != fasthttp.StatusNotFound {
			t.Errorf("状态码 = %d, want %d", got, fasthttp.StatusNotFound)
		}
	})

	t.Run("内部重定向允许访问", func(t *testing.T) {
		ctx := newTestContext(t, "/test.txt")
		// 标记为内部重定向
		ctx.SetUserValue("__internal_redirect__", "/test.txt")
		handler.Handle(ctx)

		if got := ctx.Response.StatusCode(); got != fasthttp.StatusOK {
			t.Errorf("状态码 = %d, want %d", got, fasthttp.StatusOK)
		}
	})
}

// TestStaticHandler_ValidateSymlink 测试符号链接验证
func TestStaticHandler_ValidateSymlink(t *testing.T) {
	tmpDir := t.TempDir()

	// 创建目标文件和目录
	targetDir := filepath.Join(tmpDir, "target")
	if err := os.MkdirAll(targetDir, 0o755); err != nil {
		t.Fatalf("创建目标目录失败: %v", err)
	}
	targetFile := filepath.Join(targetDir, "secret.txt")
	if err := os.WriteFile(targetFile, []byte("secret content"), 0o644); err != nil {
		t.Fatalf("创建目标文件失败: %v", err)
	}

	// 创建允许的根目录
	allowedDir := filepath.Join(tmpDir, "allowed")
	if err := os.MkdirAll(allowedDir, 0o755); err != nil {
		t.Fatalf("创建允许目录失败: %v", err)
	}

	t.Run("安全符号链接 - 在允许范围内", func(t *testing.T) {
		// 在允许目录内创建符号链接
		linkFile := filepath.Join(allowedDir, "link.txt")
		allowedTarget := filepath.Join(allowedDir, "actual.txt")
		if err := os.WriteFile(allowedTarget, []byte("allowed content"), 0o644); err != nil {
			t.Fatalf("创建实际文件失败: %v", err)
		}
		if err := os.Symlink(allowedTarget, linkFile); err != nil {
			t.Fatalf("创建符号链接失败: %v", err)
		}

		handler := NewStaticHandler(allowedDir, "/", nil, false)
		handler.SetSymlinkCheck(true)

		ctx := newTestContext(t, "/link.txt")
		handler.Handle(ctx)

		if got := ctx.Response.StatusCode(); got != fasthttp.StatusOK {
			t.Errorf("状态码 = %d, want %d", got, fasthttp.StatusOK)
		}
	})

	t.Run("不安全符号链接 - 指向允许范围外", func(t *testing.T) {
		// 创建指向允许目录外的符号链接
		unsafeLink := filepath.Join(allowedDir, "unsafe.txt")
		if err := os.Symlink(targetFile, unsafeLink); err != nil {
			t.Fatalf("创建不安全符号链接失败: %v", err)
		}

		handler := NewStaticHandler(allowedDir, "/", nil, false)
		handler.SetSymlinkCheck(true)

		ctx := newTestContext(t, "/unsafe.txt")
		handler.Handle(ctx)

		if got := ctx.Response.StatusCode(); got != fasthttp.StatusForbidden {
			t.Errorf("状态码 = %d, want %d", got, fasthttp.StatusForbidden)
		}
	})

	t.Run("普通文件 - 非符号链接", func(t *testing.T) {
		normalFile := filepath.Join(allowedDir, "normal.txt")
		if err := os.WriteFile(normalFile, []byte("normal content"), 0o644); err != nil {
			t.Fatalf("创建普通文件失败: %v", err)
		}

		handler := NewStaticHandler(allowedDir, "/", nil, false)
		handler.SetSymlinkCheck(true)

		ctx := newTestContext(t, "/normal.txt")
		handler.Handle(ctx)

		if got := ctx.Response.StatusCode(); got != fasthttp.StatusOK {
			t.Errorf("状态码 = %d, want %d", got, fasthttp.StatusOK)
		}
	})

	t.Run("未启用符号链接检查", func(t *testing.T) {
		// 创建指向允许目录外的符号链接
		externalLink := filepath.Join(allowedDir, "external.txt")
		if err := os.Symlink(targetFile, externalLink); err != nil {
			t.Fatalf("创建外部符号链接失败: %v", err)
		}

		handler := NewStaticHandler(allowedDir, "/", nil, false)
		// 不启用符号链接检查

		ctx := newTestContext(t, "/external.txt")
		handler.Handle(ctx)

		// 未启用检查时，可以访问符号链接
		if got := ctx.Response.StatusCode(); got != fasthttp.StatusOK {
			t.Errorf("状态码 = %d, want %d", got, fasthttp.StatusOK)
		}
	})
}

// TestStaticHandler_ValidateSymlink_WithAlias 测试 alias 模式下的符号链接验证
func TestStaticHandler_ValidateSymlink_WithAlias(t *testing.T) {
	tmpDir := t.TempDir()

	// 创建 alias 目录
	aliasDir := filepath.Join(tmpDir, "alias")
	if err := os.MkdirAll(aliasDir, 0o755); err != nil {
		t.Fatalf("创建 alias 目录失败: %v", err)
	}

	// 在 alias 目录内创建文件和符号链接
	actualFile := filepath.Join(aliasDir, "actual.txt")
	if err := os.WriteFile(actualFile, []byte("actual content"), 0o644); err != nil {
		t.Fatalf("创建实际文件失败: %v", err)
	}

	linkFile := filepath.Join(aliasDir, "link.txt")
	if err := os.Symlink(actualFile, linkFile); err != nil {
		t.Fatalf("创建符号链接失败: %v", err)
	}

	handler := &StaticHandler{
		alias:         aliasDir,
		pathPrefix:    "/static/",
		pathPrefixLen: len("/static/"),
	}
	handler.SetSymlinkCheck(true)

	ctx := newTestContext(t, "/static/link.txt")
	handler.Handle(ctx)

	if got := ctx.Response.StatusCode(); got != fasthttp.StatusOK {
		t.Errorf("状态码 = %d, want %d", got, fasthttp.StatusOK)
	}
}

// TestStaticHandler_ValidateSymlink_NoRootOrAlias 测试无 root/alias 时符号链接验证
func TestStaticHandler_ValidateSymlink_NoRootOrAlias(t *testing.T) {
	tmpDir := t.TempDir()

	// 创建文件和符号链接
	targetFile := filepath.Join(tmpDir, "target.txt")
	if err := os.WriteFile(targetFile, []byte("target"), 0o644); err != nil {
		t.Fatalf("创建目标文件失败: %v", err)
	}

	linkFile := filepath.Join(tmpDir, "link.txt")
	if err := os.Symlink(targetFile, linkFile); err != nil {
		t.Fatalf("创建符号链接失败: %v", err)
	}

	// 创建无 root/alias 的处理器
	handler := NewStaticHandler("", "/", nil, false)
	handler.SetSymlinkCheck(true)

	// 直接调用 validateSymlink
	err := handler.validateSymlink(linkFile)
	if err == nil {
		t.Error("无 root/alias 时验证符号链接应返回错误")
	}
}

// TestStaticHandler_Handle_WithCacheTTL 测试带 TTL 的缓存处理
func TestStaticHandler_Handle_WithCacheTTL(t *testing.T) {
	tmpDir := t.TempDir()
	content := "cached with ttl"
	testFile := filepath.Join(tmpDir, "test.txt")
	if err := os.WriteFile(testFile, []byte(content), 0o644); err != nil {
		t.Fatalf("创建测试文件失败: %v", err)
	}

	// 创建带缓存的处理器
	handler := newTestHandler(t, tmpDir)
	handler.SetFileCache(cache.NewFileCache(100, 1024*1024, time.Hour))
	handler.SetCacheTTL(5 * time.Second)

	// 第一次请求，填充缓存
	ctx1 := newTestContext(t, "/test.txt")
	handler.Handle(ctx1)

	if got := ctx1.Response.StatusCode(); got != fasthttp.StatusOK {
		t.Errorf("第一次请求状态码 = %d, want %d", got, fasthttp.StatusOK)
	}

	// 第二次请求，应该命中缓存
	ctx2 := newTestContext(t, "/test.txt")
	handler.Handle(ctx2)

	if got := ctx2.Response.StatusCode(); got != fasthttp.StatusOK {
		t.Errorf("第二次请求状态码 = %d, want %d", got, fasthttp.StatusOK)
	}

	// 内容应该一致
	if string(ctx1.Response.Body()) != string(ctx2.Response.Body()) {
		t.Error("缓存内容不一致")
	}
}

// TestStaticHandler_Handle_CacheTTLExpired 测试 TTL 过期后重新验证
func TestStaticHandler_Handle_CacheTTLExpired(t *testing.T) {
	tmpDir := t.TempDir()
	content := "initial content"
	testFile := filepath.Join(tmpDir, "test.txt")
	if err := os.WriteFile(testFile, []byte(content), 0o644); err != nil {
		t.Fatalf("创建测试文件失败: %v", err)
	}

	// 创建带缓存的处理器，TTL 设置很短
	handler := newTestHandler(t, tmpDir)
	handler.SetFileCache(cache.NewFileCache(100, 1024*1024, time.Hour))
	handler.SetCacheTTL(100 * time.Millisecond)

	// 第一次请求，填充缓存
	ctx1 := newTestContext(t, "/test.txt")
	handler.Handle(ctx1)

	// 等待 TTL 过期
	time.Sleep(150 * time.Millisecond)

	// 修改文件
	newContent := "updated content"
	if err := os.WriteFile(testFile, []byte(newContent), 0o644); err != nil {
		t.Fatalf("更新文件失败: %v", err)
	}

	// 第二次请求，TTL 过期后应该重新读取
	ctx2 := newTestContext(t, "/test.txt")
	handler.Handle(ctx2)

	if got := string(ctx2.Response.Body()); got != newContent {
		t.Errorf("TTL 过期后内容 = %q, want %q", got, newContent)
	}
}

// TestStaticHandler_Handle_CacheModTimeChanged 测试文件修改后缓存更新
func TestStaticHandler_Handle_CacheModTimeChanged(t *testing.T) {
	tmpDir := t.TempDir()
	testFile := filepath.Join(tmpDir, "test.txt")
	initialContent := "initial"
	if err := os.WriteFile(testFile, []byte(initialContent), 0o644); err != nil {
		t.Fatalf("创建测试文件失败: %v", err)
	}

	// 创建带缓存的处理器
	handler := newTestHandler(t, tmpDir)
	handler.SetFileCache(cache.NewFileCache(100, 1024*1024, time.Hour))

	// 第一次请求，填充缓存
	ctx1 := newTestContext(t, "/test.txt")
	handler.Handle(ctx1)

	// 修改文件
	time.Sleep(10 * time.Millisecond) // 确保 ModTime 变化
	newContent := "modified"
	if err := os.WriteFile(testFile, []byte(newContent), 0o644); err != nil {
		t.Fatalf("修改文件失败: %v", err)
	}

	// 第二次请求，应该检测到文件变化并更新缓存
	ctx2 := newTestContext(t, "/test.txt")
	handler.Handle(ctx2)

	if got := string(ctx2.Response.Body()); got != newContent {
		t.Errorf("修改后内容 = %q, want %q", got, newContent)
	}
}

// TestStaticHandler_SetExpires 测试 SetExpires 方法。
func TestStaticHandler_SetExpires(t *testing.T) {
	tmpDir := t.TempDir()
	handler := newTestHandler(t, tmpDir)

	// 默认为空
	if handler.expires != "" {
		t.Errorf("默认 expires = %q, want empty", handler.expires)
	}

	// 设置 expires
	handler.SetExpires("30d")
	if handler.expires != "30d" {
		t.Errorf("expires = %q, want 30d", handler.expires)
	}

	// 设置 off
	handler.SetExpires("off")
	if handler.expires != "off" {
		t.Errorf("expires = %q, want off", handler.expires)
	}

	// 设置 max
	handler.SetExpires("max")
	if handler.expires != "max" {
		t.Errorf("expires = %q, want max", handler.expires)
	}
}

// TestSetCacheHeaders 测试 setCacheHeaders 方法。
func TestSetCacheHeaders(t *testing.T) {
	tests := []struct {
		name           string
		expires        string
		wantCacheCtrl  string
		wantExpiresSet bool // 是否设置 Expires 头
	}{
		{
			name:          "empty_expires",
			expires:       "",
			wantCacheCtrl: "",
		},
		{
			name:          "off_expires",
			expires:       "off",
			wantCacheCtrl: "",
		},
		{
			name:           "epoch_expires",
			expires:        "epoch",
			wantCacheCtrl:  "no-cache, no-store, must-revalidate",
			wantExpiresSet: true,
		},
		{
			name:           "max_expires",
			expires:        "max",
			wantCacheCtrl:  "public, max-age=315360000, immutable",
			wantExpiresSet: true,
		},
		{
			name:           "duration_expires",
			expires:        "1h",
			wantCacheCtrl:  "public, max-age=3600",
			wantExpiresSet: true,
		},
		{
			name:           "complex_duration",
			expires:        "1d1h",
			wantCacheCtrl:  "public, max-age=90000",
			wantExpiresSet: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			tmpDir := t.TempDir()
			handler := newTestHandler(t, tmpDir)
			handler.SetExpires(tt.expires)

			ctx := newTestContext(t, "/test.txt")
			handler.setCacheHeaders(ctx)

			cacheCtrl := string(ctx.Response.Header.Peek("Cache-Control"))
			if tt.wantCacheCtrl == "" {
				if cacheCtrl != "" {
					t.Errorf("Cache-Control = %q, want empty", cacheCtrl)
				}
			} else {
				if cacheCtrl != tt.wantCacheCtrl {
					t.Errorf("Cache-Control = %q, want %q", cacheCtrl, tt.wantCacheCtrl)
				}
			}

			expires := string(ctx.Response.Header.Peek("Expires"))
			if tt.wantExpiresSet && expires == "" {
				t.Error("Expected Expires header to be set")
			}
		})
	}
}

// TestStaticHandler_ConcurrentSetterAndHandle 验证 setter 与 Handle 并发访问无数据竞争。
func TestStaticHandler_ConcurrentSetterAndHandle(t *testing.T) {
	tmpDir := t.TempDir()
	if err := os.WriteFile(filepath.Join(tmpDir, "test.txt"), []byte("hello"), 0o644); err != nil {
		t.Fatalf("创建测试文件失败: %v", err)
	}

	handler := newTestHandler(t, tmpDir)

	var wg sync.WaitGroup
	// 并发执行 setter
	for i := range 10 {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			switch id % 5 {
			case 0:
				handler.SetExpires("1h")
			case 1:
				handler.SetCacheTTL(time.Second)
			case 2:
				handler.SetFileCache(nil)
			case 3:
				handler.SetGzipStatic(false, nil, nil)
			case 4:
				handler.SetTryFiles([]string{"$uri"}, false, nil)
			}
		}(i)
	}

	// 并发执行 Handle
	for range 20 {
		wg.Add(1)
		go func() {
			defer wg.Done()
			ctx := newTestContext(t, "/test.txt")
			handler.Handle(ctx)
		}()
	}

	wg.Wait()
}

// TestParseExpires 测试 parseExpires 函数。
func TestParseExpires(t *testing.T) {
	tests := []struct {
		name     string
		expires  string
		wantSecs int64
	}{
		{"empty", "", 0},
		{"off", "off", 0},
		{"max", "max", 315360000},
		{"epoch", "epoch", -1},
		{"seconds", "30s", 30},
		{"minutes", "5m", 300},
		{"hours", "2h", 7200},
		{"days", "1d", 86400},
		{"complex", "1d1h1m1s", 90061},
		{"multiple_days", "30d", 2592000},
		{"mixed", "7d12h", 648000},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := parseExpires(tt.expires)
			if got != tt.wantSecs {
				t.Errorf("parseExpires(%q) = %d, want %d", tt.expires, got, tt.wantSecs)
			}
		})
	}
}
