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

import (
	"bytes"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"testing"

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

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

func runStrings(t *testing.T, script, dir string) (string, string, int) {
	t.Helper()
	return testutil.RunScript(t, script, dir, interp.AllowedPaths([]string{dir}))
}

func writeFileBytes(t *testing.T, dir, name string, content []byte) {
	t.Helper()
	require.NoError(t, os.WriteFile(filepath.Join(dir, name), content, 0644))
}

// TestStringsCap1MiB verifies that a printable sequence longer than maxStringLen
// (1 MiB) is capped: only the first 1 MiB is emitted and scanning continues
// correctly after the next non-printable separator.
func TestStringsCap1MiB(t *testing.T) {
	dir := t.TempDir()

	// Build: 2 MiB of 'A' + null byte + "after_cap"
	const capSize = 1 << 20 // 1 MiB = maxStringLen in strings.go
	var buf bytes.Buffer
	buf.Write(bytes.Repeat([]byte{'A'}, capSize*2))
	buf.WriteByte(0x00)
	buf.WriteString("after_cap")
	writeFileBytes(t, dir, "big.bin", buf.Bytes())

	stdout, _, code := runStrings(t, "strings big.bin", dir)
	assert.Equal(t, 0, code)

	lines := strings.Split(strings.TrimRight(stdout, "\n"), "\n")
	require.Len(t, lines, 2, "expected exactly 2 output strings (capped first + after_cap)")

	// First string: exactly 1 MiB of 'A' (bytes beyond cap are silently dropped)
	assert.Equal(t, capSize, len(lines[0]), "first string must be exactly 1 MiB (cap enforced)")
	assert.Equal(t, bytes.Repeat([]byte{'A'}, capSize), []byte(lines[0]))

	// Second string: the content after the null separator
	assert.Equal(t, "after_cap", lines[1])
}

// TestStringsCapOffsetAfterCap verifies that the byte offset reported for the
// string following a capped sequence is the correct file position.
func TestStringsCapOffsetAfterCap(t *testing.T) {
	dir := t.TempDir()

	// 2 MiB of 'A' + null + "offset_check"
	const capSize = 1 << 20
	var buf bytes.Buffer
	buf.Write(bytes.Repeat([]byte{'A'}, capSize*2))
	buf.WriteByte(0x00)
	buf.WriteString("offset_check")
	writeFileBytes(t, dir, "big.bin", buf.Bytes())

	stdout, _, code := runStrings(t, "strings -t d big.bin", dir)
	assert.Equal(t, 0, code)

	lines := strings.Split(strings.TrimRight(stdout, "\n"), "\n")
	require.Len(t, lines, 2)

	// The string "offset_check" starts at offset: capSize*2 + 1 (past the null byte)
	expectedOffset := capSize*2 + 1
	expectedPrefix := fmt.Sprintf("%7d offset_check", expectedOffset)
	assert.Equal(t, expectedPrefix, lines[1])
}

// TestStringsAllNullBytes verifies that a file of all null bytes produces no
// output (null is not printable) and exits 0 without hanging.
func TestStringsAllNullBytes(t *testing.T) {
	dir := t.TempDir()
	writeFileBytes(t, dir, "nulls.bin", make([]byte, 64*1024)) // 64 KiB of nulls
	stdout, stderr, code := runStrings(t, "strings nulls.bin", dir)
	assert.Equal(t, 0, code)
	assert.Equal(t, "", stdout)
	assert.Equal(t, "", stderr)
}

// TestStringsAllPrintable verifies that a file entirely consisting of printable
// ASCII characters is emitted as a single string.
func TestStringsAllPrintable(t *testing.T) {
	dir := t.TempDir()
	data := bytes.Repeat([]byte{'X'}, 4096) // 4 KiB — well under the cap
	writeFileBytes(t, dir, "print.bin", data)
	stdout, _, code := runStrings(t, "strings print.bin", dir)
	assert.Equal(t, 0, code)
	assert.Equal(t, strings.Repeat("X", 4096)+"\n", stdout)
}

// TestStringsLargeOffsetDecimal verifies that decimal offsets exceeding 7 digits
// are not truncated (the field expands beyond 7 characters, matching GNU strings).
func TestStringsLargeOffsetDecimal(t *testing.T) {
	dir := t.TempDir()

	// Place "findme" at offset 10,000,000 (8 decimal digits, exceeds 7-char field)
	const targetOffset = 10_000_000
	data := make([]byte, targetOffset+len("findme"))
	// All null bytes before the target position
	copy(data[targetOffset:], "findme")
	writeFileBytes(t, dir, "big.bin", data)

	stdout, _, code := runStrings(t, "strings -t d big.bin", dir)
	assert.Equal(t, 0, code)
	// Offset 10000000 has 8 digits — fmtOffset must not truncate to 7.
	assert.Equal(t, "10000000 findme\n", stdout)
}

// --- GTFOBins validation ---

// TestStringsGTFOBinsFileReadSandboxEscape verifies that the GTFOBins file-read
// technique for strings is blocked by the AllowedPaths sandbox.
//
// GTFOBins: https://gtfobins.org/gtfobins/strings/
// Technique: strings /path/to/file
func TestStringsGTFOBinsFileReadSandboxEscape(t *testing.T) {
	allowed := t.TempDir()
	secret := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(secret, "secret.txt"), []byte("secret data\n"), 0644))
	secretPath := strings.ReplaceAll(filepath.Join(secret, "secret.txt"), `\`, `/`)
	_, stderr, code := runStrings(t, "strings "+secretPath, allowed)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "strings:")
}
