// 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.

// Exploratory pentest for the tail builtin.
//
// These tests probe integer edge cases, special files, long lines, resource
// exhaustion, path edge cases, and flag injection scenarios. Tests that might
// hang are run in a goroutine with time.After to bound execution.

package tail_test

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

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

	"github.com/DataDog/rshell/interp"
)

const pentestTimeout = 10 * time.Second

// tailRun is a shorthand for runScript with AllowedPaths=dir.
func tailRun(t *testing.T, script, dir string, extraPaths ...string) (stdout, stderr string, exitCode int) {
	t.Helper()
	paths := append([]string{dir}, extraPaths...)
	return runScript(t, script, dir, interp.AllowedPaths(paths))
}

// mustNotHang runs f in a goroutine and fails the test if it does not return
// within pentestTimeout.
func mustNotHang(t *testing.T, f func()) {
	t.Helper()
	done := make(chan struct{})
	go func() {
		defer close(done)
		f()
	}()
	select {
	case <-done:
	case <-time.After(pentestTimeout):
		t.Fatalf("operation did not complete within %s", pentestTimeout)
	}
}

// --- Integer edge cases: -n ---

func TestCmdPentestTailLinesNZero(t *testing.T) {
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "f.txt"), []byte("hello\n"), 0644))
	stdout, _, code := tailRun(t, "tail -n 0 f.txt", dir)
	assert.Equal(t, 0, code)
	assert.Equal(t, "", stdout)
}

func TestCmdPentestTailLinesNOne(t *testing.T) {
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "f.txt"), []byte("line1\nline2\n"), 0644))
	stdout, _, code := tailRun(t, "tail -n 1 f.txt", dir)
	assert.Equal(t, 0, code)
	assert.Equal(t, "line2\n", stdout)
}

func TestCmdPentestTailLinesNMaxInt32(t *testing.T) {
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "f.txt"), []byte("tiny\n"), 0644))
	mustNotHang(t, func() {
		stdout, _, code := tailRun(t, "tail -n 2147483647 f.txt", dir)
		assert.Equal(t, 0, code)
		assert.Equal(t, "tiny\n", stdout)
	})
}

func TestCmdPentestTailLinesNMaxInt64(t *testing.T) {
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "f.txt"), []byte("tiny\n"), 0644))
	mustNotHang(t, func() {
		stdout, _, code := tailRun(t, "tail -n 9223372036854775807 f.txt", dir)
		assert.Equal(t, 0, code)
		assert.Equal(t, "tiny\n", stdout)
	})
}

func TestCmdPentestTailLinesNMaxInt64PlusOne(t *testing.T) {
	// 9223372036854775808 overflows int64 but fits in uint64, so it is
	// clamped to MaxCount and the file is printed (matching GNU tail behaviour).
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "f.txt"), []byte("tiny\n"), 0644))
	stdout, _, code := tailRun(t, "tail -n 9223372036854775808 f.txt", dir)
	assert.Equal(t, 0, code)
	assert.Equal(t, "tiny\n", stdout)
}

func TestCmdPentestTailLinesNHugeOverflow(t *testing.T) {
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "f.txt"), []byte("tiny\n"), 0644))
	_, stderr, code := tailRun(t, "tail -n 99999999999999999999 f.txt", dir)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "tail:")
}

func TestCmdPentestTailLinesNNegativeOne(t *testing.T) {
	// GNU tail treats -n -1 as -n 1 (absolute value). Verify we match.
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "f.txt"), []byte("hello\n"), 0644))
	stdout, _, code := tailRun(t, "tail -n -1 f.txt", dir)
	assert.Equal(t, 0, code)
	assert.Equal(t, "hello\n", stdout)
}

func TestCmdPentestTailLinesNMinInt64(t *testing.T) {
	// -(-9223372036854775808) overflows; clamped to MaxCount, so the file is printed (GNU tail behaviour).
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "f.txt"), []byte("hello\n"), 0644))
	stdout, _, code := tailRun(t, "tail -n -9223372036854775808 f.txt", dir)
	assert.Equal(t, 0, code)
	assert.Equal(t, "hello\n", stdout)
}

