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

//go:build linux

package ip_test

// Pentest tests for ip route. These verify that edge-case and adversarial inputs
// produce safe outcomes (exit 0 or 1, no panics, no hangs).
//
// Run with:
//
//	GOOS=linux go test ./builtins/tests/ip/ -run TestIPRoutePentest -timeout 120s -v

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

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

	ipcmd "github.com/DataDog/rshell/builtins/ip"
)

// ============================================================================
// Integer edge cases — "ip route get ADDRESS"
// ============================================================================

// TestIPRoutePentestGetAddressEdgeCases verifies that extreme and invalid
// address formats are rejected cleanly (exit 1, no panic, no hang).
func TestIPRoutePentestGetAddressEdgeCases(t *testing.T) {
	writeProcNetRoute(t, syntheticProcNetRoute)

	cases := []string{
		// Valid but degenerate
		"0.0.0.0",
		"255.255.255.255",
		// Invalid — too few octets
		"192.168",
		"192.168.1",
		// Invalid — too many octets
		"1.2.3.4.5",
		// Invalid — octet overflow
		"256.0.0.0",
		"0.0.0.256",
		"999.999.999.999",
		// Non-numeric
		"abc.def.ghi.jkl",
		// Very long — should not allocate unboundedly
		strings.Repeat("1", 1024) + ".0.0.0",
		// IPv6 address — rejected by parseIPv4
		"::1",
		"2001:db8::1",
		// Empty octet: Split gives 4 parts but ParseUint("") fails correctly.
		"1..2.3",
		"192.168..1",
	}

	for _, addr := range cases {
		t.Run(addr, func(t *testing.T) {
			ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
			defer cancel()
			_, _, code := runScriptCtx(ctx, t, "ip route get "+addr, "")
			timedOut := ctx.Err() == context.DeadlineExceeded
			if timedOut {
				t.Errorf("ip route get %q: timed out", addr)
			}
			assert.True(t, code == 0 || code == 1, "expected exit 0 or 1 for addr %q, got %d", addr, code)
		})
	}
}

// ============================================================================
// Special files as ProcNetRoutePath
// ============================================================================

// TestIPRoutePentestDevNull verifies that an empty net/route file produces
// empty output with exit 0 (empty table, not a hang or crash).
func TestIPRoutePentestDevNull(t *testing.T) {
	// Write an empty net/route file to simulate /dev/null content.
	writeProcNetRoute(t, "")

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	stdout, _, code := runScriptCtx(ctx, t, "ip route show", "")
	if ctx.Err() == context.DeadlineExceeded {
		t.Fatal("ip route show empty route: timed out")
	}
	// Empty file → no routes parsed → empty output, exit 0.
	assert.Equal(t, 0, code)
	assert.Empty(t, stdout)
}

// TestIPRoutePentestDevZero verifies that a net/route symlinked to /dev/zero
// does not hang. The scanner's MaxLineBytes cap ensures the read terminates.
func TestIPRoutePentestDevZero(t *testing.T) {
	// Create temp proc dir structure with net/route -> /dev/zero.
	dir := t.TempDir()
	netDir := filepath.Join(dir, "net")
	require.NoError(t, os.MkdirAll(netDir, 0o755))
	require.NoError(t, os.Symlink("/dev/zero", filepath.Join(netDir, "route")))
	procNetRouteMu.Lock()
	orig := ipcmd.ProcNetRoutePath
	ipcmd.ProcNetRoutePath = dir
	t.Cleanup(func() {
		ipcmd.ProcNetRoutePath = orig
		procNetRouteMu.Unlock()
	})

	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()
	_, _, code := runScriptCtx(ctx, t, "ip route show", "")
	timedOut := ctx.Err() == context.DeadlineExceeded
	if timedOut {
		t.Fatal("ip route show /dev/zero: timed out — MaxLineBytes cap not working")
	}
	// Must exit 0 or 1; the exact code depends on whether the scanner error
	// propagates (it will fail with "token too long" after MaxLineBytes).
	assert.True(t, code == 0 || code == 1, "expected exit 0 or 1, got %d", code)
}

// ============================================================================
// Long lines
// ============================================================================

