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

import (
	"bytes"
	"context"
	iofs "io/fs"
	"testing"
	"time"

	"github.com/DataDog/rshell/builtins"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// mockFileInfo is a minimal fs.FileInfo implementation for pentest helpers.
type mockFileInfo struct{}

func (m *mockFileInfo) Name() string        { return "mock" }
func (m *mockFileInfo) Size() int64         { return 0 }
func (m *mockFileInfo) Mode() iofs.FileMode { return 0o644 }
func (m *mockFileInfo) ModTime() time.Time  { return time.Time{} }
func (m *mockFileInfo) IsDir() bool         { return false }
func (m *mockFileInfo) Sys() any            { return nil }

// newPentestCallCtx returns a minimal CallContext suitable for pentest eval
// tests, wiring stdout and stderr to the provided buffers.
func newPentestCallCtx(stdout, stderr *bytes.Buffer) *builtins.CallContext {
	return &builtins.CallContext{
		Stdout: stdout,
		Stderr: stderr,
	}
}

// ---- Sandbox & AllowedCommands (CWE-284) ----

// TestFindExecDirCommandNotAllowed verifies that CommandAllowed is consulted
// at eval-time and that a blocked command causes a non-match with an error on
// stderr, without invoking RunCommand.
func TestFindExecDirCommandNotAllowed(t *testing.T) {
	var stdout, stderr bytes.Buffer
	runCalled := false

	callCtx := newPentestCallCtx(&stdout, &stderr)
	callCtx.RunCommand = func(_ context.Context, _ string, _ string, _ []string) (uint8, error) {
		runCalled = true
		return 0, nil
	}
	callCtx.CommandAllowed = func(name string) bool {
		return false // block everything
	}

	ec := &evalContext{
		callCtx: callCtx,
		ctx:     context.Background(),
		relPath: "somefile.txt",
		info:    &mockFileInfo{},
	}
	e := &expr{
		kind:     exprExecDir,
		execCmd:  "echo",
		execArgs: []string{"{}"},
	}

	result := evalExecDir(ec, e)

	assert.False(t, result.matched, "blocked command must not match")
	assert.True(t, ec.failed, "ec.failed must be set when command is blocked")
	assert.False(t, runCalled, "RunCommand must not be invoked for a blocked command")
	assert.Contains(t, stderr.String(), "not allowed", "stderr must mention 'not allowed'")
}

// TestFindExecDirNilRunCommand verifies that a nil RunCommand is handled
// gracefully: evalExecDir must set ec.failed, write to stderr, and return a
// non-matched result rather than panicking.
func TestFindExecDirNilRunCommand(t *testing.T) {
	var stdout, stderr bytes.Buffer

	callCtx := newPentestCallCtx(&stdout, &stderr)
	// RunCommand deliberately left nil.

	ec := &evalContext{
		callCtx: callCtx,
		ctx:     context.Background(),
		relPath: "somefile.txt",
		info:    &mockFileInfo{},
	}
	e := &expr{
		kind:     exprExecDir,
		execCmd:  "echo",
		execArgs: []string{"{}"},
	}

	require.NotPanics(t, func() {
		result := evalExecDir(ec, e)
		assert.False(t, result.matched)
	})
	assert.True(t, ec.failed, "ec.failed must be set when RunCommand is nil")
	assert.NotEmpty(t, stderr.String(), "an error message must appear on stderr")
}

// ---- Command Injection via Filenames (CWE-78) ----

// testExecDirFilename is a helper that verifies the argument passed to
// RunCommand for a given filename is exactly "./" + filename, ensuring that
// shell metacharacters in filenames are never interpreted.
func testExecDirFilename(t *testing.T, filename string) {
	t.Helper()

	var stdout, stderr bytes.Buffer
	var capturedArgs []string

	callCtx := newPentestCallCtx(&stdout, &stderr)
	callCtx.RunCommand = func(_ context.Context, _ string, _ string, args []string) (uint8, error) {
		capturedArgs = make([]string, len(args))
		copy(capturedArgs, args)
		return 0, nil
	}
	callCtx.CommandAllowed = func(_ string) bool { return true }

	ec := &evalContext{
		callCtx: callCtx,
		ctx:     context.Background(),
		relPath: filename,
		info:    &mockFileInfo{},
	}
	e := &expr{
		kind:     exprExecDir,
		execCmd:  "echo",
		execArgs: []string{"{}"},
	}

	result := evalExecDir(ec, e)

	assert.True(t, result.matched, "echo should succeed and match")
	require.Len(t, capturedArgs, 1, "exactly one argument must be passed to the command")
	assert.Equal(t, "./"+filename, capturedArgs[0],
		"filename %q must be passed as ./<filename>, not interpreted as shell syntax", filename)
}

// TestFindExecDirSemicolonInFilename ensures a filename containing a semicolon
// is passed literally and cannot terminate the command or inject a new command.
func TestFindExecDirSemicolonInFilename(t *testing.T) {
	testExecDirFilename(t, "hi; ls")
}

// TestFindExecDirPipeInFilename ensures a pipe character in a filename does
// not result in stdout being piped to another process.
func TestFindExecDirPipeInFilename(t *testing.T) {
	testExecDirFilename(t, "data|cat")
}

// TestFindExecDirBacktickInFilename ensures backtick command substitution
// characters in filenames are not executed by the shell.
func TestFindExecDirBacktickInFilename(t *testing.T) {
	testExecDirFilename(t, "`rm`")
}

// TestFindExecDirDollarParenInFilename ensures $(...) command substitution
// syntax in filenames is not interpreted.
func TestFindExecDirDollarParenInFilename(t *testing.T) {
	testExecDirFilename(t, "$(rm)")
}

// TestFindExecDirNewlineInFilename ensures a filename containing a newline
// character is passed as a single argument without being split.
func TestFindExecDirNewlineInFilename(t *testing.T) {
	testExecDirFilename(t, "file\nname")
}

// TestFindExecDirSpacesInFilename ensures a filename with multiple spaces is
// passed as a single argument and not word-split into multiple tokens.
func TestFindExecDirSpacesInFilename(t *testing.T) {
	testExecDirFilename(t, "a   b   c")
}

// TestFindExecDirGlobCharsInFilename ensures glob metacharacters in filenames
// are not expanded by the shell.
func TestFindExecDirGlobCharsInFilename(t *testing.T) {
	testExecDirFilename(t, "*.txt")
}

// TestFindExecDirQuotesInFilename ensures single and double quote characters
// in filenames are passed literally without breaking argument quoting.
func TestFindExecDirQuotesInFilename(t *testing.T) {
	testExecDirFilename(t, `it's a "test"`)
}

// TestFindExecDirRootPath verifies that the root path "/" is passed as "/"
// (not ".//") when used as the {} replacement.
func TestFindExecDirRootPath(t *testing.T) {
	var stdout, stderr bytes.Buffer
	var capturedArgs []string

	callCtx := newPentestCallCtx(&stdout, &stderr)
	callCtx.RunCommand = func(_ context.Context, _ string, _ string, args []string) (uint8, error) {
		capturedArgs = make([]string, len(args))
		copy(capturedArgs, args)
		return 0, nil
	}
	callCtx.CommandAllowed = func(_ string) bool { return true }

	ec := &evalContext{
		callCtx: callCtx,
		ctx:     context.Background(),
		relPath: "/",
		info:    &mockFileInfo{},
	}
	e := &expr{
		kind:     exprExecDir,
		execCmd:  "echo",
		execArgs: []string{"{}"},
	}

	result := evalExecDir(ec, e)

	assert.True(t, result.matched)
	require.Len(t, capturedArgs, 1)
	assert.Equal(t, "/", capturedArgs[0], "root path must be passed as '/', not './/'")
}

// ---- Argument Injection (CWE-88) — ./ prefix prevents flag interpretation ----

// TestFindExecDirDashFilename verifies that a filename beginning with a dash
// (e.g. "-rf") is passed as "./-rf", preventing it from being interpreted as
// a command-line flag by the invoked program.
func TestFindExecDirDashFilename(t *testing.T) {
	testExecDirFilename(t, "-rf")
}

// TestFindExecDirDoubleDashFilename verifies that a filename beginning with
// double dashes (e.g. "--help") is passed as "./--help" to prevent flag
// injection.
func TestFindExecDirDoubleDashFilename(t *testing.T) {
	testExecDirFilename(t, "--help")
}

// ---- PATH Injection (CWE-426) ----

// TestFindExecDirNoPathLookup verifies that specifying a path-relative command
// such as "./malicious" via -execdir fails because path-traversal executables
// are not registered builtins and RunCommand is not provided.
func TestFindExecDirNoPathLookup(t *testing.T) {
	var stdout, stderr bytes.Buffer

	callCtx := newPentestCallCtx(&stdout, &stderr)
	// RunCommand is nil — simulates an environment where no external execution
	// is available, exercising that ./malicious cannot be launched.

	ec := &evalContext{
		callCtx: callCtx,
		ctx:     context.Background(),
		relPath: "target.txt",
		info:    &mockFileInfo{},
	}
	e := &expr{
		kind:     exprExecDir,
		execCmd:  "./malicious",
		execArgs: []string{"{}"},
	}

	result := evalExecDir(ec, e)

	assert.False(t, result.matched, "./malicious must not be executed via path lookup")
	assert.True(t, ec.failed, "ec.failed must be set")
	assert.NotEmpty(t, stderr.String(), "an error must be reported on stderr")
}

// ---- execDirParent plumbing (regression for filepath.Join cleaning bug) ----

// TestFindExecDirParentDir verifies that evalExecDir passes ec.execDirParent
// as the dir argument to RunCommand. This is the plumbing test — it ensures
// the value computed by processEntry is forwarded faithfully.
func TestFindExecDirParentDir(t *testing.T) {
	tests := []struct {
		name          string
		relPath       string
		execDirParent string
	}{
		{"file in cwd", "file.txt", "/cwd"},
		{"file in subdir", "sub/file.txt", "/cwd/sub"},
		{"dot start path", ".", "/cwd"},
		{"named dir start path", "dir", "/cwd"},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var stdout, stderr bytes.Buffer
			var capturedDir string

			callCtx := newPentestCallCtx(&stdout, &stderr)
			callCtx.RunCommand = func(_ context.Context, dir string, _ string, _ []string) (uint8, error) {
				capturedDir = dir
				return 0, nil
			}
			callCtx.CommandAllowed = func(_ string) bool { return true }

			ec := &evalContext{
				callCtx:       callCtx,
				ctx:           context.Background(),
				relPath:       tt.relPath,
				info:          &mockFileInfo{},
				execDirParent: tt.execDirParent,
			}
			e := &expr{
				kind:     exprExecDir,
				execCmd:  "echo",
				execArgs: []string{"{}"},
			}

			result := evalExecDir(ec, e)

			assert.True(t, result.matched)
			assert.Equal(t, tt.execDirParent, capturedDir,
				"RunCommand dir must equal ec.execDirParent")
		})
	}
}

