//go:build !windows

package core

import (
	"context"
	"os/exec"
	"strconv"
	"strings"
	"testing"
	"time"
)

func TestPreflightRunAsUser_AllPass(t *testing.T) {
	runner := &stubSudoRunner{
		script: map[string]stubResponse{
			key("-n", "-iu", "target", "--", "/usr/bin/true"):                          {nil, nil},
			key("-n", "-iu", "target", "--", "sudo", "-n", "/usr/bin/true"):            {nil, &exec.ExitError{}}, // escalation fails as required
			key("-n", "-iu", "target", "--", "test", "-r", "/tmp/wd", "-a", "-w", "/tmp/wd"): {nil, nil},
		},
	}
	// Build the find args the real scanner will use. For this test we
	// just make the find call return empty output so no warnings are
	// generated.
	cfg := PreflightConfig{
		Project:   "demo",
		RunAsUser: "target",
		WorkDir:   "/tmp/wd",
		Runner:    runner,
		ScanConfig: DescendantScanConfig{
			PrunePaths: []string{".git"},
			MaxReport:  50,
			Timeout:    time.Second,
		},
	}
	// Script the find call to return empty.
	findArgs := []string{
		"-n", "-iu", "target", "--",
		"find", "/tmp/wd",
		"(", "-name", ".git", ")", "-prune", "-o",
		"(",
		"-not", "-readable", "-printf", `noread\t%p\n`,
		",",
		"-type", "f", "-not", "-writable", "-printf", `nowrite\t%p\n`,
		",",
		"-type", "d", "-not", "-executable", "-printf", `nosearch\t%p\n`,
		")",
	}
	runner.script[key(findArgs...)] = stubResponse{out: []byte(""), err: nil}

	result := PreflightRunAsUser(context.Background(), cfg)
	if result.HasFatal() {
		t.Fatalf("want no fatal, got: %v", result.Fatal)
	}
	if len(result.Warnings) != 0 {
		t.Fatalf("want no warnings, got: %v", result.Warnings)
	}
}

func TestPreflightRunAsUser_NoSudoToTargetIsFatal(t *testing.T) {
	runner := &stubSudoRunner{
		script: map[string]stubResponse{
			key("-n", "-iu", "target", "--", "/usr/bin/true"): {[]byte("a password is required"), &exec.ExitError{}},
		},
	}
	cfg := PreflightConfig{Project: "demo", RunAsUser: "target", WorkDir: "/tmp/wd", Runner: runner}
	result := PreflightRunAsUser(context.Background(), cfg)
	if !result.HasFatal() {
		t.Fatal("want fatal error")
	}
	if !strings.Contains(result.Fatal[0].Error(), "passwordless sudo to user") {
		t.Errorf("want 'passwordless sudo to user' in error, got %v", result.Fatal[0])
	}
}

func TestPreflightRunAsUser_TargetCanEscalateIsFatal(t *testing.T) {
	runner := &stubSudoRunner{
		script: map[string]stubResponse{
			key("-n", "-iu", "target", "--", "/usr/bin/true"):               {nil, nil},
			key("-n", "-iu", "target", "--", "sudo", "-n", "/usr/bin/true"): {nil, nil}, // BAD
			key("-n", "-iu", "target", "--", "sudo", "-n", "-l"):        {[]byte("(ALL) NOPASSWD: ALL"), nil},
			key("-n", "-iu", "target", "--", "test", "-r", "/tmp/wd", "-a", "-w", "/tmp/wd"): {nil, nil},
		},
	}
	findArgs := []string{
		"-n", "-iu", "target", "--",
		"find", "/tmp/wd",
		"(",
		"-not", "-readable", "-printf", `noread\t%p\n`,
		",",
		"-type", "f", "-not", "-writable", "-printf", `nowrite\t%p\n`,
		",",
		"-type", "d", "-not", "-executable", "-printf", `nosearch\t%p\n`,
		")",
	}
	runner.script[key(findArgs...)] = stubResponse{out: nil, err: nil}

	cfg := PreflightConfig{
		Project:    "demo",
		RunAsUser:  "target",
		WorkDir:    "/tmp/wd",
		Runner:     runner,
		ScanConfig: DescendantScanConfig{MaxReport: 50, Timeout: time.Second},
	}
	result := PreflightRunAsUser(context.Background(), cfg)
	if !result.HasFatal() {
		t.Fatal("want fatal error")
	}
	if !strings.Contains(result.Fatal[0].Error(), "can run passwordless sudo") {
		t.Errorf("want 'can run passwordless sudo' in error, got %v", result.Fatal[0])
	}
	if !strings.Contains(result.Fatal[0].Error(), "NOPASSWD: ALL") {
		t.Errorf("want sudo -l output included in error, got %v", result.Fatal[0])
	}
}