func TestCmdPentestTailBytesNMinInt64(t *testing.T) {
	// Same overflow clamp for -c.
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "f.txt"), []byte("hello\n"), 0644))
	stdout, _, code := tailRun(t, "tail -c -9223372036854775808 f.txt", dir)
	assert.Equal(t, 0, code)
	assert.Equal(t, "hello\n", stdout)
}

func TestCmdPentestTailLinesNPlusZero(t *testing.T) {
	// "+0" offset mode: skip 0 lines = output everything.
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "f.txt"), []byte("alpha\nbeta\n"), 0644))
	stdout, _, code := tailRun(t, "tail -n +0 f.txt", dir)
	assert.Equal(t, 0, code)
	assert.Equal(t, "alpha\nbeta\n", stdout)
}

func TestCmdPentestTailLinesNPlusOne(t *testing.T) {
	// "+1" offset mode: from line 1 = all lines.
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "f.txt"), []byte("alpha\nbeta\n"), 0644))
	stdout, _, code := tailRun(t, "tail -n +1 f.txt", dir)
	assert.Equal(t, 0, code)
	assert.Equal(t, "alpha\nbeta\n", stdout)
}

func TestCmdPentestTailLinesNPlusTwo(t *testing.T) {
	// "+2" offset mode: from line 2 = skip 1 line.
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "f.txt"), []byte("alpha\nbeta\ngamma\n"), 0644))
	stdout, _, code := tailRun(t, "tail -n +2 f.txt", dir)
	assert.Equal(t, 0, code)
	assert.Equal(t, "beta\ngamma\n", stdout)
}

func TestCmdPentestTailLinesNEmptyString(t *testing.T) {
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "f.txt"), []byte("hello\n"), 0644))
	_, stderr, code := tailRun(t, `tail -n "" f.txt`, dir)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "tail:")
}

func TestCmdPentestTailLinesNWhitespace(t *testing.T) {
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "f.txt"), []byte("hello\n"), 0644))
	_, stderr, code := tailRun(t, `tail -n "   " f.txt`, dir)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "tail:")
}

// --- Integer edge cases: -c ---

func TestCmdPentestTailBytesNZero(t *testing.T) {
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "f.txt"), []byte("hello"), 0644))
	stdout, _, code := tailRun(t, "tail -c 0 f.txt", dir)
	assert.Equal(t, 0, code)
	assert.Equal(t, "", stdout)
}

func TestCmdPentestTailBytesNOne(t *testing.T) {
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "f.txt"), []byte("hello"), 0644))
	stdout, _, code := tailRun(t, "tail -c 1 f.txt", dir)
	assert.Equal(t, 0, code)
	assert.Equal(t, "o", stdout)
}

func TestCmdPentestTailBytesNMaxInt32(t *testing.T) {
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "f.txt"), []byte("tiny"), 0644))
	mustNotHang(t, func() {
		stdout, _, code := tailRun(t, "tail -c 2147483647 f.txt", dir)
		assert.Equal(t, 0, code)
		assert.Equal(t, "tiny", stdout)
	})
}

func TestCmdPentestTailBytesNHugeOverflow(t *testing.T) {
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "f.txt"), []byte("tiny"), 0644))
	_, stderr, code := tailRun(t, "tail -c 99999999999999999999 f.txt", dir)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "tail:")
}

func TestCmdPentestTailBytesNNegative(t *testing.T) {
	// GNU tail treats -c -1 as -c 1 (absolute value). Verify we match.
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "f.txt"), []byte("hello"), 0644))
	stdout, _, code := tailRun(t, "tail -c -1 f.txt", dir)
	assert.Equal(t, 0, code)
	assert.Equal(t, "o", stdout)
}