// TestFindExecDirEmbeddedBracesReplacement verifies that {} embedded in an
// argument (e.g. {}.bak) is replaced with the ./basename substitution.
func TestFindExecDirEmbeddedBracesReplacement(t *testing.T) {
	var stdout, stderr bytes.Buffer
	var capturedArgs []string

	callCtx := newPentestCallCtx(&stdout, &stderr)
	callCtx.RunCommand = func(_ context.Context, _ string, _ string, args []string) (uint8, error) {
		capturedArgs = make([]string, len(args))
		copy(capturedArgs, args)
		return 0, nil
	}
	callCtx.CommandAllowed = func(_ string) bool { return true }

	ec := &evalContext{
		callCtx: callCtx,
		ctx:     context.Background(),
		relPath: "photo.jpg",
		info:    &mockFileInfo{},
	}
	e := &expr{
		kind:     exprExecDir,
		execCmd:  "echo",
		execArgs: []string{"{}.bak", "prefix-{}-suffix"},
	}

	result := evalExecDir(ec, e)

	assert.True(t, result.matched)
	require.Len(t, capturedArgs, 2)
	assert.Equal(t, "./photo.jpg.bak", capturedArgs[0])
	assert.Equal(t, "prefix-./photo.jpg-suffix", capturedArgs[1])
}

