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

// Security-focused (pentest) tests for the ping builtin.
// Each test exercises a class of attack and verifies the implementation
// behaves safely (no crash, no hang, exit 0 or 1 only).

package ping_test

import (
	"context"
	"strings"
	"testing"
	"time"

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

// ============================================================================
// GTFOBins / dangerous flag vectors
// ============================================================================

func TestPingPentestFloodRejected(t *testing.T) {
	// -f flood is a DoS vector against the target network.
	_, stderr, code := cmdRun(t, "ping -f 127.0.0.1")
	assert.Equal(t, 1, code, "-f must be rejected as unknown flag")
	assert.Contains(t, stderr, "ping:")
}

func TestPingPentestBroadcastRejected(t *testing.T) {
	// -b allows pinging broadcast addresses, which can amplify traffic.
	_, stderr, code := cmdRun(t, "ping -b 255.255.255.255")
	assert.Equal(t, 1, code, "-b must be rejected as unknown flag")
	assert.Contains(t, stderr, "ping:")
}

func TestPingPentestSizeRejected(t *testing.T) {
	// -s would let the caller control payload size; not implemented.
	_, stderr, code := cmdRun(t, "ping -s 65507 localhost")
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "ping:")
}

func TestPingPentestInterfaceRejected(t *testing.T) {
	// -I interface binding not implemented.
	_, stderr, code := cmdRun(t, "ping -I eth0 localhost")
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "ping:")
}

func TestPingPentestPatternRejected(t *testing.T) {
	// -p pattern filling not implemented.
	_, stderr, code := cmdRun(t, "ping -p ff localhost")
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "ping:")
}

func TestPingPentestRecordRouteRejected(t *testing.T) {
	// -R record-route not implemented.
	_, stderr, code := cmdRun(t, "ping -R localhost")
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "ping:")
}

// ============================================================================
// Integer overflow / boundary inputs
// ============================================================================

func TestPingPentestCountNegative(t *testing.T) {
	// Negative count clamped to 1; should not hang.
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	// DNS resolution of "no-such-host-xyzzy.invalid" should fail quickly.
	_, _, code := runScriptCtx(ctx, t, "ping -c -99 no-such-host-xyzzy.invalid")
	assert.Equal(t, 1, code)
	assert.NoError(t, ctx.Err(), "should not exceed 5-second deadline")
}

func TestPingPentestCountZero(t *testing.T) {
	// -c 0 is clamped to 1; should not hang.
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	_, _, _ = runScriptCtx(ctx, t, "ping -c 0 no-such-host-xyzzy.invalid")
	assert.NoError(t, ctx.Err(), "clamped count=0 should not hang")
}

func TestPingPentestCountOverflow(t *testing.T) {
	// Very large count clamped to 20.
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	_, _, _ = runScriptCtx(ctx, t, "ping -c 2147483647 no-such-host-xyzzy.invalid")
	assert.NoError(t, ctx.Err(), "large count should not hang (clamped + DNS fails fast)")
}

func TestPingPentestIntervalTooShort(t *testing.T) {
	// Interval below 200ms floor; should not hang.
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	_, _, _ = runScriptCtx(ctx, t, "ping -c 1 -i 1ns no-such-host-xyzzy.invalid")
	assert.NoError(t, ctx.Err())
}

// ============================================================================
// Path / host injection
// ============================================================================

func TestPingPentestShellInjectionInHost(t *testing.T) {
	// Verify that the literal string "$(id)" is treated as a hostname (DNS
	// lookup fails) rather than triggering command substitution inside the
	// builtin. The argument is single-quoted so the shell passes it verbatim
	// to ping; only the resolver sees it.
	_, _, code := cmdRun(t, `ping -c 1 '$(id)'`)
	assert.Equal(t, 1, code)
}

func TestPingPentestLongHostname(t *testing.T) {
	// A very long hostname should result in a DNS error, not a crash.
	host := strings.Repeat("a", 10000) + ".invalid"
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	_, _, code := runScriptCtx(ctx, t, "ping -c 1 -- "+host)
	assert.Equal(t, 1, code, "long hostname should fail with exit 1")
	assert.NoError(t, ctx.Err(), "should not hang on long hostname")
}

func TestPingPentestEmptyHostname(t *testing.T) {
	// Empty hostname (after shell expansion) should give a clear error.
	_, stderr, code := cmdRun(t, `ping -c 1 ""`)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "ping:")
}

// ============================================================================
// Context cancellation (timeout safety)
// ============================================================================

func TestPingPentestHeaderPrintedBeforeICMP(t *testing.T) {
	// The PING header is emitted after DNS resolution but before opening the
	// ICMP socket. This test verifies the invariant: stdout contains the
	// header even when the ICMP step fails (e.g. EPERM or timeout).
	// 192.0.2.1 is an RFC 5737 documentation address that never responds.
	stdout, _, code := cmdRun(t, "ping -c 1 -W 100ms 192.0.2.1")
	assert.Equal(t, 1, code)
	assert.Contains(t, stdout, "PING 192.0.2.1 (192.0.2.1):", "PING header must appear in stdout even when ICMP fails")
}

func TestPingPentestContextTimeout(t *testing.T) {
	// With a very short outer deadline, RunWithContext must exit promptly.
	ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
	defer cancel()
	start := time.Now()
	_, _, _ = runScriptCtx(ctx, t, "ping -c 10 -W 10s -i 5s 192.0.2.1")
	elapsed := time.Since(start)
	assert.Less(t, elapsed, 2*time.Second, "ping must stop when context is cancelled")
}

func TestPingPentestHardTotalTimeout(t *testing.T) {
	// The builtin computes a hard total timeout. With maxed-out count and
	// wait, the deadline must still be within the 120s cap.
	// We use an unresolvable host so execution terminates on DNS failure.
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	// -c 20 -W 30s -i 60s = 20*(60+30)+5 = 1805s → capped at 120s. But DNS
	// failure returns immediately.
	_, _, code := runScriptCtx(ctx, t, "ping -c 20 -W 30s -i 60s no-such-host-xyzzy.invalid")
	assert.Equal(t, 1, code)
	assert.NoError(t, ctx.Err(), "should not exceed 5-second outer deadline")
}
