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

import (
	"bytes"
	"context"
	"errors"
	"runtime"
	"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"
)

// runWithCtx runs a shell script with a caller-supplied context.
// Used for timeout-sensitive tests; all other tests use runScript.
func runWithCtx(ctx context.Context, t *testing.T, script string) (stdout, stderr string, exitCode int) {
	t.Helper()
	parser := syntax.NewParser()
	prog, err := parser.Parse(strings.NewReader(script), "")
	require.NoError(t, err)
	var outBuf, errBuf bytes.Buffer
	runner, err := interp.New(interp.StdIO(nil, &outBuf, &errBuf), interpoption.AllowAllCommands().(interp.RunnerOption))
	require.NoError(t, err)
	defer runner.Close()
	runErr := runner.Run(ctx, prog)
	exitCode = 0
	if runErr != nil {
		var es interp.ExitStatus
		if errors.As(runErr, &es) {
			exitCode = int(es)
		} else if ctx.Err() == nil {
			t.Fatalf("unexpected error: %v", runErr)
		}
	}
	return outBuf.String(), errBuf.String(), exitCode
}

// ============================================================================
// GTFOBins vectors
// ============================================================================

// TestIPPentestBatchFlagLower verifies -b is rejected (GTFOBins file-read via batch mode).
func TestIPPentestBatchFlagLower(t *testing.T) {
	_, stderr, code := runScript(t, "ip -b /tmp/evil addr show", "")
	assert.Equal(t, 1, code)
	assert.NotEmpty(t, stderr)
}

// TestIPPentestBatchFlagUpper verifies -B is rejected.
func TestIPPentestBatchFlagUpper(t *testing.T) {
	_, stderr, code := runScript(t, "ip -B /tmp/evil addr show", "")
	assert.Equal(t, 1, code)
	assert.NotEmpty(t, stderr)
}

// TestIPPentestForceFlag verifies --force is rejected (GTFOBins companion to -batch).
func TestIPPentestForceFlag(t *testing.T) {
	_, stderr, code := runScript(t, "ip --force addr show", "")
	assert.Equal(t, 1, code)
	assert.NotEmpty(t, stderr)
}

// TestIPPentestNetnsExec verifies ip netns exec is blocked (shell escape).
func TestIPPentestNetnsExec(t *testing.T) {
	_, stderr, code := runScript(t, "ip netns exec mynamespace sh", "")
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "blocked")
}

// TestIPPentestNetnsAdd verifies ip netns add is blocked (namespace creation).
func TestIPPentestNetnsAdd(t *testing.T) {
	_, stderr, code := runScript(t, "ip netns add myns", "")
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "blocked")
}

// TestIPPentestNetnsFlag verifies -n (netns switch) is rejected.
func TestIPPentestNetnsFlag(t *testing.T) {
	_, stderr, code := runScript(t, "ip -n mynamespace addr show", "")
	assert.Equal(t, 1, code)
	assert.NotEmpty(t, stderr)
}

// ============================================================================
// Write operation blocking
// ============================================================================

// TestIPPentestAddrAdd verifies addr add is blocked.
func TestIPPentestAddrAdd(t *testing.T) {
	_, stderr, code := runScript(t, "ip addr add 192.168.1.100/24 dev lo", "")
	assert.Equal(t, 1, code)
	assert.NotEmpty(t, stderr)
}

// TestIPPentestAddrDel verifies addr del is blocked.
func TestIPPentestAddrDel(t *testing.T) {
	_, stderr, code := runScript(t, "ip addr del 192.168.1.100/24 dev lo", "")
	assert.Equal(t, 1, code)
	assert.NotEmpty(t, stderr)
}

// TestIPPentestAddrFlush verifies addr flush is blocked.
func TestIPPentestAddrFlush(t *testing.T) {
	_, stderr, code := runScript(t, "ip addr flush dev lo", "")
	assert.Equal(t, 1, code)
	assert.NotEmpty(t, stderr)
}

// TestIPPentestAddrChange verifies addr change is blocked.
func TestIPPentestAddrChange(t *testing.T) {
	_, stderr, code := runScript(t, "ip addr change 192.168.1.100/24 dev lo", "")
	assert.Equal(t, 1, code)
	assert.NotEmpty(t, stderr)
}

// TestIPPentestAddrReplace verifies addr replace is blocked.
func TestIPPentestAddrReplace(t *testing.T) {
	_, stderr, code := runScript(t, "ip addr replace 192.168.1.100/24 dev lo", "")
	assert.Equal(t, 1, code)
	assert.NotEmpty(t, stderr)
}

// TestIPPentestAddrAppend verifies addr append is blocked.
func TestIPPentestAddrAppend(t *testing.T) {
	_, stderr, code := runScript(t, "ip addr append 192.168.1.100/24 dev lo", "")
	assert.Equal(t, 1, code)
	assert.NotEmpty(t, stderr)
}

// TestIPPentestLinkSet verifies link set is blocked.
func TestIPPentestLinkSet(t *testing.T) {
	_, stderr, code := runScript(t, "ip link set lo up", "")
	assert.Equal(t, 1, code)
	assert.NotEmpty(t, stderr)
}