// TestIPRoutePentestLongLines verifies that /proc/net/route lines up to and
// beyond MaxLineBytes are handled safely (no panic, no OOM).
func TestIPRoutePentestLongLines(t *testing.T) {
	validRow := "eth0\t00000000\t0101A8C0\t0003\t0\t0\t100\t00000000\t0\t0\t0\n"

	tests := []struct {
		name         string
		padLen       int
		wantExitCode int // expected exit code: 0 = row parsed, 1 = scanner ErrTooLong
	}{
		// All three pad lengths are large (near or above MaxLineBytes).
		// The total line length = ~50 base bytes + padLen, which in every case
		// exceeds the scanner's MaxLineBytes cap.  bufio.ErrTooLong is returned,
		// ReadRoutes propagates the error, and ip route show exits 1.
		{"pad_MaxLineBytes_minus_1", ipcmd.MaxLineBytes - 1, 1},
		{"pad_MaxLineBytes", ipcmd.MaxLineBytes, 1},
		{"pad_MaxLineBytes_plus_1", ipcmd.MaxLineBytes + 1, 1},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			// Build a line that starts with a valid row but has padding appended.
			// The padding is spaces (not newlines) so the scanner sees it as one long line.
			row := strings.TrimRight(validRow, "\n") + strings.Repeat(" ", tc.padLen) + "\n"
			content := "Iface\tDestination\tGateway\tFlags\tRefCnt\tUse\tMetric\tMask\tMTU\tWindow\tIRTT\n" + row
			writeProcNetRoute(t, content)

			ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
			defer cancel()
			_, _, code := runScriptCtx(ctx, t, "ip route show", "")
			if ctx.Err() == context.DeadlineExceeded {
				t.Fatalf("ip route show: timed out on %s line", tc.name)
			}
			assert.Equal(t, tc.wantExitCode, code, "unexpected exit code for %s", tc.name)
		})
	}
}

// ============================================================================
// Memory / resource exhaustion
// ============================================================================

// TestIPRoutePentestManyFileArgs verifies ip route does not leak file descriptors
// when invoked many times in sequence (each invocation opens ProcNetRoutePath once).
func TestIPRoutePentestManyFileArgs(t *testing.T) {
	writeProcNetRoute(t, syntheticProcNetRoute)
	for range 50 {
		_, _, code := cmdRun(t, "ip route show")
		assert.Equal(t, 0, code)
	}
}

// ============================================================================
// Flag and argument injection
// ============================================================================

// TestIPRoutePentestUnknownFlags verifies unknown flags produce exit 1.
func TestIPRoutePentestUnknownFlags(t *testing.T) {
	unknownFlags := []string{
		"ip route --follow",
		"ip route -f",
		"ip route --no-such-flag",
		"ip route show --filter=evil",
	}
	for _, script := range unknownFlags {
		t.Run(script, func(t *testing.T) {
			_, _, code := cmdRun(t, script)
			assert.Equal(t, 1, code, "%s: expected exit 1", script)
		})
	}
}

// TestIPRoutePentestEndOfFlags verifies that -- end-of-flags is handled
// gracefully (the word after -- is treated as a subcommand, which will be
// unknown unless it's show/list/get).
func TestIPRoutePentestEndOfFlags(t *testing.T) {
	writeProcNetRoute(t, syntheticProcNetRoute)
	// "ip route -- show" — the shell strips -- before passing args to ip.
	// "ip" receives ["route", "--", "show"]; after pflag parsing, args=["route","show"].
	stdout, _, code := cmdRun(t, "ip route -- show")
	// ip's pflag processes -- as end-of-flags; "show" becomes a positional arg
	// passed to routeCmd which treats it as the subcommand. Depending on how
	// pflag passes -- through, this should succeed or return exit 1.
	assert.True(t, code == 0 || code == 1, "expected 0 or 1, got %d", code)
	_ = stdout
}

// ============================================================================
// Integer overflow in metric field
// ============================================================================