func TestCmdPentestTailBytesOffsetPlus1(t *testing.T) {
	// "+1" = from byte 1 = all bytes.
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "f.txt"), []byte("hello"), 0644))
	mustNotHang(t, func() {
		stdout, _, code := tailRun(t, "tail -c +1 f.txt", dir)
		assert.Equal(t, 0, code)
		assert.Equal(t, "hello", stdout)
	})
}

// --- Special files / infinite sources (Unix only) ---

func TestCmdPentestTailDevNull(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("no /dev/null on Windows")
	}
	dir := t.TempDir()
	stdout, _, code := tailRun(t, "tail /dev/null", dir, "/dev")
	assert.Equal(t, 0, code)
	assert.Equal(t, "", stdout)
}

func TestCmdPentestTailDevZeroByteMode(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("no /dev/zero on Windows")
	}
	// tail -c 10 /dev/zero: /dev/zero is not a regular file, so the
	// MaxTotalReadBytes (256 MiB) infinite-stream guard applies. The command
	// must terminate with an error rather than hang indefinitely.
	dir := t.TempDir()
	mustNotHang(t, func() {
		_, stderr, code := tailRun(t, "tail -c 10 /dev/zero", dir, "/dev")
		assert.Equal(t, 1, code)
		assert.Contains(t, stderr, "tail:")
	})
}

// --- Long lines ---

func TestCmdPentestTailLongLineBelowCap(t *testing.T) {
	dir := t.TempDir()
	content := bytes.Repeat([]byte("a"), 1<<20-1)
	content = append(content, '\n')
	require.NoError(t, os.WriteFile(filepath.Join(dir, "long.txt"), content, 0644))
	mustNotHang(t, func() {
		stdout, _, code := tailRun(t, "tail -n 1 long.txt", dir)
		assert.Equal(t, 0, code)
		assert.Equal(t, string(content), stdout)
	})
}

func TestCmdPentestTailLongLineExactlyAtCap(t *testing.T) {
	dir := t.TempDir()
	content := bytes.Repeat([]byte("a"), 1<<20)
	require.NoError(t, os.WriteFile(filepath.Join(dir, "exact.txt"), content, 0644))
	mustNotHang(t, func() {
		_, stderr, code := tailRun(t, "tail -n 1 exact.txt", dir)
		assert.Equal(t, 1, code)
		assert.Contains(t, stderr, "tail:")
	})
}

func TestCmdPentestTailLongLineExceedsCap(t *testing.T) {
	dir := t.TempDir()
	content := bytes.Repeat([]byte("a"), 1<<20+1)
	require.NoError(t, os.WriteFile(filepath.Join(dir, "huge.txt"), content, 0644))
	mustNotHang(t, func() {
		_, stderr, code := tailRun(t, "tail -n 1 huge.txt", dir)
		assert.Equal(t, 1, code)
		assert.Contains(t, stderr, "tail:")
	})
}

// --- Memory / resource exhaustion ---

func TestCmdPentestTailMaxInt32CountSmallFile(t *testing.T) {
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "small.txt"), []byte("small\n"), 0644))
	mustNotHang(t, func() {
		stdout, _, code := tailRun(t, "tail -n 2147483647 small.txt", dir)
		assert.Equal(t, 0, code)
		assert.Equal(t, "small\n", stdout)
	})
}

func TestCmdPentestTailMaxInt32ByteCountSmallFile(t *testing.T) {
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "small.txt"), []byte("tiny"), 0644))
	mustNotHang(t, func() {
		stdout, _, code := tailRun(t, "tail -c 2147483647 small.txt", dir)
		assert.Equal(t, 0, code)
		assert.Equal(t, "tiny", stdout)
	})
}

func TestCmdPentestTailManyFileArguments(t *testing.T) {
	dir := t.TempDir()
	var sb strings.Builder
	sb.WriteString("tail -q -n 1")
	for i := 0; i < 210; i++ {
		name := fmt.Sprintf("f%03d.txt", i)
		require.NoError(t, os.WriteFile(filepath.Join(dir, name), []byte(fmt.Sprintf("file%d\n", i)), 0644))
		sb.WriteString(" ")
		sb.WriteString(name)
	}
	mustNotHang(t, func() {
		stdout, _, code := tailRun(t, sb.String(), dir)
		assert.Equal(t, 0, code)
		assert.Equal(t, 210, strings.Count(stdout, "\n"))
	})
}

