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

import (
	"context"
	"fmt"
	"math"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

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

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

const pentestTimeout = 10 * time.Second

func catRun(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 catRunCtx(ctx context.Context, t *testing.T, script, dir string) (string, string, int) {
	t.Helper()
	return testutil.RunScriptCtx(ctx, t, script, dir, interp.AllowedPaths([]string{dir}))
}

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")
	}
}

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

func TestCatPentestUnknownLongFlag(t *testing.T) {
	dir := t.TempDir()
	_, stderr, code := catRun(t, "cat --no-such-flag file.txt", dir)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "cat:")
}

func TestCatPentestUnknownShortFlag(t *testing.T) {
	dir := t.TempDir()
	_, stderr, code := catRun(t, "cat -f file.txt", dir)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "cat:")
}

func TestCatPentestFlagViaExpansion(t *testing.T) {
	dir := t.TempDir()
	writeFile(t, dir, "file.txt", "ok\n")
	_, stderr, code := catRun(t, `for flag in -f; do cat $flag file.txt; done`, dir)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "cat:")
}

func TestCatPentestDoubleDashThenFlagLikeFile(t *testing.T) {
	dir := t.TempDir()
	writeFile(t, dir, "-n", "content\n")
	stdout, _, code := catRun(t, "cat -- -n", dir)
	assert.Equal(t, 0, code)
	assert.Equal(t, "content\n", stdout)
}

func TestCatPentestMultipleStdinDash(t *testing.T) {
	dir := t.TempDir()
	writeFile(t, dir, "src.txt", "hello\n")
	stdout, _, code := catRun(t, "cat - - < src.txt", dir)
	assert.Equal(t, 0, code)
	assert.Equal(t, "hello\n", stdout)
}

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

func TestCatPentestNonexistentFile(t *testing.T) {
	dir := t.TempDir()
	_, stderr, code := catRun(t, "cat does_not_exist", dir)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "cat:")
}

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

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