// TestIPRoutePentestMetricOverflow verifies that a metric value exceeding
// uint32 max is handled gracefully (line skipped or truncated, no panic).
func TestIPRoutePentestMetricOverflow(t *testing.T) {
	overflowCases := []string{
		"4294967296",             // MaxUint32 + 1
		"99999999999999",         // very large decimal
		"0xFFFFFFFFFFFF",         // hex prefix (not valid decimal, will fail ParseUint)
		"-1",                     // negative metric
		strings.Repeat("9", 100), // very long decimal
	}
	for _, metric := range overflowCases {
		t.Run(metric, func(t *testing.T) {
			content := "Iface\tDestination\tGateway\tFlags\tRefCnt\tUse\tMetric\tMask\tMTU\tWindow\tIRTT\n" +
				"eth0\t00000000\t0101A8C0\t0003\t0\t0\t" + metric + "\t00000000\t0\t0\t0\n"
			writeProcNetRoute(t, content)
			_, _, code := cmdRun(t, "ip route show")
			// The row is skipped (ParseUint fails) → empty output but exit 0.
			assert.True(t, code == 0 || code == 1, "expected 0 or 1 for metric %q, got %d", metric, code)
		})
	}
}

// ============================================================================
// Binary / adversarial file content
// ============================================================================

// TestIPRoutePentestBinaryContent verifies that binary garbage in ProcNetRoutePath
// is handled safely (no panic, no hang).
func TestIPRoutePentestBinaryContent(t *testing.T) {
	binaryCases := [][]byte{
		{0x00, 0x01, 0x02, 0x03, 0xFF, 0xFE},                      // null bytes + high bytes
		[]byte("\x7fELF\x02\x01\x01\x00"),                         // ELF magic bytes
		[]byte("MZ\x90\x00\x03\x00\x00\x00"),                      // PE magic bytes
		[]byte("PK\x03\x04"),                                      // ZIP magic bytes
		[]byte("\x1b[31mred\x1b[0m"),                              // ANSI escape sequences
		append([]byte("Iface\tDest\tGW\n"), make([]byte, 100)...), // valid header + nulls
	}

	for i, content := range binaryCases {
		t.Run("binary_"+string(rune('A'+i)), func(t *testing.T) {
			writeProcNetRoute(t, string(content))
			ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
			defer cancel()
			_, _, code := runScriptCtx(ctx, t, "ip route show", "")
			if ctx.Err() == context.DeadlineExceeded {
				t.Fatal("ip route show: timed out on binary content")
			}
			assert.True(t, code == 0 || code == 1, "expected 0 or 1 for binary case %d, got %d", i, code)
		})
	}
}

// ============================================================================
// Behaviour matching
// ============================================================================

// TestIPRoutePentestOutputFormat verifies the ip route show output format
// matches the expected ip-route(8) text format for known inputs.
// This documents intentional differences from real ip route (no proto/scope/src).
func TestIPRoutePentestOutputFormat(t *testing.T) {
	// Single default route via a gateway on eth0 with metric 100.
	content := "Iface\tDestination\tGateway\tFlags\tRefCnt\tUse\tMetric\tMask\tMTU\tWindow\tIRTT\n" +
		"eth0\t00000000\t0101A8C0\t0003\t0\t0\t100\t00000000\t0\t0\t0\n"
	writeProcNetRoute(t, content)
	stdout, _, code := cmdRun(t, "ip route show")
	assert.Equal(t, 0, code)
	// Expected: "default via 192.168.1.1 dev eth0 metric 100"
	assert.Equal(t, "default via 192.168.1.1 dev eth0 metric 100\n", stdout)
}

// TestIPRoutePentestGetOutputFormat verifies ip route get output format.
func TestIPRoutePentestGetOutputFormat(t *testing.T) {
	content := "Iface\tDestination\tGateway\tFlags\tRefCnt\tUse\tMetric\tMask\tMTU\tWindow\tIRTT\n" +
		"eth0\t00000000\t0101A8C0\t0003\t0\t0\t100\t00000000\t0\t0\t0\n"
	writeProcNetRoute(t, content)
	stdout, _, code := cmdRun(t, "ip route get 8.8.8.8")
	assert.Equal(t, 0, code)
	// Expected: "8.8.8.8 via 192.168.1.1 dev eth0 metric 100\n"
	// metric is included to match real ip-route(8) output for route get.
	assert.Equal(t, "8.8.8.8 via 192.168.1.1 dev eth0 metric 100\n", stdout)
}