// TestFindExecDirTrailingSlashPreserved verifies that a trailing slash on the
// relPath is preserved in the {} replacement (e.g. "dir/" → "./dir/").
func TestFindExecDirTrailingSlashPreserved(t *testing.T) {
	var stdout, stderr bytes.Buffer
	var capturedArgs []string

	callCtx := newPentestCallCtx(&stdout, &stderr)
	callCtx.RunCommand = func(_ context.Context, _ string, _ string, args []string) (uint8, error) {
		capturedArgs = make([]string, len(args))
		copy(capturedArgs, args)
		return 0, nil
	}
	callCtx.CommandAllowed = func(_ string) bool { return true }

	ec := &evalContext{
		callCtx: callCtx,
		ctx:     context.Background(),
		relPath: "dir/",
		info:    &mockFileInfo{},
	}
	e := &expr{
		kind:     exprExecDir,
		execCmd:  "echo",
		execArgs: []string{"{}"},
	}

	result := evalExecDir(ec, e)

	assert.True(t, result.matched)
	require.Len(t, capturedArgs, 1)
	assert.Equal(t, "./dir/", capturedArgs[0],
		"trailing slash must be preserved: dir/ → ./dir/")
}

// TestFindExecDirWindowsDriveRoot verifies that a Windows drive root path
// like "C:/" is forwarded as-is to RunCommand, not truncated to "C:" which
// would produce a drive-relative CWD.
func TestFindExecDirWindowsDriveRoot(t *testing.T) {
	var stdout, stderr bytes.Buffer
	var capturedDir string

	callCtx := newPentestCallCtx(&stdout, &stderr)
	callCtx.RunCommand = func(_ context.Context, dir string, _ string, _ []string) (uint8, error) {
		capturedDir = dir
		return 0, nil
	}
	callCtx.CommandAllowed = func(_ string) bool { return true }

	ec := &evalContext{
		callCtx:       callCtx,
		ctx:           context.Background(),
		relPath:       "C:/",
		info:          &mockFileInfo{},
		execDirParent: "C:/",
	}
	e := &expr{
		kind:     exprExecDir,
		execCmd:  "echo",
		execArgs: []string{"{}"},
	}

	result := evalExecDir(ec, e)

	assert.True(t, result.matched)
	assert.Equal(t, "C:/", capturedDir,
		"drive root must be preserved as C:/, not truncated to C:")
}

