//go:build linux

package sandbox

import (
	"os/exec"
	"strings"
	"syscall"
	"testing"
)

func TestLandlockBackend_Name(t *testing.T) {
	if (&LandlockBackend{}).Name() != "landlock" {
		t.Error("Name should return \"landlock\"")
	}
}

func TestLandlockBackend_WrapMulti_NoPaths(t *testing.T) {
	sb := &LandlockBackend{selfExe: "/proc/self/exe"}
	cmd := exec.Command("git", "status")

	wrapped, err := sb.WrapMulti(nil, cmd)
	if err != nil {
		t.Fatalf("WrapMulti: %v", err)
	}
	if wrapped != cmd {
		t.Error("empty paths should return the original cmd unchanged")
	}
}

func TestLandlockBackend_WrapMulti_ArgsConstruction(t *testing.T) {
	sb := &LandlockBackend{selfExe: "/path/to/knot"}
	cmd := exec.Command("git", "upload-pack", "--stateless-rpc", ".")
	cmd.Env = []string{"GIT_PROTOCOL=version=2"}
	cmd.Stdin = strings.NewReader("input")

	wrapped, err := sb.WrapMulti([]string{"/repos/a", "/repos/b"}, cmd)
	if err != nil {
		t.Fatalf("WrapMulti: %v", err)
	}

	// argv[0] is the selfExe
	if wrapped.Path != "/path/to/knot" {
		t.Errorf("Path = %q, want %q", wrapped.Path, "/path/to/knot")
	}

	// argv should be: [knot, sandbox-exec, --repo-path=/repos/a, --repo-path=/repos/b, --, <abs-git>, upload-pack, ...]
	args := wrapped.Args
	if len(args) < 6 {
		t.Fatalf("Args too short: %v", args)
	}
	if args[0] != "/path/to/knot" {
		t.Errorf("Args[0] = %q, want %q", args[0], "/path/to/knot")
	}
	if args[1] != "sandbox-exec" {
		t.Errorf("Args[1] = %q, want %q", args[1], "sandbox-exec")
	}
	if args[2] != "--repo-path=/repos/a" {
		t.Errorf("Args[2] = %q, want --repo-path=/repos/a", args[2])
	}
	if args[3] != "--repo-path=/repos/b" {
		t.Errorf("Args[3] = %q, want --repo-path=/repos/b", args[3])
	}
	if args[4] != "--" {
		t.Errorf("Args[4] = %q, want --", args[4])
	}
	// args[5] is the resolved absolute git path; just check it ends with /git
	if !strings.HasSuffix(args[5], "/git") && args[5] != "git" {
		t.Errorf("Args[5] = %q, want path ending in /git or bare \"git\"", args[5])
	}
	if got := args[len(args)-1]; got != "." {
		t.Errorf("last arg = %q, want %q", got, ".")
	}

	// Dir should be the first repo path so the kernel chdirs there
	// after setuid, before execve.
	if wrapped.Dir != "/repos/a" {
		t.Errorf("Dir = %q, want %q", wrapped.Dir, "/repos/a")
	}

	// Env propagated
	if len(wrapped.Env) != 1 || wrapped.Env[0] != "GIT_PROTOCOL=version=2" {
		t.Errorf("Env = %v, want [GIT_PROTOCOL=version=2]", wrapped.Env)
	}

	// Stdio propagated
	if wrapped.Stdin != cmd.Stdin {
		t.Error("Stdin not propagated to wrapped cmd")
	}
}

func TestLandlockBackend_WrapMulti_NoLookupNoCredential(t *testing.T) {
	sb := &LandlockBackend{selfExe: "/path/to/knot"} // no lookup
	cmd := exec.Command("git", "status")

	wrapped, err := sb.WrapMulti([]string{"/repos/a"}, cmd)
	if err != nil {
		t.Fatalf("WrapMulti: %v", err)
	}
	if wrapped.SysProcAttr != nil && wrapped.SysProcAttr.Credential != nil {
		t.Error("no lookup configured; Credential should not be set")
	}
}

func TestLandlockBackend_WrapMulti_LookupSetsCredential(t *testing.T) {
	// lookup deliberately returns a different gid (the service group) to
	// confirm that WrapMulti ignores it and uses uid as the primary gid.
	// see the comment in sandbox_linux.go for why this matters.
	sb := &LandlockBackend{
		selfExe: "/path/to/knot",
		lookup: func(repoPath string) (uint32, uint32, error) {
			if repoPath != "/repos/a" {
				t.Errorf("lookup called with %q, want /repos/a", repoPath)
			}
			return 100042, 1234, nil
		},
	}
	cmd := exec.Command("git", "status")

	wrapped, err := sb.WrapMulti([]string{"/repos/a"}, cmd)
	if err != nil {
		t.Fatalf("WrapMulti: %v", err)
	}
	if wrapped.SysProcAttr == nil || wrapped.SysProcAttr.Credential == nil {
		t.Fatal("Credential should be set when lookup returns uid > 0")
	}
	cred := wrapped.SysProcAttr.Credential
	if cred.Uid != 100042 {
		t.Errorf("Credential.Uid = %d, want 100042", cred.Uid)
	}
	if cred.Gid != 100042 {
		t.Errorf("Credential.Gid = %d, want 100042 (must equal Uid, not lookup's gid 1234)", cred.Gid)
	}
	if !cred.NoSetGroups {
		t.Error("NoSetGroups should be true")
	}
}

func TestLandlockBackend_WrapMulti_LookupErrSkipsCredential(t *testing.T) {
	sb := &LandlockBackend{
		selfExe: "/path/to/knot",
		lookup: func(string) (uint32, uint32, error) {
			return 0, 0, syscall.ENOENT
		},
	}
	cmd := exec.Command("git", "status")

	wrapped, err := sb.WrapMulti([]string{"/repos/a"}, cmd)
	if err != nil {
		t.Fatalf("WrapMulti: %v", err)
	}
	if wrapped.SysProcAttr != nil && wrapped.SysProcAttr.Credential != nil {
		t.Error("lookup errored; Credential should not be set")
	}
}

func TestLandlockBackend_WrapMulti_LookupZeroSkipsCredential(t *testing.T) {
	// uid == 0 is treated as "don't drop" so we never accidentally drop to
	// root. Verify Credential isn't set in that case.
	sb := &LandlockBackend{
		selfExe: "/path/to/knot",
		lookup: func(string) (uint32, uint32, error) {
			return 0, 0, nil
		},
	}
	cmd := exec.Command("git", "status")

	wrapped, err := sb.WrapMulti([]string{"/repos/a"}, cmd)
	if err != nil {
		t.Fatalf("WrapMulti: %v", err)
	}
	if wrapped.SysProcAttr != nil && wrapped.SysProcAttr.Credential != nil {
		t.Error("lookup returned uid=0; Credential should not be set")
	}
}
