// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package fileserver

import (
	"context"
	"fmt"
	"io/fs"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"testing"
	"time"

	"go.uber.org/zap"

	"github.com/caddyserver/caddy/v2"
	"github.com/caddyserver/caddy/v2/internal/filesystems"
)

// makeBenchDir populates dir with n regular files and a handful of
// symlinks (both to files and to a subdirectory), mirroring what a
// real, large, browsable directory tends to contain.
func makeBenchDir(tb testing.TB, dir string, n int) {
	tb.Helper()

	subdir := filepath.Join(dir, "subdir")
	if err := os.Mkdir(subdir, 0o755); err != nil {
		tb.Fatal(err)
	}

	for i := range n {
		name := filepath.Join(dir, fmt.Sprintf("file-%d.txt", i))
		if err := os.WriteFile(name, []byte("x"), 0o600); err != nil {
			tb.Fatal(err)
		}
	}

	target := filepath.Join(dir, "file-0.txt")
	if err := os.Symlink(target, filepath.Join(dir, "symlink-to-file")); err != nil {
		tb.Skipf("symlink not supported on this platform: %v", err)
	}
	if err := os.Symlink(subdir, filepath.Join(dir, "symlink-to-dir")); err != nil {
		tb.Skipf("symlink not supported on this platform: %v", err)
	}
}

// readBenchDirEntries reads all entries of dir once, up front, so that
// benchmark iterations measure only the cost of directoryListing itself
// (allocations, stat calls per entry) rather than the readdir syscall.
//
// It deliberately opens the directory and calls the ReadDir method on the
// resulting file (like loadDirectoryContents does in production), not
// fs.ReadDir/os.ReadDir - the latter sort entries by filename before
// returning them, which would silently hand the sort benchmarks
// already-sorted input and let sort.Sort's adaptive algorithm breeze
// through with far fewer comparisons than a real, filesystem-ordered
// directory listing requires.
func readBenchDirEntries(tb testing.TB, dir string) (fs.FS, []fs.DirEntry) {
	tb.Helper()

	fileSystem := filesystems.OsFS{}
	f, err := fileSystem.Open(dir)
	if err != nil {
		tb.Fatal(err)
	}
	defer f.Close()

	entries, err := f.(fs.ReadDirFile).ReadDir(-1)
	if err != nil {
		tb.Fatal(err)
	}
	return fileSystem, entries
}

func benchFileServer() *FileServer {
	return &FileServer{
		Browse: &Browse{},
		logger: zap.NewNop(),
	}
}

func benchmarkDirectoryListing(b *testing.B, n int, listFn func(fsrv *FileServer, ctx context.Context, fileSystem fs.FS, entries []fs.DirEntry, root string) *browseTemplateContext) {
	dir := b.TempDir()
	makeBenchDir(b, dir, n)
	fileSystem, entries := readBenchDirEntries(b, dir)
	fsrv := benchFileServer()
	ctx := context.Background()

	b.ReportAllocs()

	for b.Loop() {
		listFn(fsrv, ctx, fileSystem, entries, dir)
	}
}

func BenchmarkDirectoryListing(b *testing.B) {
	for _, n := range []int{100, 1_000, 10_000, 50_000} {
		b.Run(fmt.Sprintf("entries=%d", n), func(b *testing.B) {
			benchmarkDirectoryListing(b, n, func(fsrv *FileServer, ctx context.Context, fileSystem fs.FS, entries []fs.DirEntry, root string) *browseTemplateContext {
				return fsrv.directoryListing(ctx, fileSystem, time.Time{}, entries, true, root, "/", caddy.NewReplacer())
			})
		})
	}
}