// ---- Command token substitution ----

// TestFindExecDirCommandTokenSubstitution verifies that {} in the command name
// (argv[0]) is substituted with ./basename, matching GNU find behavior.
func TestFindExecDirCommandTokenSubstitution(t *testing.T) {
	var stdout, stderr bytes.Buffer
	var capturedCmd string

	callCtx := newPentestCallCtx(&stdout, &stderr)
	callCtx.RunCommand = func(_ context.Context, _ string, cmd string, _ []string) (uint8, error) {
		capturedCmd = cmd
		return 0, nil
	}
	callCtx.CommandAllowed = func(_ string) bool { return true }

	ec := &evalContext{
		callCtx: callCtx,
		ctx:     context.Background(),
		relPath: "echo",
		info:    &mockFileInfo{},
	}
	e := &expr{
		kind:     exprExecDir,
		execCmd:  "{}",
		execArgs: []string{"hello"},
	}

	result := evalExecDir(ec, e)

	assert.True(t, result.matched)
	assert.Equal(t, "./echo", capturedCmd,
		"command token {} must be substituted with ./basename")
}

// TestFindExecCommandTokenSubstitution verifies that {} in the command name
// is substituted with the full printPath, matching GNU find behavior.
func TestFindExecCommandTokenSubstitution(t *testing.T) {
	var stdout, stderr bytes.Buffer
	var capturedCmd string

	callCtx := newPentestCallCtx(&stdout, &stderr)
	callCtx.RunCommand = func(_ context.Context, _ string, cmd string, _ []string) (uint8, error) {
		capturedCmd = cmd
		return 0, nil
	}
	callCtx.CommandAllowed = func(_ string) bool { return true }

	ec := &evalContext{
		callCtx:   callCtx,
		ctx:       context.Background(),
		relPath:   "dir/echo",
		printPath: "dir/echo",
		info:      &mockFileInfo{},
	}
	e := &expr{
		kind:     exprExec,
		execCmd:  "{}",
		execArgs: []string{"hello"},
	}

	result := evalExec(ec, e)

	assert.True(t, result.matched)
	assert.Equal(t, "dir/echo", capturedCmd,
		"command token {} must be substituted with full printPath")
}

