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

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

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

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

const pentestTimeout = 5 * time.Second

// pwdRunCtx is the timeout-aware variant of pwdRun.
func pwdRunCtx(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}))
}

// withTimeout wraps fn in a per-test deadline so a hang is reported as a
// failure instead of the full Go test timeout.
func withTimeout(t *testing.T, fn func(ctx context.Context)) {
	t.Helper()
	ctx, cancel := context.WithTimeout(context.Background(), pentestTimeout)
	defer cancel()
	done := make(chan struct{})
	go func() {
		fn(ctx)
		close(done)
	}()
	select {
	case <-done:
	case <-ctx.Done():
		t.Fatal("operation did not complete within pentestTimeout")
	}
}

// --- Many positional arguments are silently ignored, no FD leak ---

func TestPwdPentestManyPositionalArgs(t *testing.T) {
	dir := canonicalTempDir(t)
	args := strings.Repeat("x ", 1000)
	withTimeout(t, func(ctx context.Context) {
		stdout, stderr, code := pwdRunCtx(ctx, t, "pwd "+args, dir)
		assert.Equal(t, 0, code)
		assert.Equal(t, "", stderr)
		assert.True(t, filepath.IsAbs(strings.TrimRight(stdout, "\n")))
	})
}

// --- Many calls in a tight loop don't leak resources or slow over time ---

func TestPwdPentestTightLoop(t *testing.T) {
	dir := canonicalTempDir(t)
	// 200 invocations via brace expansion (mvdan/sh supports {1..N}).
	// The runner has a 30-second executor budget; this should finish
	// well under a second.
	withTimeout(t, func(ctx context.Context) {
		stdout, _, code := pwdRunCtx(ctx, t, "for i in {1..200}; do pwd; done", dir)
		assert.Equal(t, 0, code)
		assert.Equal(t, 200, strings.Count(stdout, "\n"))
	})
}

// --- Flag injection via word splitting/expansion is handled safely ---

func TestPwdPentestFlagInjectionViaExpansion(t *testing.T) {
	dir := canonicalTempDir(t)
	// $bad expands to "-x". rshell must reject the expanded flag the same
	// way as a literal -x — i.e. exit 1, not crash, not exec anything.
	withTimeout(t, func(ctx context.Context) {
		_, stderr, code := pwdRunCtx(ctx, t, "bad=-x; pwd $bad", dir)
		assert.Equal(t, 1, code)
		assert.Contains(t, stderr, "pwd:")
	})
}

func TestPwdPentestFlagInjectionViaUnquotedArrayLikeArgs(t *testing.T) {
	dir := canonicalTempDir(t)
	withTimeout(t, func(ctx context.Context) {
		_, stderr, code := pwdRunCtx(ctx, t, `for f in "--no-such"; do pwd $f; done`, dir)
		assert.Equal(t, 1, code)
		assert.Contains(t, stderr, "pwd:")
	})
}

// --- pwd output never contains stdin content (stdin must be ignored) ---

func TestPwdPentestStdinNotEchoed(t *testing.T) {
	dir := canonicalTempDir(t)
	withTimeout(t, func(ctx context.Context) {
		stdout, _, code := pwdRunCtx(ctx, t, `printf "abc\ndef\n" | pwd`, dir)
		assert.Equal(t, 0, code)
		assert.False(t, strings.Contains(stdout, "abc"))
		assert.False(t, strings.Contains(stdout, "def"))
	})
}

// --- pwd does not write to filesystem (no creation, no modification) ---

func TestPwdPentestNoFilesystemWrite(t *testing.T) {
	dir := canonicalTempDir(t)
	// Snapshot the directory contents.
	beforeEntries, err := os.ReadDir(dir)
	require.NoError(t, err)

	withTimeout(t, func(ctx context.Context) {
		_, _, code := pwdRunCtx(ctx, t, "pwd; pwd -L; pwd -P; pwd --help", dir)
		assert.Equal(t, 0, code)
	})

	afterEntries, err := os.ReadDir(dir)
	require.NoError(t, err)
	assert.Equal(t, len(beforeEntries), len(afterEntries),
		"pwd must not create or remove files")
}

