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

import (
	"bytes"
	"context"
	"errors"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"mvdan.cc/sh/v3/syntax"

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

func cutPentestRun(t *testing.T, script, dir string) (string, string, int) {
	t.Helper()
	return cutPentestRunCtx(context.Background(), t, script, dir)
}

func cutPentestRunCtx(ctx context.Context, t *testing.T, script, dir string) (string, string, int) {
	t.Helper()
	parser := syntax.NewParser()
	prog, err := parser.Parse(strings.NewReader(script), "")
	require.NoError(t, err)

	var outBuf, errBuf bytes.Buffer
	opts := []interp.RunnerOption{
		interp.StdIO(nil, &outBuf, &errBuf),
		interp.AllowedPaths([]string{dir}),
		interpoption.AllowAllCommands().(interp.RunnerOption),
	}

	runner, err := interp.New(opts...)
	require.NoError(t, err)
	defer runner.Close()

	if dir != "" {
		runner.Dir = dir
	}

	err = runner.Run(ctx, prog)
	exitCode := 0
	if err != nil {
		var es interp.ExitStatus
		if errors.As(err, &es) {
			exitCode = int(es)
		} else if ctx.Err() == nil {
			t.Fatalf("unexpected error: %v", err)
		}
	}
	return outBuf.String(), errBuf.String(), exitCode
}

func cutPentestWriteFile(t *testing.T, dir, name, content string) {
	t.Helper()
	require.NoError(t, os.WriteFile(filepath.Join(dir, name), []byte(content), 0644))
}

// --- Integer edge cases ---

func TestCutPentestZeroByte(t *testing.T) {
	dir := t.TempDir()
	_, stderr, code := cutPentestRun(t, "cut -b0 file", dir)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "cut:")
}

func TestCutPentestZeroField(t *testing.T) {
	dir := t.TempDir()
	_, stderr, code := cutPentestRun(t, "cut -f0 file", dir)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "cut:")
}

func TestCutPentestNegativeField(t *testing.T) {
	dir := t.TempDir()
	// -f-1 is parsed as -f with value "-1"; the leading dash in the value is
	// tricky because pflag may interpret "-1" as a flag. Let's try with space.
	_, stderr, code := cutPentestRun(t, "cut -f -1 file", dir)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "cut:")
}

func TestCutPentestHugeNumber(t *testing.T) {
	dir := t.TempDir()
	cutPentestWriteFile(t, dir, "file.txt", "abc\n")
	// Very large number should not cause OOM — strconv.Atoi handles overflow.
	_, stderr, code := cutPentestRun(t, "cut -b99999999999999999999 file.txt", dir)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "cut:")
}

func TestCutPentestMaxInt32Range(t *testing.T) {
	dir := t.TempDir()
	cutPentestWriteFile(t, dir, "file.txt", "abc\n")
	// Large bounded range should work (clamped to line length).
	stdout, _, code := cutPentestRun(t, "cut -b1-2147483647 file.txt", dir)
	assert.Equal(t, 0, code)
	assert.Equal(t, "abc\n", stdout)
}

func TestCutPentestLargeUnboundedRange(t *testing.T) {
	dir := t.TempDir()
	cutPentestWriteFile(t, dir, "file.txt", "")
	stdout, _, code := cutPentestRun(t, "cut -b1234567890- file.txt", dir)
	assert.Equal(t, 0, code)
	assert.Equal(t, "", stdout)
}

func TestCutPentestEmptyList(t *testing.T) {
	dir := t.TempDir()
	_, stderr, code := cutPentestRun(t, "cut -b '' file", dir)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "cut:")
}

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

func TestCutPentestUnknownFlags(t *testing.T) {
	dir := t.TempDir()
	for _, flag := range []string{"-z", "--follow", "--no-such-flag"} {
		_, stderr, code := cutPentestRun(t, "cut "+flag+" file", dir)
		assert.Equal(t, 1, code, "flag: %s", flag)
		assert.Contains(t, stderr, "cut:", "flag: %s", flag)
	}
}

func TestCutPentestDoubleDashFlagLikeFile(t *testing.T) {
	dir := t.TempDir()
	cutPentestWriteFile(t, dir, "-v", "hello\n")
	stdout, _, code := cutPentestRun(t, "cut -f1 -- -v", dir)
	assert.Equal(t, 0, code)
	assert.Equal(t, "hello\n", stdout)
}

func TestCutPentestMultipleStdin(t *testing.T) {
	dir := t.TempDir()
	cutPentestWriteFile(t, dir, "file.txt", "a:b\n")
	stdout, _, code := cutPentestRun(t, "cat file.txt | cut -d: -f1 - -", dir)
	assert.Equal(t, 0, code)
	// First stdin reads data, second gets EOF.
	assert.Equal(t, "a\n", stdout)
}

func TestCutPentestFlagExpansionInLoop(t *testing.T) {
	dir := t.TempDir()
	cutPentestWriteFile(t, dir, "file.txt", "hello\n")
	_, stderr, code := cutPentestRun(t, "for flag in --follow; do cut $flag file.txt; done", dir)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "cut:")
}

// --- Path edge cases ---

func TestCutPentestNonexistentFile(t *testing.T) {
	dir := t.TempDir()
	_, stderr, code := cutPentestRun(t, "cut -b1 nonexistent.txt", dir)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "cut:")
}

func TestCutPentestEmptyFilename(t *testing.T) {
	dir := t.TempDir()
	_, stderr, code := cutPentestRun(t, "cut -b1 ''", dir)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "cut:")
}

// --- Context cancellation ---