// ---- GTFOBins ----

// TestFindExecParses verifies that -exec echo {} ; parses successfully now
// that -exec is implemented and no longer in the blocked predicates list.
func TestFindExecParses(t *testing.T) {
	_, err := parseExpression([]string{"-exec", "echo", "{}", ";"})
	require.NoError(t, err, "-exec must parse successfully")
}

// ---- -exec tests ----

// ---- Sandbox & AllowedCommands (CWE-284) ----

// TestFindExecCommandNotAllowed verifies that CommandAllowed is consulted
// at eval-time and that a blocked command causes a non-match with an error on
// stderr, without invoking RunCommand.
func TestFindExecCommandNotAllowed(t *testing.T) {
	var stdout, stderr bytes.Buffer
	runCalled := false

	callCtx := newPentestCallCtx(&stdout, &stderr)
	callCtx.RunCommand = func(_ context.Context, _ string, _ string, _ []string) (uint8, error) {
		runCalled = true
		return 0, nil
	}
	callCtx.CommandAllowed = func(name string) bool {
		return false // block everything
	}

	ec := &evalContext{
		callCtx:   callCtx,
		ctx:       context.Background(),
		relPath:   "somefile.txt",
		printPath: "dir/somefile.txt",
		info:      &mockFileInfo{},
	}
	e := &expr{
		kind:     exprExec,
		execCmd:  "echo",
		execArgs: []string{"{}"},
	}

	result := evalExec(ec, e)

	assert.False(t, result.matched, "blocked command must not match")
	assert.True(t, ec.failed, "ec.failed must be set when command is blocked")
	assert.False(t, runCalled, "RunCommand must not be invoked for a blocked command")
	assert.Contains(t, stderr.String(), "not allowed", "stderr must mention 'not allowed'")
}

// TestFindExecNilRunCommand verifies that a nil RunCommand is handled
// gracefully: evalExec must set ec.failed, write to stderr, and return a
// non-matched result rather than panicking.
func TestFindExecNilRunCommand(t *testing.T) {
	var stdout, stderr bytes.Buffer

	callCtx := newPentestCallCtx(&stdout, &stderr)
	// RunCommand deliberately left nil.

	ec := &evalContext{
		callCtx:   callCtx,
		ctx:       context.Background(),
		relPath:   "somefile.txt",
		printPath: "dir/somefile.txt",
		info:      &mockFileInfo{},
	}
	e := &expr{
		kind:     exprExec,
		execCmd:  "echo",
		execArgs: []string{"{}"},
	}

	require.NotPanics(t, func() {
		result := evalExec(ec, e)
		assert.False(t, result.matched)
	})
	assert.True(t, ec.failed, "ec.failed must be set when RunCommand is nil")
	assert.NotEmpty(t, stderr.String(), "an error message must appear on stderr")
}