func TestCatPentestPathTraversal(t *testing.T) {
	dir := t.TempDir()
	outer := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(outer, "secret.txt"), []byte("secret"), 0644))
	outerPath := strings.ReplaceAll(filepath.Join(outer, "secret.txt"), `\`, `/`)
	_, stderr, code := catRun(t, "cat "+outerPath, dir)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "cat:")
}

// --- Long lines ---

func TestCatPentestLineAtCapMinus1(t *testing.T) {
	dir := t.TempDir()
	content := make([]byte, cat.MaxLineBytes-1)
	for i := range content {
		content[i] = 'a'
	}
	content = append(content, '\n')
	require.NoError(t, os.WriteFile(filepath.Join(dir, "file.txt"), content, 0644))
	stdout, _, code := catRun(t, "cat -n file.txt", dir)
	assert.Equal(t, 0, code)
	assert.Contains(t, stdout, "     1\t")
}

func TestCatPentestLineAtCapExact(t *testing.T) {
	dir := t.TempDir()
	content := make([]byte, cat.MaxLineBytes)
	for i := range content {
		content[i] = 'a'
	}
	require.NoError(t, os.WriteFile(filepath.Join(dir, "file.txt"), content, 0644))
	stdout, _, code := catRun(t, "cat -n file.txt", dir)
	assert.Equal(t, 0, code)
	assert.Contains(t, stdout, "     1\t")
}

func TestCatPentestLineAtCapPlus1(t *testing.T) {
	dir := t.TempDir()
	content := make([]byte, cat.MaxLineBytes+1)
	for i := range content {
		content[i] = 'a'
	}
	require.NoError(t, os.WriteFile(filepath.Join(dir, "file.txt"), content, 0644))
	_, stderr, code := catRun(t, "cat -n file.txt", dir)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "cat:")
}

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

func TestCatPentestSmallFileRawMode(t *testing.T) {
	dir := t.TempDir()
	writeFile(t, dir, "tiny.txt", "small\n")
	mustNotHang(t, func() {
		stdout, _, code := catRun(t, "cat tiny.txt", dir)
		assert.Equal(t, 0, code)
		assert.Equal(t, "small\n", stdout)
	})
}

func TestCatPentestManyFiles(t *testing.T) {
	dir := t.TempDir()
	var args []string
	for i := 0; i < 200; i++ {
		name := fmt.Sprintf("f%03d.txt", i)
		writeFile(t, dir, name, "line\n")
		args = append(args, name)
	}
	mustNotHang(t, func() {
		_, _, code := catRun(t, "cat "+strings.Join(args, " "), dir)
		assert.Equal(t, 0, code)
	})
}

func TestCatPentestLargeFileNumbered(t *testing.T) {
	dir := t.TempDir()
	var sb strings.Builder
	for i := 0; i < 10000; i++ {
		sb.WriteString("line\n")
	}
	writeFile(t, dir, "big.txt", sb.String())
	mustNotHang(t, func() {
		stdout, _, code := catRun(t, "cat -n big.txt", dir)
		assert.Equal(t, 0, code)
		assert.Contains(t, stdout, " 10000\t")
	})
}

// --- Special files / infinite sources ---

func TestCatPentestDevNull(t *testing.T) {
	if os.DevNull == "NUL" {
		// Windows reserved device names (NUL, CON, etc.) are blocked by the
		// sandbox to prevent hangs — same as head's TestHeadWindowsReservedName.
		dir := t.TempDir()
		mustNotHang(t, func() {
			_, stderr, code := catRun(t, "cat NUL", dir)
			assert.Equal(t, 1, code)
			assert.Contains(t, stderr, "cat:")
		})
		return
	}
	dir := t.TempDir()
	mustNotHang(t, func() {
		stdout, _, code := catRun(t, "cat "+os.DevNull, dir, filepath.Dir(os.DevNull))
		assert.Equal(t, 0, code)
		assert.Equal(t, "", stdout)
	})
}

func TestCatPentestContextCancelledDuringRaw(t *testing.T) {
	dir := t.TempDir()
	writeFile(t, dir, "file.txt", strings.Repeat("x\n", 10000))
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()
	mustNotHang(t, func() {
		catRunCtx(ctx, t, "cat file.txt", dir)
	})
}

func TestCatPentestContextCancelledDuringLines(t *testing.T) {
	dir := t.TempDir()
	writeFile(t, dir, "file.txt", strings.Repeat("x\n", 10000))
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()
	mustNotHang(t, func() {
		catRunCtx(ctx, t, "cat -n file.txt", dir)
	})
}

// --- Integer edge cases in line numbering ---

func TestCatPentestLineNumberMaxInt(t *testing.T) {
	dir := t.TempDir()
	writeFile(t, dir, "file.txt", "hello\n")
	mustNotHang(t, func() {
		stdout, _, code := catRun(t, "cat -n file.txt", dir)
		assert.Equal(t, 0, code)
		assert.Equal(t, "     1\thello\n", stdout)
	})
}

func TestCatPentestLineNumberWidthOverflow(t *testing.T) {
	_ = math.MaxInt64
	dir := t.TempDir()
	writeFile(t, dir, "file.txt", "hello\n")
	stdout, _, code := catRun(t, "cat -n file.txt", dir)
	assert.Equal(t, 0, code)
	assert.Contains(t, stdout, "     1\t")
}

// --- Behavior matching ---

func TestCatPentestSqueezeOnlyAffectsBlankLines(t *testing.T) {
	dir := t.TempDir()
	writeFile(t, dir, "file.txt", "a\n \n \nb\n")
	stdout, _, code := catRun(t, "cat -s file.txt", dir)
	assert.Equal(t, 0, code)
	assert.Equal(t, "a\n \n \nb\n", stdout)
}

func TestCatPentestCRLFNotBlank(t *testing.T) {
	dir := t.TempDir()
	writeFile(t, dir, "file.txt", "a\n\r\n\r\nb\n")
	stdout, _, code := catRun(t, "cat -s file.txt", dir)
	assert.Equal(t, 0, code)
	assert.Equal(t, "a\n\r\n\r\nb\n", stdout)
}

func TestCatPentestShowEndsCRLFNoV(t *testing.T) {
	dir := t.TempDir()
	writeFile(t, dir, "file.txt", "a\rb\r\nc\n\r\nd\r")
	stdout, _, code := catRun(t, "cat -E file.txt", dir)
	assert.Equal(t, 0, code)
	assert.Equal(t, "a\rb^M$\nc$\n^M$\nd\r", stdout)
}

func TestCatPentestShowEndsCRLFWithV(t *testing.T) {
	dir := t.TempDir()
	writeFile(t, dir, "file.txt", "a\r\nb\n")
	stdout, _, code := catRun(t, "cat -vE file.txt", dir)
	assert.Equal(t, 0, code)
	assert.Equal(t, "a^M$\nb$\n", stdout)
}

// --- GTFOBins validation ---
//
// GTFOBins documents cat as capable of reading arbitrary files.
// In rshell, file access is restricted by the AllowedPaths sandbox.
// See: https://gtfobins.org/gtfobins/cat/

// TestCatGTFOBinsFileReadSandboxEscape verifies that the GTFOBins file-read
// technique for cat is blocked by the AllowedPaths sandbox.
//
// GTFOBins: https://gtfobins.org/gtfobins/cat/
// Technique: cat /path/to/input-file
func TestCatGTFOBinsFileReadSandboxEscape(t *testing.T) {
	allowed := t.TempDir()
	secret := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(secret, "secret.txt"), []byte("secret data"), 0644))
	secretPath := strings.ReplaceAll(filepath.Join(secret, "secret.txt"), `\`, `/`)
	// Attempt the exact GTFOBins technique: cat <file outside sandbox>
	_, stderr, code := catRun(t, "cat "+secretPath, allowed)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "cat:")
}

// TestCatGTFOBinsFileReadTraversal verifies that path traversal cannot
// escape the sandbox, even when combined with the cat file-read technique.
//
// GTFOBins: https://gtfobins.org/gtfobins/cat/
// Technique: cat ../../etc/passwd
func TestCatGTFOBinsFileReadTraversal(t *testing.T) {
	dir := t.TempDir()
	_, stderr, code := catRun(t, "cat ../../etc/passwd", dir)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "cat:")
}
