// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2026-present Datadog, Inc.

package ls_test

import (
	"fmt"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/DataDog/rshell/builtins/ls"
	"github.com/DataDog/rshell/builtins/testutil"
	"github.com/DataDog/rshell/interp"
)

const pentestTimeout = 10 * time.Second

func lsRun(t *testing.T, script, dir string, extraPaths ...string) (string, string, int) {
	t.Helper()
	paths := append([]string{dir}, extraPaths...)
	return testutil.RunScript(t, script, dir, interp.AllowedPaths(paths))
}

func mustNotHang(t *testing.T, fn func()) {
	t.Helper()
	done := make(chan struct{})
	go func() {
		fn()
		close(done)
	}()
	select {
	case <-done:
	case <-time.After(pentestTimeout):
		t.Fatal("operation did not complete within timeout")
	}
}

// --- Symlink loop and follow tests ---

func TestLsPentestSymlinkLoopRecursive(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("symlinks not reliably supported on Windows")
	}
	dir := t.TempDir()
	sub := filepath.Join(dir, "sub")
	require.NoError(t, os.Mkdir(sub, 0755))
	require.NoError(t, os.Symlink(sub, filepath.Join(sub, "loop")))

	mustNotHang(t, func() {
		stdout, _, code := lsRun(t, "ls -R", dir)
		assert.Equal(t, 0, code)
		// The symlink "loop" should appear in listing but not be recursed into.
		assert.Contains(t, stdout, "loop")
		// If recursed, we'd see "loop" repeated in subdir headers.
		// Count occurrences of "./sub" headers — should be exactly one.
		count := strings.Count(stdout, "./sub:")
		assert.Equal(t, 1, count, "symlink should not be recursed into; got stdout:\n%s", stdout)
	})
}

func TestLsPentestSymlinkToDirectoryRecursive(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("symlinks not reliably supported on Windows")
	}
	dir := t.TempDir()
	// Create a real subdir with a file.
	realSub := filepath.Join(dir, "real")
	require.NoError(t, os.Mkdir(realSub, 0755))
	require.NoError(t, os.WriteFile(filepath.Join(realSub, "file.txt"), []byte("hello"), 0644))
	// Create a symlink to the real subdir.
	require.NoError(t, os.Symlink(realSub, filepath.Join(dir, "link")))

	stdout, _, code := lsRun(t, "ls -R", dir)
	assert.Equal(t, 0, code)
	// "link" should appear in the listing.
	assert.Contains(t, stdout, "link")
	// "real" subdir should be recursed into.
	assert.Contains(t, stdout, "./real:")
	// "link" should NOT be recursed into (no "./link:" header).
	assert.NotContains(t, stdout, "./link:")
}

// --- Recursion depth ---

func TestLsPentestDeepNesting(t *testing.T) {
	dir := t.TempDir()
	// Create directories 300 levels deep (exceeds maxRecursionDepth=256).
	cur := dir
	for i := 0; i < 300; i++ {
		cur = filepath.Join(cur, "d")
		require.NoError(t, os.Mkdir(cur, 0755))
	}

	mustNotHang(t, func() {
		_, stderr, code := lsRun(t, "ls -R", dir)
		assert.Equal(t, 1, code)
		assert.Contains(t, stderr, "recursion depth limit exceeded")
	})
}

// --- MaxDirEntries ---