// ---- Command Injection via Filenames (CWE-78) ----

// testExecFilename is a helper that verifies the argument passed to
// RunCommand for a given (relPath, printPath) pair is exactly printPath,
// ensuring that shell metacharacters in filenames are never interpreted.
// Unlike -execdir, -exec uses the full printPath (no "./" prefix).
func testExecFilename(t *testing.T, relPath, printPath string) {
	t.Helper()

	var stdout, stderr bytes.Buffer
	var capturedArgs []string

	callCtx := newPentestCallCtx(&stdout, &stderr)
	callCtx.RunCommand = func(_ context.Context, _ string, _ string, args []string) (uint8, error) {
		capturedArgs = make([]string, len(args))
		copy(capturedArgs, args)
		return 0, nil
	}
	callCtx.CommandAllowed = func(_ string) bool { return true }

	ec := &evalContext{
		callCtx:   callCtx,
		ctx:       context.Background(),
		relPath:   relPath,
		printPath: printPath,
		info:      &mockFileInfo{},
	}
	e := &expr{
		kind:     exprExec,
		execCmd:  "echo",
		execArgs: []string{"{}"},
	}

	result := evalExec(ec, e)

	assert.True(t, result.matched, "echo should succeed and match")
	require.Len(t, capturedArgs, 1, "exactly one argument must be passed to the command")
	assert.Equal(t, printPath, capturedArgs[0],
		"printPath %q must be passed verbatim, not interpreted as shell syntax", printPath)
}

// TestFindExecSemicolonInFilename ensures a filename containing a semicolon
// is passed literally and cannot terminate the command or inject a new command.
func TestFindExecSemicolonInFilename(t *testing.T) {
	testExecFilename(t, "dir/hi; ls", "dir/hi; ls")
}

// TestFindExecPipeInFilename ensures a pipe character in a filename does
// not result in stdout being piped to another process.
func TestFindExecPipeInFilename(t *testing.T) {
	testExecFilename(t, "dir/data|cat", "dir/data|cat")
}

// TestFindExecBacktickInFilename ensures backtick command substitution
// characters in filenames are not executed by the shell.
func TestFindExecBacktickInFilename(t *testing.T) {
	testExecFilename(t, "dir/`rm`", "dir/`rm`")
}

// TestFindExecDollarParenInFilename ensures $(...) command substitution
// syntax in filenames is not interpreted.
func TestFindExecDollarParenInFilename(t *testing.T) {
	testExecFilename(t, "dir/$(rm)", "dir/$(rm)")
}

// TestFindExecNewlineInFilename ensures a filename containing a newline
// character is passed as a single argument without being split.
func TestFindExecNewlineInFilename(t *testing.T) {
	testExecFilename(t, "dir/file\nname", "dir/file\nname")
}

// TestFindExecSpacesInFilename ensures a filename with multiple spaces is
// passed as a single argument and not word-split into multiple tokens.
func TestFindExecSpacesInFilename(t *testing.T) {
	testExecFilename(t, "dir/a   b   c", "dir/a   b   c")
}

// TestFindExecGlobCharsInFilename ensures glob metacharacters in filenames
// are not expanded by the shell.
func TestFindExecGlobCharsInFilename(t *testing.T) {
	testExecFilename(t, "dir/*.txt", "dir/*.txt")
}

// TestFindExecQuotesInFilename ensures single and double quote characters
// in filenames are passed literally without breaking argument quoting.
func TestFindExecQuotesInFilename(t *testing.T) {
	testExecFilename(t, `dir/it's a "test"`, `dir/it's a "test"`)
}

// ---- Argument Injection (CWE-88) ----

// TestFindExecDashInPath verifies that a path beginning with a dash
// (e.g. "dir/-rf") is passed as-is without a "./" prefix, unlike -execdir.
// Note: -exec does NOT protect against dash-injection; this is expected behavior.
func TestFindExecDashInPath(t *testing.T) {
	testExecFilename(t, "dir/-rf", "dir/-rf")
}