// --- Symlink cycle is reported on -P, not infinite loop ---

func TestPwdPentestSymlinkCycleTerminates(t *testing.T) {
	if filepath.Separator == '\\' {
		t.Skip("symlink cycles are exercised on Unix; Windows has separate test")
	}
	root := canonicalTempDir(t)
	a := filepath.Join(root, "a")
	b := filepath.Join(root, "b")
	require.NoError(t, os.Symlink(b, a))
	require.NoError(t, os.Symlink(a, b))

	withTimeout(t, func(ctx context.Context) {
		stdout, stderr, code := testutil.RunScriptCtx(ctx, t, "pwd -P", a, interp.AllowedPaths([]string{root}))
		assert.Equal(t, 1, code)
		assert.Equal(t, "", stdout, "no stdout when -P aborts on cycle")
		assert.Contains(t, stderr, "too many levels of symbolic links")
	})
}

// --- Self-referential symlink (a → a) is detected via the hop cap ---

func TestPwdPentestSelfReferentialSymlink(t *testing.T) {
	if filepath.Separator == '\\' {
		t.Skip("symlinks differ on Windows")
	}
	root := canonicalTempDir(t)
	self := filepath.Join(root, "self")
	require.NoError(t, os.Symlink("self", self))

	withTimeout(t, func(ctx context.Context) {
		_, stderr, code := testutil.RunScriptCtx(ctx, t, "pwd -P", self, interp.AllowedPaths([]string{root}))
		assert.Equal(t, 1, code)
		assert.Contains(t, stderr, "too many levels of symbolic links")
	})
}

// --- Long path through many real (non-symlink) components is fine ---

func TestPwdPentestDeepRealDirectoryTree(t *testing.T) {
	root := canonicalTempDir(t)
	// Build 50 nested real directories (not symlinks).
	deepest := root
	for range 50 {
		deepest = filepath.Join(deepest, "d")
		require.NoError(t, os.Mkdir(deepest, 0755))
	}
	withTimeout(t, func(ctx context.Context) {
		stdout, stderr, code := testutil.RunScriptCtx(ctx, t, "pwd -P", deepest, interp.AllowedPaths([]string{root}))
		assert.Equal(t, 0, code, "stderr=%q", stderr)
		assert.Equal(t, deepest+"\n", stdout)
	})
}

// --- 39 chained symlinks (one below the 40-hop cap) succeeds ---

func TestPwdPentestSymlinkChainAtCapMinusOne(t *testing.T) {
	if filepath.Separator == '\\' {
		t.Skip("symlinks differ on Windows")
	}
	root := canonicalTempDir(t)
	target := filepath.Join(root, "target")
	require.NoError(t, os.Mkdir(target, 0755))
	prev := target
	for i := range 39 {
		next := filepath.Join(root, "lnk"+string('a'+byte(i%26))+strings.Repeat("x", i))
		require.NoError(t, os.Symlink(prev, next))
		prev = next
	}
	withTimeout(t, func(ctx context.Context) {
		stdout, stderr, code := testutil.RunScriptCtx(ctx, t, "pwd -P", prev, interp.AllowedPaths([]string{root}))
		assert.Equal(t, 0, code, "stderr=%q", stderr)
		assert.Equal(t, target+"\n", stdout)
	})
}

// --- Path with embedded special characters is preserved verbatim ---

func TestPwdPentestPathWithSpaces(t *testing.T) {
	root := canonicalTempDir(t)
	weird := filepath.Join(root, "with spaces and 'quotes'")
	require.NoError(t, os.Mkdir(weird, 0755))

	withTimeout(t, func(ctx context.Context) {
		stdout, stderr, code := testutil.RunScriptCtx(ctx, t, "pwd", weird, interp.AllowedPaths([]string{root}))
		assert.Equal(t, 0, code, "stderr=%q", stderr)
		assert.Equal(t, weird+"\n", stdout)
	})
}