func TestCmdPentestTailMillionLineFile(t *testing.T) {
	// 1M-line file: tail -n 5 must output last 5 lines quickly.
	dir := t.TempDir()
	var buf bytes.Buffer
	for i := 0; i < 1_000_000; i++ {
		buf.WriteString("x\n")
	}
	require.NoError(t, os.WriteFile(filepath.Join(dir, "million.txt"), buf.Bytes(), 0644))
	done := make(chan struct{})
	go func() {
		defer close(done)
		stdout, _, code := tailRun(t, "tail -n 5 million.txt", dir)
		assert.Equal(t, 0, code)
		assert.Equal(t, "x\nx\nx\nx\nx\n", stdout)
	}()
	select {
	case <-done:
	case <-time.After(30 * time.Second):
		t.Fatal("tail on 1M-line file did not complete within 30s")
	}
}

// --- Path and filename edge cases ---

func TestCmdPentestTailNonexistentFile(t *testing.T) {
	dir := t.TempDir()
	_, stderr, code := tailRun(t, "tail does_not_exist.txt", dir)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "tail:")
}

func TestCmdPentestTailEmptyFilename(t *testing.T) {
	dir := t.TempDir()
	_, _, code := tailRun(t, `tail ""`, dir)
	assert.Equal(t, 1, code)
}

func TestCmdPentestTailFlagLikeName(t *testing.T) {
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "-n"), []byte("flag-file\n"), 0644))
	stdout, _, code := tailRun(t, "tail -- -n", dir)
	assert.Equal(t, 0, code)
	assert.Equal(t, "flag-file\n", stdout)
}

func TestCmdPentestTailDirectoryAsFile(t *testing.T) {
	dir := t.TempDir()
	require.NoError(t, os.Mkdir(filepath.Join(dir, "subdir"), 0755))
	_, stderr, code := tailRun(t, "tail subdir", dir)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "tail:")
}

func TestCmdPentestTailOutsideSandbox(t *testing.T) {
	allowed := t.TempDir()
	secret := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(secret, "s.txt"), []byte("secret"), 0644))
	secretPath := filepath.ToSlash(filepath.Join(secret, "s.txt"))
	_, stderr, code := runScript(t, "tail "+secretPath, allowed, interp.AllowedPaths([]string{allowed}))
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "tail:")
}

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

// --- Flag and argument injection ---

func TestCmdPentestTailUnknownLongFlag(t *testing.T) {
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "f.txt"), []byte("hello\n"), 0644))
	_, stderr, code := tailRun(t, "tail --no-such-flag f.txt", dir)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "tail:")
}

func TestCmdPentestTailFollowFlagRejected(t *testing.T) {
	// --follow / -f must be rejected by tail.
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "f.txt"), []byte("hello\n"), 0644))
	_, stderr, code := tailRun(t, "tail -f f.txt", dir)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "tail:")
}

func TestCmdPentestTailRetryFlagRejected(t *testing.T) {
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "f.txt"), []byte("hello\n"), 0644))
	_, stderr, code := tailRun(t, "tail --retry f.txt", dir)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "tail:")
}

func TestCmdPentestTailFlagViaExpansion(t *testing.T) {
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "f.txt"), []byte("hello\n"), 0644))
	_, stderr, code := tailRun(t, `flag="-f"; tail $flag f.txt`, dir)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "tail:")
}

// --- Circular buffer correctness ---

func TestCmdPentestTailBytesCircularBufferExact(t *testing.T) {
	// Write exactly bufSize bytes (32 MiB would be too slow; use 10 bytes).
	dir := t.TempDir()
	content := []byte("0123456789")
	require.NoError(t, os.WriteFile(filepath.Join(dir, "f.txt"), content, 0644))
	mustNotHang(t, func() {
		stdout, _, code := tailRun(t, "tail -c 5 f.txt", dir)
		assert.Equal(t, 0, code)
		assert.Equal(t, "56789", stdout)
	})
}