// ---- Correctness ----

// TestFindExecFullPathReplacement verifies that {} is replaced with the full
// printPath (not the relPath or a ./-prefixed basename).
func TestFindExecFullPathReplacement(t *testing.T) {
	tests := []struct {
		name      string
		relPath   string
		printPath string
	}{
		{"file in cwd", "file.txt", "./file.txt"},
		{"file in subdir", "sub/deep.txt", "dir/sub/deep.txt"},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			testExecFilename(t, tt.relPath, tt.printPath)
		})
	}
}

// TestFindExecWorkDir verifies that evalExec passes ec.execWorkDir as the
// dir argument to RunCommand, not ec.execDirParent.
func TestFindExecWorkDir(t *testing.T) {
	var stdout, stderr bytes.Buffer
	var capturedDir string

	callCtx := newPentestCallCtx(&stdout, &stderr)
	callCtx.RunCommand = func(_ context.Context, dir string, _ string, _ []string) (uint8, error) {
		capturedDir = dir
		return 0, nil
	}
	callCtx.CommandAllowed = func(_ string) bool { return true }

	ec := &evalContext{
		callCtx:       callCtx,
		ctx:           context.Background(),
		relPath:       "sub/file.txt",
		printPath:     "dir/sub/file.txt",
		info:          &mockFileInfo{},
		execWorkDir:   "/absolute/cwd",
		execDirParent: "/absolute/cwd/sub",
	}
	e := &expr{
		kind:     exprExec,
		execCmd:  "echo",
		execArgs: []string{"{}"},
	}

	result := evalExec(ec, e)

	assert.True(t, result.matched)
	assert.Equal(t, "/absolute/cwd", capturedDir,
		"RunCommand dir must equal ec.execWorkDir, not ec.execDirParent")
}

// TestFindExecEmbeddedBracesReplacement verifies that {} embedded in an
// argument (e.g. {}.bak) is replaced with the full printPath.
func TestFindExecEmbeddedBracesReplacement(t *testing.T) {
	var stdout, stderr bytes.Buffer
	var capturedArgs []string

	callCtx := newPentestCallCtx(&stdout, &stderr)
	callCtx.RunCommand = func(_ context.Context, _ string, _ string, args []string) (uint8, error) {
		capturedArgs = make([]string, len(args))
		copy(capturedArgs, args)
		return 0, nil
	}
	callCtx.CommandAllowed = func(_ string) bool { return true }

	ec := &evalContext{
		callCtx:   callCtx,
		ctx:       context.Background(),
		relPath:   "file.txt",
		printPath: "dir/file.txt",
		info:      &mockFileInfo{},
	}
	e := &expr{
		kind:     exprExec,
		execCmd:  "echo",
		execArgs: []string{"{}.bak"},
	}

	result := evalExec(ec, e)

	assert.True(t, result.matched)
	require.Len(t, capturedArgs, 1)
	assert.Equal(t, "dir/file.txt.bak", capturedArgs[0])
}

// TestFindExecNoPathLookup verifies that specifying a path-relative command
// such as "./malicious" via -exec fails because RunCommand is not provided.
func TestFindExecNoPathLookup(t *testing.T) {
	var stdout, stderr bytes.Buffer

	callCtx := newPentestCallCtx(&stdout, &stderr)
	// RunCommand is nil — simulates an environment where no external execution
	// is available, exercising that ./malicious cannot be launched.

	ec := &evalContext{
		callCtx:   callCtx,
		ctx:       context.Background(),
		relPath:   "target.txt",
		printPath: "dir/target.txt",
		info:      &mockFileInfo{},
	}
	e := &expr{
		kind:     exprExec,
		execCmd:  "./malicious",
		execArgs: []string{"{}"},
	}

	result := evalExec(ec, e)

	assert.False(t, result.matched, "./malicious must not be executed via path lookup")
	assert.True(t, ec.failed, "ec.failed must be set")
	assert.NotEmpty(t, stderr.String(), "an error must be reported on stderr")
}