func TestLsPentestMaxDirEntries(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping in short mode")
	}
	dir := t.TempDir()
	// Create MaxDirEntries+1 files.
	count := ls.MaxDirEntries + 1
	for i := 0; i < count; i++ {
		name := fmt.Sprintf("f%06d", i)
		require.NoError(t, os.WriteFile(filepath.Join(dir, name), nil, 0644))
	}

	mustNotHang(t, func() {
		stdout, stderr, code := lsRun(t, "ls", dir)
		assert.Equal(t, 1, code, "should exit 1 on truncation")
		assert.Contains(t, stderr, "too many entries")
		assert.Contains(t, stderr, "output truncated")
		assert.NotEmpty(t, stdout, "should show truncated output, not suppress it")

		// Should contain the first entry but not the last.
		assert.Contains(t, stdout, "f000000")
		lastEntry := fmt.Sprintf("f%06d", ls.MaxDirEntries)
		assert.NotContains(t, stdout, lastEntry, "entry beyond limit should not appear")

		// Exactly MaxDirEntries lines of output.
		lines := strings.Split(strings.TrimRight(stdout, "\n"), "\n")
		assert.Equal(t, ls.MaxDirEntries, len(lines), "should have exactly MaxDirEntries lines")
	})
}

func TestLsPentestPagination(t *testing.T) {
	dir := t.TempDir()
	// Create 50 files: f00..f49
	for i := 0; i < 50; i++ {
		name := fmt.Sprintf("f%02d", i)
		require.NoError(t, os.WriteFile(filepath.Join(dir, name), nil, 0644))
	}

	t.Run("offset 10 limit 20 returns 20 sorted entries", func(t *testing.T) {
		stdout, stderr, code := lsRun(t, "ls --offset 10 --limit 20", dir)
		assert.Equal(t, 0, code)
		assert.Empty(t, stderr)
		lines := strings.Split(strings.TrimRight(stdout, "\n"), "\n")
		assert.Equal(t, 20, len(lines))
		// Entries should be sorted within the page.
		for i := 1; i < len(lines); i++ {
			assert.True(t, lines[i-1] <= lines[i], "entries should be sorted: %s <= %s", lines[i-1], lines[i])
		}
	})

	t.Run("offset 40 limit 20 returns remaining entries", func(t *testing.T) {
		stdout, stderr, code := lsRun(t, "ls --offset 40 --limit 20", dir)
		assert.Equal(t, 0, code)
		assert.Empty(t, stderr)
		lines := strings.Split(strings.TrimRight(stdout, "\n"), "\n")
		assert.Equal(t, 10, len(lines), "only 10 entries remain after offset 40 of 50")
	})

	t.Run("offset beyond count returns empty", func(t *testing.T) {
		stdout, stderr, code := lsRun(t, "ls --offset 100 --limit 10", dir)
		assert.Equal(t, 0, code)
		assert.Empty(t, stderr)
		assert.Empty(t, stdout)
	})
}

func TestLsPentestNegativeOffset(t *testing.T) {
	dir := t.TempDir()
	for i := 0; i < 5; i++ {
		require.NoError(t, os.WriteFile(filepath.Join(dir, fmt.Sprintf("f%02d", i)), nil, 0644))
	}

	// Negative offset should be clamped to 0 (behaves like no offset).
	stdout, stderr, code := lsRun(t, "ls --offset -1", dir)
	assert.Equal(t, 0, code)
	assert.Empty(t, stderr)
	lines := strings.Split(strings.TrimRight(stdout, "\n"), "\n")
	assert.Equal(t, 5, len(lines))
	assert.Equal(t, "f00", lines[0])
}

func TestLsPentestDotDotDoesNotConsumeLimitSlots(t *testing.T) {
	dir := t.TempDir()
	// Create 20 files: f00..f19
	for i := 0; i < 20; i++ {
		require.NoError(t, os.WriteFile(filepath.Join(dir, fmt.Sprintf("f%02d", i)), nil, 0644))
	}

	// ls -a --limit 20 should show ".", "..", and all 20 real files (22 lines).
	stdout, stderr, code := lsRun(t, "ls -a --limit 20", dir)
	assert.Equal(t, 0, code)
	assert.Empty(t, stderr)
	lines := strings.Split(strings.TrimRight(stdout, "\n"), "\n")
	// 20 real files + "." + ".." = 22 lines.
	assert.Equal(t, 22, len(lines), "dot and dotdot should not consume limit slots; got:\n%s", stdout)
	assert.Equal(t, ".", lines[0])
	assert.Equal(t, "..", lines[1])
	assert.Equal(t, "f00", lines[2])
	assert.Equal(t, "f19", lines[len(lines)-1])
}