// makeBenchItems builds n synthetic fileInfo items (no disk I/O), mixing
// upper/lower case names and marking roughly 1 in 10 as directories, to
// exercise the sort comparators - including bySize's directory name
// tie-break - the way a real, large, mixed listing would.
func makeBenchItems(n int) []fileInfo {
	items := make([]fileInfo, n)
	for i := range n {
		name := fmt.Sprintf("file_%d.txt", i)
		if i%2 == 0 {
			name = fmt.Sprintf("FILE_%d.TXT", i)
		}
		items[i] = fileInfo{
			Name:  name,
			Size:  int64(i),
			IsDir: i%10 == 0,
		}
	}
	return items
}

func fillNameLower(items []fileInfo) {
	for i := range items {
		items[i].nameLower = strings.ToLower(items[i].Name)
	}
}

// benchmarkSort times sortFn against a fresh, unsorted copy of a synthetic
// item set on every iteration. The copy, and the optional prepare step
// (e.g. filling the nameLower cache), happen outside the timed portion so
// the benchmark isolates the cost of the sort itself.
func benchmarkSort(b *testing.B, n int, prepare func(items []fileInfo), sortFn func(items []fileInfo)) {
	pristine := makeBenchItems(n)
	items := make([]fileInfo, n)

	for b.Loop() {
		b.StopTimer()
		copy(items, pristine)
		if prepare != nil {
			prepare(items)
		}
		b.StartTimer()
		sortFn(items)
	}
}

func BenchmarkSortByName(b *testing.B) {
	for _, n := range []int{100, 1_000, 10_000, 50_000} {
		b.Run(fmt.Sprintf("entries=%d", n), func(b *testing.B) {
			benchmarkSort(b, n, fillNameLower, func(items []fileInfo) {
				sort.Sort(byName(browseTemplateContext{Items: items}))
			})
		})
	}
}

func BenchmarkSortByNameDirFirst(b *testing.B) {
	for _, n := range []int{100, 1_000, 10_000, 50_000} {
		b.Run(fmt.Sprintf("entries=%d", n), func(b *testing.B) {
			benchmarkSort(b, n, fillNameLower, func(items []fileInfo) {
				sort.Sort(byNameDirFirst(browseTemplateContext{Items: items}))
			})
		})
	}
}

func BenchmarkSortBySize(b *testing.B) {
	for _, n := range []int{100, 1_000, 10_000, 50_000} {
		b.Run(fmt.Sprintf("entries=%d", n), func(b *testing.B) {
			benchmarkSort(b, n, fillNameLower, func(items []fileInfo) {
				sort.Sort(bySize(browseTemplateContext{Items: items}))
			})
		})
	}
}

// ---------------------------------------------------------------------
// Overall benchmark: the whole real-world browse-request path - reading
// directory entries into a listing and then sorting it.
// ---------------------------------------------------------------------

func benchmarkDirectoryListingAndSort(b *testing.B, n int, run func(fsrv *FileServer, ctx context.Context, fileSystem fs.FS, entries []fs.DirEntry, root string) *browseTemplateContext) {
	dir := b.TempDir()
	makeBenchDir(b, dir, n)
	fileSystem, entries := readBenchDirEntries(b, dir)
	fsrv := benchFileServer()
	ctx := context.Background()

	b.ReportAllocs()

	for b.Loop() {
		run(fsrv, ctx, fileSystem, entries, dir)
	}
}

func BenchmarkDirectoryListingAndSort(b *testing.B) {
	for _, n := range []int{100, 1_000, 10_000, 50_000} {
		b.Run(fmt.Sprintf("entries=%d", n), func(b *testing.B) {
			benchmarkDirectoryListingAndSort(b, n, func(fsrv *FileServer, ctx context.Context, fileSystem fs.FS, entries []fs.DirEntry, root string) *browseTemplateContext {
				listing := fsrv.directoryListing(ctx, fileSystem, time.Time{}, entries, true, root, "/", caddy.NewReplacer())
				listing.applySortAndLimit(sortByNameDirFirst, sortOrderAsc, "", "")
				return listing
			})
		})
	}
}