func TestCmdPentestTailBytesCircularBufferWrap(t *testing.T) {
	// Write more than bufSize would hold for a small count to exercise wrap.
	dir := t.TempDir()
	content := []byte("abcdefghij") // 10 bytes; request last 3 = "hij"
	require.NoError(t, os.WriteFile(filepath.Join(dir, "f.txt"), content, 0644))
	mustNotHang(t, func() {
		stdout, _, code := tailRun(t, "tail -c 3 f.txt", dir)
		assert.Equal(t, 0, code)
		assert.Equal(t, "hij", stdout)
	})
}

// --- Ring buffer correctness ---

func TestCmdPentestTailRingBufferExact(t *testing.T) {
	// Request exactly as many lines as exist.
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "f.txt"), []byte("a\nb\nc\n"), 0644))
	stdout, _, code := tailRun(t, "tail -n 3 f.txt", dir)
	assert.Equal(t, 0, code)
	assert.Equal(t, "a\nb\nc\n", stdout)
}

func TestCmdPentestTailRingBufferMoreThanFile(t *testing.T) {
	// Request more lines than exist: output all.
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "f.txt"), []byte("a\nb\nc\n"), 0644))
	stdout, _, code := tailRun(t, "tail -n 10 f.txt", dir)
	assert.Equal(t, 0, code)
	assert.Equal(t, "a\nb\nc\n", stdout)
}

// --- GTFOBins validation ---
//
// GTFOBins documents tail as capable of reading arbitrary files via
// "tail -c+0 /path/to/file" (+0 byte offset = read from start = all bytes).
// In rshell, file access is restricted by the AllowedPaths sandbox.
// The +0 offset mode itself is supported but sandboxed.
// See: https://gtfobins.org/gtfobins/tail/

// TestTailGTFOBinsFileReadOffsetSandboxEscape verifies that the GTFOBins
// file-read technique "tail -c+0" cannot read files outside the sandbox.
//
// GTFOBins: https://gtfobins.org/gtfobins/tail/
// Technique: tail -c+0 /path/to/input-file
func TestTailGTFOBinsFileReadOffsetSandboxEscape(t *testing.T) {
	allowed := t.TempDir()
	secret := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(secret, "secret.txt"), []byte("secret data"), 0644))
	secretPath := filepath.ToSlash(filepath.Join(secret, "secret.txt"))
	// Attempt the exact GTFOBins technique: tail -c+0 <file outside sandbox>
	_, stderr, code := runScript(t, "tail -c+0 "+secretPath, allowed, interp.AllowedPaths([]string{allowed}))
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "tail:")
}

// TestTailGTFOBinsFileReadOffsetWithinSandbox verifies that the GTFOBins
// technique works within the sandbox (reading allowed files is fine).
//
// GTFOBins: https://gtfobins.org/gtfobins/tail/
// Technique: tail -c+0 /path/to/input-file
func TestTailGTFOBinsFileReadOffsetWithinSandbox(t *testing.T) {
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "allowed.txt"), []byte("safe content"), 0644))
	// Within the sandbox, tail -c+0 should output the entire file.
	stdout, _, code := tailRun(t, "tail -c+0 allowed.txt", dir)
	assert.Equal(t, 0, code)
	assert.Equal(t, "safe content", stdout)
}

// TestTailGTFOBinsFileReadTraversal verifies that path traversal cannot
// escape the sandbox when using the tail file-read technique.
//
// GTFOBins: https://gtfobins.org/gtfobins/tail/
// Technique: tail -c+0 ../../etc/passwd
func TestTailGTFOBinsFileReadTraversal(t *testing.T) {
	dir := t.TempDir()
	_, stderr, code := tailRun(t, "tail -c+0 ../../etc/passwd", dir)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "tail:")
}