// --- Large offset DoS ---

func TestLsPentestLargeOffsetCapped(t *testing.T) {
	dir := t.TempDir()
	for i := 0; i < 5; i++ {
		require.NoError(t, os.WriteFile(filepath.Join(dir, fmt.Sprintf("f%02d", i)), nil, 0644))
	}

	mustNotHang(t, func() {
		stdout, stderr, code := lsRun(t, "ls --offset 999999 --limit 1", dir)
		assert.Equal(t, 0, code)
		assert.Empty(t, stderr)
		assert.Empty(t, stdout, "offset far beyond entry count should return empty")
	})
}

// --- Flag injection ---

func TestLsPentestUnknownFlag(t *testing.T) {
	dir := t.TempDir()
	_, stderr, code := lsRun(t, "ls --no-such-flag", dir)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "ls:")
}

func TestLsPentestFlagViaExpansion(t *testing.T) {
	dir := t.TempDir()
	_, stderr, code := lsRun(t, `flag="--no-such-flag"; ls $flag`, dir)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "ls:")
}

// --- Sandbox escape ---

func TestLsPentestOutsideSandbox(t *testing.T) {
	allowed := t.TempDir()
	forbidden := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(forbidden, "secret.txt"), []byte("secret"), 0644))

	forbiddenPath := strings.ReplaceAll(forbidden, `\`, `/`)
	_, stderr, code := lsRun(t, "ls "+forbiddenPath, allowed)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "ls:")
}

func TestLsPentestPathTraversal(t *testing.T) {
	dir := t.TempDir()
	_, stderr, code := lsRun(t, "ls ../../etc/passwd", dir)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "ls:")
}

// --- Edge cases ---

func TestLsPentestEmptyFilename(t *testing.T) {
	dir := t.TempDir()
	_, stderr, code := lsRun(t, `ls ""`, dir)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "ls:")
}

func TestLsPentestDashPrefixedFilename(t *testing.T) {
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "-n"), []byte("content"), 0644))
	stdout, _, code := lsRun(t, "ls -- -n", dir)
	assert.Equal(t, 0, code)
	assert.Contains(t, stdout, "-n")
}

func TestLsPentestRecursiveLimitIgnored(t *testing.T) {
	dir := t.TempDir()
	// Create 3 subdirectories, each with a file.
	for _, name := range []string{"aaa", "bbb", "ccc"} {
		sub := filepath.Join(dir, name)
		require.NoError(t, os.Mkdir(sub, 0755))
		require.NoError(t, os.WriteFile(filepath.Join(sub, "file.txt"), nil, 0644))
	}

	// ls -R --limit 1 should still recurse into all 3 subdirs because
	// --limit is silently ignored in recursive mode.
	stdout, _, code := lsRun(t, "ls -R --limit 1", dir)
	assert.Equal(t, 0, code)
	assert.Contains(t, stdout, "./aaa:")
	assert.Contains(t, stdout, "./bbb:")
	assert.Contains(t, stdout, "./ccc:")
}

func TestLsPentestMultiDirLimitIgnored(t *testing.T) {
	dir := t.TempDir()
	// Create two subdirs, each with 3 files.
	for _, sub := range []string{"d1", "d2"} {
		subDir := filepath.Join(dir, sub)
		require.NoError(t, os.Mkdir(subDir, 0755))
		for i := 0; i < 3; i++ {
			require.NoError(t, os.WriteFile(filepath.Join(subDir, fmt.Sprintf("f%02d", i)), nil, 0644))
		}
	}

	// ls --limit 1 d1 d2 should show ALL entries in both dirs because
	// --limit is silently ignored with multiple arguments.
	stdout, stderr, code := lsRun(t, "ls --limit 1 d1 d2", dir)
	assert.Equal(t, 0, code)
	assert.Empty(t, stderr)
	// Both directories should show all 3 files.
	lines := strings.Split(strings.TrimRight(stdout, "\n"), "\n")
	// d1:\nf00\nf01\nf02\n\nd2:\nf00\nf01\nf02 => 9 lines (2 headers + 6 files + 1 blank separator)
	assert.Equal(t, 9, len(lines), "should show all entries in both dirs; got:\n%s", stdout)
}

func TestLsPentestLargeOffsetStreaming(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping in short mode")
	}
	dir := t.TempDir()
	// Create 5010 files to test streaming offset beyond MaxDirEntries.
	for i := 0; i < 5010; i++ {
		require.NoError(t, os.WriteFile(filepath.Join(dir, fmt.Sprintf("f%06d", i)), nil, 0644))
	}

	mustNotHang(t, func() {
		stdout, stderr, code := lsRun(t, "ls --offset 5000 --limit 10", dir)
		assert.Equal(t, 0, code)
		assert.Empty(t, stderr)
		lines := strings.Split(strings.TrimRight(stdout, "\n"), "\n")
		assert.Equal(t, 10, len(lines), "should return exactly 10 entries; got:\n%s", stdout)
	})
}

func TestLsPentestNegativeLimit(t *testing.T) {
	dir := t.TempDir()
	for i := 0; i < 5; i++ {
		require.NoError(t, os.WriteFile(filepath.Join(dir, fmt.Sprintf("f%02d", i)), nil, 0644))
	}

	// Negative limit should be clamped to 0 (behaves like no limit).
	stdout, stderr, code := lsRun(t, "ls --limit -1", dir)
	assert.Equal(t, 0, code)
	assert.Empty(t, stderr)
	lines := strings.Split(strings.TrimRight(stdout, "\n"), "\n")
	assert.Equal(t, 5, len(lines), "negative limit should show all entries")
}

func TestLsPentestRecursiveOffsetIgnored(t *testing.T) {
	dir := t.TempDir()
	for _, name := range []string{"aaa", "bbb", "ccc"} {
		sub := filepath.Join(dir, name)
		require.NoError(t, os.Mkdir(sub, 0755))
		require.NoError(t, os.WriteFile(filepath.Join(sub, "file.txt"), nil, 0644))
	}

	// ls -R --offset 100 should still show all 3 subdirs because
	// --offset is silently ignored in recursive mode.
	stdout, _, code := lsRun(t, "ls -R --offset 100", dir)
	assert.Equal(t, 0, code)
	assert.Contains(t, stdout, "./aaa:")
	assert.Contains(t, stdout, "./bbb:")
	assert.Contains(t, stdout, "./ccc:")
}

func TestLsPentestMultiDirOffsetIgnored(t *testing.T) {
	dir := t.TempDir()
	for _, sub := range []string{"d1", "d2"} {
		subDir := filepath.Join(dir, sub)
		require.NoError(t, os.Mkdir(subDir, 0755))
		for i := 0; i < 3; i++ {
			require.NoError(t, os.WriteFile(filepath.Join(subDir, fmt.Sprintf("f%02d", i)), nil, 0644))
		}
	}

	// ls --offset 100 d1 d2 should show ALL entries because
	// --offset is silently ignored with multiple arguments.
	stdout, stderr, code := lsRun(t, "ls --offset 100 d1 d2", dir)
	assert.Equal(t, 0, code)
	assert.Empty(t, stderr)
	assert.Contains(t, stdout, "d1:")
	assert.Contains(t, stdout, "d2:")
	// Both dirs should show all 3 files.
	assert.Equal(t, 6, strings.Count(stdout, "f0"), "should show all 6 files across both dirs")
}

func TestLsPentestDotDotNotAffectedByOffset(t *testing.T) {
	dir := t.TempDir()
	for i := 0; i < 10; i++ {
		require.NoError(t, os.WriteFile(filepath.Join(dir, fmt.Sprintf("f%02d", i)), nil, 0644))
	}

	// ls -a --offset 5 --limit 5: offset skips 5 raw entries, reads 5, then
	// prepends . and .. (which are never consumed by offset/limit).
	stdout, stderr, code := lsRun(t, "ls -a --offset 5 --limit 5", dir)
	assert.Equal(t, 0, code)
	assert.Empty(t, stderr)
	lines := strings.Split(strings.TrimRight(stdout, "\n"), "\n")
	// 5 real entries + "." + ".." = 7 lines.
	assert.Equal(t, 7, len(lines), "dot/dotdot should always appear; got:\n%s", stdout)
	assert.Equal(t, ".", lines[0])
	assert.Equal(t, "..", lines[1])
}

func TestLsPentestNoTruncationWarningWithPagination(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping in short mode")
	}
	dir := t.TempDir()
	count := ls.MaxDirEntries + 100
	for i := 0; i < count; i++ {
		require.NoError(t, os.WriteFile(filepath.Join(dir, fmt.Sprintf("f%06d", i)), nil, 0644))
	}

	// With explicit --limit, no warning should appear even though
	// the directory has more entries than MaxDirEntries.
	stdout, stderr, code := lsRun(t, "ls --limit 10", dir)
	assert.Equal(t, 0, code, "should exit 0 with explicit pagination")
	assert.Empty(t, stderr, "no truncation warning with explicit --limit")
	lines := strings.Split(strings.TrimRight(stdout, "\n"), "\n")
	assert.Equal(t, 10, len(lines))

	// With explicit --offset, same behavior.
	stdout, stderr, code = lsRun(t, "ls --offset 10 --limit 10", dir)
	assert.Equal(t, 0, code, "should exit 0 with explicit pagination")
	assert.Empty(t, stderr, "no truncation warning with explicit --offset/--limit")
	lines = strings.Split(strings.TrimRight(stdout, "\n"), "\n")
	assert.Equal(t, 10, len(lines))
}

func TestLsPentestImplicitTruncationWarning(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping in short mode")
	}
	dir := t.TempDir()
	count := ls.MaxDirEntries + 1
	for i := 0; i < count; i++ {
		require.NoError(t, os.WriteFile(filepath.Join(dir, fmt.Sprintf("f%06d", i)), nil, 0644))
	}

	// Without pagination flags, truncation should produce warning + exit 1.
	_, stderr, code := lsRun(t, "ls", dir)
	assert.Equal(t, 1, code, "implicit truncation must exit 1")
	assert.Contains(t, stderr, "too many entries")
	assert.Contains(t, stderr, "output truncated")
}

func TestLsPentestRecursivePerDirCap(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping in short mode")
	}
	dir := t.TempDir()
	// Create a subdir with MaxDirEntries+1 files.
	sub := filepath.Join(dir, "big")
	require.NoError(t, os.Mkdir(sub, 0755))
	for i := 0; i < ls.MaxDirEntries+1; i++ {
		require.NoError(t, os.WriteFile(filepath.Join(sub, fmt.Sprintf("f%06d", i)), nil, 0644))
	}

	mustNotHang(t, func() {
		_, stderr, code := lsRun(t, "ls -R", dir)
		assert.Equal(t, 1, code, "recursive should still exit 1 on per-dir truncation")
		assert.Contains(t, stderr, "too many entries")
	})
}

func TestLsPentestDotfilesDoNotBypassCap(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping in short mode")
	}
	dir := t.TempDir()
	// Create MaxDirEntries dotfiles + 10 regular files.
	// Without raw cap, reading all dotfiles to find visible ones would be unbounded.
	for i := 0; i < ls.MaxDirEntries; i++ {
		require.NoError(t, os.WriteFile(filepath.Join(dir, fmt.Sprintf(".hidden%06d", i)), nil, 0644))
	}
	for i := 0; i < 10; i++ {
		require.NoError(t, os.WriteFile(filepath.Join(dir, fmt.Sprintf("visible%02d", i)), nil, 0644))
	}

	mustNotHang(t, func() {
		// ls (without -a) sees only visible files that fall within the raw cap window.
		// The key invariant: ls must not read beyond MaxDirEntries raw entries.
		stdout, stderr, code := lsRun(t, "ls", dir)
		// Depending on filesystem order, we may or may not see all visible files.
		// The important thing is that it completes and doesn't OOM.
		_ = stdout
		// With MaxDirEntries dotfiles + 10 visible files = 1010 total > MaxDirEntries,
		// so truncation warning should appear.
		assert.Equal(t, 1, code)
		assert.Contains(t, stderr, "too many entries")
	})
}

func TestLsPentestLimitOnly(t *testing.T) {
	dir := t.TempDir()
	for i := 0; i < 20; i++ {
		require.NoError(t, os.WriteFile(filepath.Join(dir, fmt.Sprintf("f%02d", i)), nil, 0644))
	}

	stdout, stderr, code := lsRun(t, "ls --limit 5", dir)
	assert.Equal(t, 0, code)
	assert.Empty(t, stderr)
	lines := strings.Split(strings.TrimRight(stdout, "\n"), "\n")
	assert.Equal(t, 5, len(lines), "limit-only should cap output")
	// Entries should be sorted.
	for i := 1; i < len(lines); i++ {
		assert.True(t, lines[i-1] <= lines[i], "entries should be sorted: %s <= %s", lines[i-1], lines[i])
	}
}

func TestLsPentestOffsetOnly(t *testing.T) {
	dir := t.TempDir()
	for i := 0; i < 20; i++ {
		require.NoError(t, os.WriteFile(filepath.Join(dir, fmt.Sprintf("f%02d", i)), nil, 0644))
	}

	// offset-only should skip entries and return the rest (up to MaxDirEntries).
	stdout, stderr, code := lsRun(t, "ls --offset 15", dir)
	assert.Equal(t, 0, code)
	assert.Empty(t, stderr)
	lines := strings.Split(strings.TrimRight(stdout, "\n"), "\n")
	assert.Equal(t, 5, len(lines), "offset 15 of 20 should return 5 entries")
}

func TestLsPentestLimitCappedAtMax(t *testing.T) {
	dir := t.TempDir()
	for i := 0; i < 10; i++ {
		require.NoError(t, os.WriteFile(filepath.Join(dir, fmt.Sprintf("f%02d", i)), nil, 0644))
	}

	// --limit 999999 should behave like --limit MaxDirEntries.
	stdout, stderr, code := lsRun(t, "ls --limit 999999", dir)
	assert.Equal(t, 0, code)
	assert.Empty(t, stderr)
	lines := strings.Split(strings.TrimRight(stdout, "\n"), "\n")
	assert.Equal(t, 10, len(lines), "all 10 entries should appear")
}

func TestLsPentestAlmostAllWithOffset(t *testing.T) {
	dir := t.TempDir()
	// Create some dotfiles and regular files.
	for _, name := range []string{".hidden1", ".hidden2", "visible1", "visible2", "visible3"} {
		require.NoError(t, os.WriteFile(filepath.Join(dir, name), nil, 0644))
	}

	// -A shows dotfiles (not . and ..) — offset applies to raw entries.
	stdout, stderr, code := lsRun(t, "ls -A --offset 2 --limit 10", dir)
	assert.Equal(t, 0, code)
	assert.Empty(t, stderr)
	lines := strings.Split(strings.TrimRight(stdout, "\n"), "\n")
	// Offset skips 2 raw filesystem entries; remaining 3 are returned.
	assert.Equal(t, 3, len(lines), "should show 3 entries after offset 2 of 5")
	// . and .. should NOT appear with -A.
	assert.NotContains(t, stdout, "\n.\n")
	assert.NotContains(t, stdout, "\n..\n")
}

func TestLsPentestPaginationWithSortFlags(t *testing.T) {
	dir := t.TempDir()
	for i := 0; i < 10; i++ {
		name := fmt.Sprintf("f%02d", i)
		require.NoError(t, os.WriteFile(filepath.Join(dir, name), []byte(strings.Repeat("x", (i+1)*100)), 0644))
	}

	t.Run("limit with reverse sort", func(t *testing.T) {
		stdout, stderr, code := lsRun(t, "ls -r --limit 5", dir)
		assert.Equal(t, 0, code)
		assert.Empty(t, stderr)
		lines := strings.Split(strings.TrimRight(stdout, "\n"), "\n")
		assert.Equal(t, 5, len(lines))
		// Entries should be in reverse sorted order within the page.
		for i := 1; i < len(lines); i++ {
			assert.True(t, lines[i-1] >= lines[i], "entries should be reverse sorted: %s >= %s", lines[i-1], lines[i])
		}
	})

	t.Run("limit with size sort", func(t *testing.T) {
		stdout, stderr, code := lsRun(t, "ls -S --limit 3", dir)
		assert.Equal(t, 0, code)
		assert.Empty(t, stderr)
		lines := strings.Split(strings.TrimRight(stdout, "\n"), "\n")
		assert.Equal(t, 3, len(lines))
	})
}

func TestLsPentestRecursiveBothPaginationFlagsIgnored(t *testing.T) {
	dir := t.TempDir()
	for _, name := range []string{"aaa", "bbb"} {
		sub := filepath.Join(dir, name)
		require.NoError(t, os.Mkdir(sub, 0755))
		for i := 0; i < 5; i++ {
			require.NoError(t, os.WriteFile(filepath.Join(sub, fmt.Sprintf("f%02d", i)), nil, 0644))
		}
	}

	// ls -R --offset 100 --limit 1 should show everything.
	stdout, _, code := lsRun(t, "ls -R --offset 100 --limit 1", dir)
	assert.Equal(t, 0, code)
	assert.Contains(t, stdout, "./aaa:")
	assert.Contains(t, stdout, "./bbb:")
	// Each subdir should have all 5 files.
	assert.Equal(t, 10, strings.Count(stdout, "f0"), "all entries should appear in recursive mode")
}

func TestLsPentestMultiDirBothPaginationFlagsIgnored(t *testing.T) {
	dir := t.TempDir()
	for _, sub := range []string{"d1", "d2"} {
		subDir := filepath.Join(dir, sub)
		require.NoError(t, os.Mkdir(subDir, 0755))
		for i := 0; i < 5; i++ {
			require.NoError(t, os.WriteFile(filepath.Join(subDir, fmt.Sprintf("f%02d", i)), nil, 0644))
		}
	}

	stdout, stderr, code := lsRun(t, "ls --offset 100 --limit 1 d1 d2", dir)
	assert.Equal(t, 0, code)
	assert.Empty(t, stderr)
	assert.Contains(t, stdout, "d1:")
	assert.Contains(t, stdout, "d2:")
	assert.Equal(t, 10, strings.Count(stdout, "f0"), "all entries should appear with multi-dir")
}

func TestLsPentestTruncatedOutputShowsPartialResults(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping in short mode")
	}
	dir := t.TempDir()
	count := ls.MaxDirEntries + 50
	for i := 0; i < count; i++ {
		require.NoError(t, os.WriteFile(filepath.Join(dir, fmt.Sprintf("f%06d", i)), nil, 0644))
	}

	stdout, stderr, code := lsRun(t, "ls", dir)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "output truncated")
	// Must have exactly MaxDirEntries lines (partial output shown before warning).
	lines := strings.Split(strings.TrimRight(stdout, "\n"), "\n")
	assert.Equal(t, ls.MaxDirEntries, len(lines), "should show exactly MaxDirEntries entries before warning")
	assert.NotEmpty(t, stdout, "must print partial results before warning")
}

func TestLsPentestReadDirLimitedBoundsActualRead(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping in short mode")
	}
	dir := t.TempDir()
	// Create 2x MaxDirEntries files.
	count := ls.MaxDirEntries * 2
	for i := 0; i < count; i++ {
		require.NoError(t, os.WriteFile(filepath.Join(dir, fmt.Sprintf("f%06d", i)), nil, 0644))
	}

	mustNotHang(t, func() {
		stdout, stderr, code := lsRun(t, "ls", dir)
		assert.Equal(t, 1, code)
		assert.Contains(t, stderr, "too many entries")
		lines := strings.Split(strings.TrimRight(stdout, "\n"), "\n")
		// Must show exactly MaxDirEntries, not more.
		assert.Equal(t, ls.MaxDirEntries, len(lines))
	})
}

func TestLsPentestZeroOffsetZeroLimitNoPagination(t *testing.T) {
	dir := t.TempDir()
	for i := 0; i < 10; i++ {
		require.NoError(t, os.WriteFile(filepath.Join(dir, fmt.Sprintf("f%02d", i)), nil, 0644))
	}

	// Explicit zero values should behave identically to omitting the flags.
	stdout, stderr, code := lsRun(t, "ls --offset 0 --limit 0", dir)
	assert.Equal(t, 0, code)
	assert.Empty(t, stderr)
	lines := strings.Split(strings.TrimRight(stdout, "\n"), "\n")
	assert.Equal(t, 10, len(lines), "offset=0 limit=0 should show all entries")
}

func TestLsPentestDirectoryFlagWithPagination(t *testing.T) {
	dir := t.TempDir()
	sub := filepath.Join(dir, "sub")
	require.NoError(t, os.Mkdir(sub, 0755))
	for i := 0; i < 5; i++ {
		require.NoError(t, os.WriteFile(filepath.Join(sub, fmt.Sprintf("f%02d", i)), nil, 0644))
	}

	// ls -d with --limit shouldn't crash and should list the directory itself.
	stdout, stderr, code := lsRun(t, "ls -d --limit 1 sub", dir)
	assert.Equal(t, 0, code)
	assert.Empty(t, stderr)
	assert.Contains(t, stdout, "sub")
}

func TestLsPentestPaginationWithAllFlag(t *testing.T) {
	dir := t.TempDir()
	for i := 0; i < 10; i++ {
		require.NoError(t, os.WriteFile(filepath.Join(dir, fmt.Sprintf("f%02d", i)), nil, 0644))
	}

	// -a --limit 3: should show ".", "..", and 3 real entries = 5 lines.
	stdout, stderr, code := lsRun(t, "ls -a --limit 3", dir)
	assert.Equal(t, 0, code)
	assert.Empty(t, stderr)
	lines := strings.Split(strings.TrimRight(stdout, "\n"), "\n")
	assert.Equal(t, 5, len(lines), "3 real entries + . + .. = 5; got:\n%s", stdout)
	assert.Equal(t, ".", lines[0])
	assert.Equal(t, "..", lines[1])
}

func TestLsPentestPaginationExitZeroOnHugeDir(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping in short mode")
	}
	dir := t.TempDir()
	count := ls.MaxDirEntries + 500
	for i := 0; i < count; i++ {
		require.NoError(t, os.WriteFile(filepath.Join(dir, fmt.Sprintf("f%06d", i)), nil, 0644))
	}

	// With pagination active, exit code should be 0 even on oversized dir.
	_, stderr, code := lsRun(t, "ls --offset 0 --limit 100", dir)
	assert.Equal(t, 0, code, "pagination should suppress truncation failure")
	assert.Empty(t, stderr, "no warning with explicit pagination")
}

func TestLsPentestSpecialCharFilenames(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("special characters in filenames may not be supported on Windows")
	}
	dir := t.TempDir()
	names := []string{"file with spaces", "café", "日本語"}
	for _, name := range names {
		require.NoError(t, os.WriteFile(filepath.Join(dir, name), nil, 0644))
	}
	stdout, _, code := lsRun(t, "ls", dir)
	assert.Equal(t, 0, code)
	for _, name := range names {
		assert.Contains(t, stdout, name)
	}
}