func TestCutPentestContextCancelled(t *testing.T) {
	dir := t.TempDir()
	ctx, cancel := context.WithCancel(context.Background())
	cancel()
	_, _, _ = cutPentestRunCtx(ctx, t, "cut -b1 file", dir)
}

func TestCutPentestContextTimeout(t *testing.T) {
	dir := t.TempDir()
	cutPentestWriteFile(t, dir, "file.txt", strings.Repeat("abcdef\n", 10000))
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	stdout, _, code := cutPentestRunCtx(ctx, t, "cut -b1-3 file.txt", dir)
	assert.Equal(t, 0, code)
	assert.Contains(t, stdout, "abc")
}

// --- Large input ---

func TestCutPentestLargeFile(t *testing.T) {
	dir := t.TempDir()
	content := strings.Repeat("field1:field2:field3\n", 40000)
	cutPentestWriteFile(t, dir, "large.txt", content)
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()
	stdout, _, code := cutPentestRunCtx(ctx, t, "cut -d: -f2 large.txt", dir)
	assert.Equal(t, 0, code)
	assert.Contains(t, stdout, "field2")
}

// --- Many files (FD leak check) ---

func TestCutPentestManyFiles(t *testing.T) {
	dir := t.TempDir()
	var args []string
	for i := range 50 {
		name := strings.ReplaceAll(filepath.Base(t.Name()), "/", "_") + "_" + string(rune('a'+i%26)) + string(rune('0'+i/26)) + ".txt"
		require.NoError(t, os.WriteFile(filepath.Join(dir, name), []byte("x:y\n"), 0644))
		args = append(args, name)
	}
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()
	stdout, _, code := cutPentestRunCtx(ctx, t, "cut -d: -f1 "+strings.Join(args, " "), dir)
	assert.Equal(t, 0, code)
	assert.Contains(t, stdout, "x")
}

// --- Long lines ---

func TestCutPentestLongLine(t *testing.T) {
	dir := t.TempDir()
	longLine := strings.Repeat("x", 1024*1024-1) + "\n" // MaxLineBytes - 1
	cutPentestWriteFile(t, dir, "file.txt", longLine)
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	stdout, _, code := cutPentestRunCtx(ctx, t, "cut -b1 file.txt", dir)
	assert.Equal(t, 0, code)
	assert.Equal(t, "x\n", stdout)
}

func TestCutPentestLineBeyondMaxBytes(t *testing.T) {
	dir := t.TempDir()
	// Line exactly at MaxLineBytes + 1 will cause scanner error.
	longLine := strings.Repeat("x", 1024*1024+1)
	cutPentestWriteFile(t, dir, "file.txt", longLine)
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	_, stderr, code := cutPentestRunCtx(ctx, t, "cut -b1 file.txt", dir)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "cut:")
}

// --- Edge: empty file ---

func TestCutPentestEmptyFile(t *testing.T) {
	dir := t.TempDir()
	cutPentestWriteFile(t, dir, "file.txt", "")
	stdout, _, code := cutPentestRun(t, "cut -b1 file.txt", dir)
	assert.Equal(t, 0, code)
	assert.Equal(t, "", stdout)
}

// --- Edge: file with only newlines ---

func TestCutPentestOnlyNewlines(t *testing.T) {
	dir := t.TempDir()
	cutPentestWriteFile(t, dir, "file.txt", strings.Repeat("\n", 100))
	stdout, _, code := cutPentestRun(t, "cut -b1 file.txt", dir)
	assert.Equal(t, 0, code)
	// Each empty line produces just a newline.
	assert.Equal(t, strings.Repeat("\n", 100), stdout)
}

// --- Behavior: binary input ---

func TestCutPentestBinaryInput(t *testing.T) {
	dir := t.TempDir()
	// Binary data with embedded NULs.
	content := []byte{0x00, 0x01, 0x02, 0x03, 0x0A, 0xFF, 0xFE, 0x0A}
	require.NoError(t, os.WriteFile(filepath.Join(dir, "binary.bin"), content, 0644))
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	_, _, code := cutPentestRunCtx(ctx, t, "cut -b1 binary.bin", dir)
	// Should not crash on binary data.
	assert.Equal(t, 0, code)
}

// --- Behavior: complement with large range ---

func TestCutPentestComplementLargeRange(t *testing.T) {
	dir := t.TempDir()
	cutPentestWriteFile(t, dir, "file.txt", "abcdef\n")
	stdout, _, code := cutPentestRun(t, "cut --complement -b2147483647 file.txt", dir)
	assert.Equal(t, 0, code)
	assert.Equal(t, "abcdef\n", stdout)
}

// --- Decreasing range ---

func TestCutPentestDecreasingRange(t *testing.T) {
	dir := t.TempDir()
	_, stderr, code := cutPentestRun(t, "cut -f5-3 file", dir)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "decreasing")
}

// --- Bare dash in list ---

func TestCutPentestBareDash(t *testing.T) {
	dir := t.TempDir()
	_, stderr, code := cutPentestRun(t, "cut -f - file", dir)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "cut:")
}

// --- GTFOBins validation ---

// TestCutGTFOBinsFileReadSandboxEscape verifies that the GTFOBins file-read
// technique for cut is blocked by the AllowedPaths sandbox.
//
// GTFOBins: https://gtfobins.org/gtfobins/cut/
// Technique: cut -d "" -f1 /path/to/file
func TestCutGTFOBinsFileReadSandboxEscape(t *testing.T) {
	allowed := t.TempDir()
	secret := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(secret, "secret.txt"), []byte("secret data\n"), 0644))
	secretPath := strings.ReplaceAll(filepath.Join(secret, "secret.txt"), `\`, `/`)
	_, stderr, code := cutPentestRun(t, `cut -d '' -f1 `+secretPath, allowed)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "cut:")
}