func TestPwdPentestPathWithUnicode(t *testing.T) {
	root := canonicalTempDir(t)
	weird := filepath.Join(root, "ünïcödé-測試-🦝")
	require.NoError(t, os.Mkdir(weird, 0755))

	withTimeout(t, func(ctx context.Context) {
		stdout, stderr, code := testutil.RunScriptCtx(ctx, t, "pwd", weird, interp.AllowedPaths([]string{root}))
		assert.Equal(t, 0, code, "stderr=%q", stderr)
		assert.Equal(t, weird+"\n", stdout)
	})
}

// --- Output stays bounded: pwd never produces more than one line ---

func TestPwdPentestOutputIsSingleLine(t *testing.T) {
	dir := canonicalTempDir(t)
	withTimeout(t, func(ctx context.Context) {
		stdout, _, code := pwdRunCtx(ctx, t, "pwd", dir)
		assert.Equal(t, 0, code)
		assert.Equal(t, 1, strings.Count(stdout, "\n"))
	})
}

// --- Help output stays bounded (a small fixed payload) ---

func TestPwdPentestHelpOutputBounded(t *testing.T) {
	dir := canonicalTempDir(t)
	withTimeout(t, func(ctx context.Context) {
		stdout, _, code := pwdRunCtx(ctx, t, "pwd --help", dir)
		assert.Equal(t, 0, code)
		assert.Less(t, len(stdout), 4096, "help should be a small fixed payload")
	})
}

// --- Context cancellation aborts an in-flight -P walk promptly ---

func TestPwdPentestContextCancelDuringPhysicalWalk(t *testing.T) {
	if filepath.Separator == '\\' {
		t.Skip("symlinks differ on Windows")
	}
	root := canonicalTempDir(t)
	target := filepath.Join(root, "target")
	require.NoError(t, os.Mkdir(target, 0755))
	prev := target
	// Build a valid 39-link chain (well under the cap, and won't error).
	for i := range 39 {
		next := filepath.Join(root, "lnk"+string('a'+byte(i%26))+strings.Repeat("x", i))
		require.NoError(t, os.Symlink(prev, next))
		prev = next
	}
	ctx, cancel := context.WithCancel(context.Background())
	cancel() // pre-cancel: the resolver must give up at the first iteration
	stdout, stderr, code := testutil.RunScriptCtx(ctx, t, "pwd -P", prev, interp.AllowedPaths([]string{root}))
	// The interpreter may report a context-cancelled error or simply
	// return early. Either way, we must not hang and must not write
	// stdout (since the script never completed).
	_ = stdout
	_ = stderr
	_ = code
	// no assertion on exact code; the goal is "did not hang"
}

// --- pwd does not run any external command ---
//
// The runner is configured to allow all commands but the *only* allowed
// path is `dir`. If pwd were to exec/spawn anything, the interpreter
// would refuse it (because the binary lives outside dir). No assertion
// needed beyond "does not hang and does not error."

func TestPwdPentestDoesNotInvokeExternalCommand(t *testing.T) {
	dir := canonicalTempDir(t)
	withTimeout(t, func(ctx context.Context) {
		_, stderr, code := pwdRunCtx(ctx, t, "pwd", dir)
		assert.Equal(t, 0, code)
		assert.Equal(t, "", stderr)
	})
}

// --- Concurrent shell runs of pwd in different goroutines must be safe ---

func TestPwdPentestConcurrentRuns(t *testing.T) {
	dir := canonicalTempDir(t)
	const N = 16
	errCh := make(chan error, N)
	withTimeout(t, func(ctx context.Context) {
		for range N {
			go func() {
				_, _, code := pwdRunCtx(ctx, t, "pwd", dir)
				if code != 0 {
					errCh <- assertErr("non-zero exit")
					return
				}
				errCh <- nil
			}()
		}
		for range N {
			require.NoError(t, <-errCh)
		}
	})
}

// assertErr is a tiny helper used by TestPwdPentestConcurrentRuns to
// avoid pulling in a fmt.Errorf in pentest code.
type assertErr string

func (e assertErr) Error() string { return string(e) }