func TestPreflightRunAsUser_WorkDirInaccessibleIsFatal(t *testing.T) {
	runner := &stubSudoRunner{
		script: map[string]stubResponse{
			key("-n", "-iu", "target", "--", "/usr/bin/true"):                          {nil, nil},
			key("-n", "-iu", "target", "--", "sudo", "-n", "/usr/bin/true"):            {nil, &exec.ExitError{}},
			key("-n", "-iu", "target", "--", "test", "-r", "/tmp/wd", "-a", "-w", "/tmp/wd"): {[]byte(""), &exec.ExitError{}},
		},
	}
	cfg := PreflightConfig{
		Project:    "demo",
		RunAsUser:  "target",
		WorkDir:    "/tmp/wd",
		Runner:     runner,
		ScanConfig: DescendantScanConfig{MaxReport: 50, Timeout: time.Second},
	}
	result := PreflightRunAsUser(context.Background(), cfg)
	if !result.HasFatal() {
		t.Fatal("want fatal error")
	}
	if !strings.Contains(result.Fatal[0].Error(), "cannot read AND write work_dir") {
		t.Errorf("want 'cannot read AND write work_dir' in error, got %v", result.Fatal[0])
	}
}

func TestPreflightRunAsUser_DescendantWarnings(t *testing.T) {
	runner := &stubSudoRunner{
		script: map[string]stubResponse{
			key("-n", "-iu", "target", "--", "/usr/bin/true"):                          {nil, nil},
			key("-n", "-iu", "target", "--", "sudo", "-n", "/usr/bin/true"):            {nil, &exec.ExitError{}},
			key("-n", "-iu", "target", "--", "test", "-r", "/tmp/wd", "-a", "-w", "/tmp/wd"): {nil, nil},
		},
	}
	findArgs := []string{
		"-n", "-iu", "target", "--",
		"find", "/tmp/wd",
		"(",
		"-not", "-readable", "-printf", `noread\t%p\n`,
		",",
		"-type", "f", "-not", "-writable", "-printf", `nowrite\t%p\n`,
		",",
		"-type", "d", "-not", "-executable", "-printf", `nosearch\t%p\n`,
		")",
	}
	runner.script[key(findArgs...)] = stubResponse{
		out: []byte("noread\t/tmp/wd/secret.env\nnowrite\t/tmp/wd/readonly.log\n"),
		err: nil,
	}
	cfg := PreflightConfig{
		Project:    "demo",
		RunAsUser:  "target",
		WorkDir:    "/tmp/wd",
		Runner:     runner,
		ScanConfig: DescendantScanConfig{MaxReport: 50, Timeout: time.Second},
	}
	result := PreflightRunAsUser(context.Background(), cfg)
	if result.HasFatal() {
		t.Fatalf("want no fatal, got: %v", result.Fatal)
	}
	if len(result.Warnings) != 1 {
		t.Fatalf("want 1 warning, got %d: %v", len(result.Warnings), result.Warnings)
	}
	if !strings.Contains(result.Warnings[0], "secret.env") || !strings.Contains(result.Warnings[0], "readonly.log") {
		t.Errorf("warning missing expected paths: %s", result.Warnings[0])
	}
}

func TestPreflightRunAsUser_DescendantWarningsCapped(t *testing.T) {
	runner := &stubSudoRunner{
		script: map[string]stubResponse{
			key("-n", "-iu", "target", "--", "/usr/bin/true"):                          {nil, nil},
			key("-n", "-iu", "target", "--", "sudo", "-n", "/usr/bin/true"):            {nil, &exec.ExitError{}},
			key("-n", "-iu", "target", "--", "test", "-r", "/tmp/wd", "-a", "-w", "/tmp/wd"): {nil, nil},
		},
	}
	// Generate 75 lines; cap is 3.
	var sb strings.Builder
	for i := 0; i < 75; i++ {
		sb.WriteString("noread\t/tmp/wd/file")
		sb.WriteString(strconv.Itoa(i))
		sb.WriteByte('\n')
	}
	findArgs := []string{
		"-n", "-iu", "target", "--",
		"find", "/tmp/wd",
		"(",
		"-not", "-readable", "-printf", `noread\t%p\n`,
		",",
		"-type", "f", "-not", "-writable", "-printf", `nowrite\t%p\n`,
		",",
		"-type", "d", "-not", "-executable", "-printf", `nosearch\t%p\n`,
		")",
	}
	runner.script[key(findArgs...)] = stubResponse{out: []byte(sb.String()), err: nil}
	cfg := PreflightConfig{
		Project:    "demo",
		RunAsUser:  "target",
		WorkDir:    "/tmp/wd",
		Runner:     runner,
		ScanConfig: DescendantScanConfig{MaxReport: 3, Timeout: time.Second},
	}
	result := PreflightRunAsUser(context.Background(), cfg)
	if result.HasFatal() {
		t.Fatalf("want no fatal, got: %v", result.Fatal)
	}
	if len(result.Warnings) != 1 {
		t.Fatalf("want 1 warning, got %d", len(result.Warnings))
	}
	w := result.Warnings[0]
	if !strings.Contains(w, "... and 72 more") {
		t.Errorf("want '... and 72 more' in warning, got: %s", w)
	}
}