// TestIPPentestLinkDel verifies link del is blocked.
func TestIPPentestLinkDel(t *testing.T) {
	_, stderr, code := runScript(t, "ip link del lo", "")
	assert.Equal(t, 1, code)
	assert.NotEmpty(t, stderr)
}

// TestIPPentestLinkChange verifies link change is blocked.
func TestIPPentestLinkChange(t *testing.T) {
	_, stderr, code := runScript(t, "ip link change lo mtu 1500", "")
	assert.Equal(t, 1, code)
	assert.NotEmpty(t, stderr)
}

// ============================================================================
// Unknown/unsupported subcommands
// ============================================================================

// TestIPPentestRouteSubcmd verifies ip route behaviour by platform.
// On Linux, ip route show is implemented and exits 0.
// On other platforms it is not supported and exits 1 with an error.
func TestIPPentestRouteSubcmd(t *testing.T) {
	if runtime.GOOS == "linux" {
		// ip route is now implemented on Linux; tested in depth in builtins/tests/ip/.
		t.Skip("ip route is implemented on Linux; skipping pentest stub check")
	}
	_, stderr, code := runScript(t, "ip route show", "")
	assert.Equal(t, 1, code)
	assert.NotEmpty(t, stderr)
}

// TestIPPentestTunnelSubcmd verifies ip tunnel is blocked.
func TestIPPentestTunnelSubcmd(t *testing.T) {
	_, stderr, code := runScript(t, "ip tunnel add tun0 mode gre remote 1.2.3.4", "")
	assert.Equal(t, 1, code)
	assert.NotEmpty(t, stderr)
}

// TestIPPentestMonitorSubcmd verifies ip monitor is blocked (infinite stream).
func TestIPPentestMonitorSubcmd(t *testing.T) {
	_, _, code := runScript(t, "ip monitor", "")
	assert.Equal(t, 1, code)
}

// TestIPPentestXfrmSubcmd verifies ip xfrm is blocked.
func TestIPPentestXfrmSubcmd(t *testing.T) {
	_, _, code := runScript(t, "ip xfrm state list", "")
	assert.Equal(t, 1, code)
}

// ============================================================================
// Flag injection and edge cases
// ============================================================================

// TestIPPentestUnknownFlag verifies completely unknown flags are rejected.
func TestIPPentestUnknownFlag(t *testing.T) {
	_, stderr, code := runScript(t, "ip --no-such-flag addr show", "")
	assert.Equal(t, 1, code)
	assert.NotEmpty(t, stderr)
}

// TestIPPentestDoubleDashThenFlagLikeName verifies -- stops flag parsing;
// "ip -- --help" tries to show interface named "--help" which doesn't exist.
func TestIPPentestDoubleDashThenFlagLikeName(t *testing.T) {
	_, stderr, code := runScript(t, "ip -- --help", "")
	// "--help" is treated as an object name and rejected.
	assert.Equal(t, 1, code)
	assert.NotEmpty(t, stderr)
}

// TestIPPentestEmptyDevArg verifies "dev "" is safe — treated as no-filter
// and shows all interfaces (empty string is the "no filter" sentinel value
// in getInterfaces; no interface can have an empty name at the OS level).
func TestIPPentestEmptyDevArg(t *testing.T) {
	stdout, _, code := runScript(t, `ip addr show dev ""`, "")
	// Safe: either exits 0 (shows all interfaces) or 1 (cannot find device "").
	// Either outcome is acceptable — neither executes code nor leaks sensitive data.
	assert.True(t, code == 0 || code == 1, "unexpected exit code %d", code)
	// If exit 0, we should see interface data (not arbitrary file content).
	if code == 0 {
		assert.NotContains(t, stdout, "/etc/passwd")
	}
}

// TestIPPentestFlagInjectionViaShellVar verifies flag injection via variable expansion
// does not bypass pflag parsing (pflag processes args slice, not string).
func TestIPPentestFlagInjectionViaShellVar(t *testing.T) {
	// The -b flag is rejected regardless of whether it came from a literal or variable.
	_, stderr, code := runScript(t, `
FLAG=-b
ip $FLAG /tmp/evil addr show
`, "")
	assert.Equal(t, 1, code)
	assert.NotEmpty(t, stderr)
}

// ============================================================================
// Context cancellation / DoS prevention
// ============================================================================

// TestIPPentestAddrShowNoHang verifies ip addr show completes within 5 seconds.
func TestIPPentestAddrShowNoHang(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	_, _, code := runWithCtx(ctx, t, "ip addr show")
	assert.True(t, code == 0 || code == 1, "unexpected exit code: %d", code)
}

// TestIPPentestLinkShowNoHang verifies ip link show completes within 5 seconds.
func TestIPPentestLinkShowNoHang(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	_, _, code := runWithCtx(ctx, t, "ip link show")
	assert.True(t, code == 0 || code == 1, "unexpected exit code: %d", code)
}

// TestIPPentestNoStdinRead verifies ip does not block reading from stdin
// (ip queries OS directly and never reads stdin).
func TestIPPentestNoStdinRead(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()
	// If ip tries to read stdin, this will hang until the timeout.
	_, _, code := runWithCtx(ctx, t, "ip addr show")
	// Should complete fast; ctx.Err() will be non-nil only on timeout.
	assert.Nil(t, ctx.Err(), "ip addr show hung (possible stdin read)")
	assert.True(t, code == 0 || code == 1)
}
