//go:build linux

package platform

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

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

	llsyscall "github.com/landlock-lsm/go-landlock/landlock/syscall"
	"github.com/safedep/dry/utils"
	"github.com/safedep/pmg/sandbox"
)

func newTestPolicy() *sandbox.SandboxPolicy {
	return &sandbox.SandboxPolicy{
		Name:            "test-policy",
		PackageManagers: []string{"npm"},
	}
}

func findRule(rules []landlockPathRule, path string) *landlockPathRule {
	for i := range rules {
		if rules[i].Path == path {
			return &rules[i]
		}
	}
	return nil
}

func findDenyPath(entries []denyPathEntry, path string) *denyPathEntry {
	for i := range entries {
		if entries[i].Path == path {
			return &entries[i]
		}
	}
	return nil
}

func TestLandlockTranslatePolicy_AllowRead(t *testing.T) {
	tests := []struct {
		name      string
		paths     []string
		wantPaths []string
	}{
		{
			name:      "single path",
			paths:     []string{"/usr/lib"},
			wantPaths: []string{"/usr/lib"},
		},
		{
			name:      "multiple paths",
			paths:     []string{"/usr/lib", "/etc"},
			wantPaths: []string{"/usr/lib", "/etc"},
		},
	}

	abi := newLandlockABI(3)
	expectedAccess := uint64(llsyscall.AccessFSReadFile | llsyscall.AccessFSReadDir | llsyscall.AccessFSExecute)

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			policy := newTestPolicy()
			policy.Filesystem.AllowRead = tt.paths

			ep, err := landlockTranslatePolicy(policy, abi)
			if err != nil {
				t.Fatalf("unexpected error: %v", err)
			}

			for _, wantPath := range tt.wantPaths {
				rule := findRule(ep.FilesystemRules, wantPath)
				if rule == nil {
					t.Errorf("expected rule for path %s, not found", wantPath)
					continue
				}
				if rule.Access != expectedAccess {
					t.Errorf("path %s: access = %x, want %x", wantPath, rule.Access, expectedAccess)
				}
			}
		})
	}
}

func TestLandlockTranslatePolicy_AllowWrite(t *testing.T) {
	tests := []struct {
		name       string
		abiVersion int
		wantRefer  bool
		wantTrunc  bool
	}{
		{
			name:       "V1 - no Refer, no Truncate",
			abiVersion: 1,
			wantRefer:  false,
			wantTrunc:  false,
		},
		{
			name:       "V2 - has Refer, no Truncate",
			abiVersion: 2,
			wantRefer:  true,
			wantTrunc:  false,
		},
		{
			name:       "V3 - has Refer and Truncate",
			abiVersion: 3,
			wantRefer:  true,
			wantTrunc:  true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			abi := newLandlockABI(tt.abiVersion)
			policy := newTestPolicy()
			policy.Filesystem.AllowWrite = []string{"/tmp/test"}

			ep, err := landlockTranslatePolicy(policy, abi)
			if err != nil {
				t.Fatalf("unexpected error: %v", err)
			}

			rule := findRule(ep.FilesystemRules, "/tmp/test")
			if rule == nil {
				t.Fatal("expected rule for /tmp/test, not found")
			}

			hasRefer := rule.Access&uint64(llsyscall.AccessFSRefer) != 0
			hasTrunc := rule.Access&uint64(llsyscall.AccessFSTruncate) != 0

			if hasRefer != tt.wantRefer {
				t.Errorf("Refer flag: got %v, want %v", hasRefer, tt.wantRefer)
			}
			if hasTrunc != tt.wantTrunc {
				t.Errorf("Truncate flag: got %v, want %v", hasTrunc, tt.wantTrunc)
			}

			// Verify base write flags are always present
			if rule.Access&uint64(llsyscall.AccessFSWriteFile) == 0 {
				t.Error("WriteFile flag should always be present")
			}
			if rule.Access&uint64(llsyscall.AccessFSMakeReg) == 0 {
				t.Error("MakeReg flag should always be present")
			}
		})
	}
}

func TestLandlockTranslatePolicy_AllowExec(t *testing.T) {
	policy := newTestPolicy()
	policy.Process.AllowExec = []string{"/usr/bin/node", "/usr/bin/npm"}
	abi := newLandlockABI(3)

	ep, err := landlockTranslatePolicy(policy, abi)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	// Execute access includes ReadFile because the kernel must read the
	// shebang line of script files to determine the interpreter.
	expectedAccess := uint64(llsyscall.AccessFSExecute | llsyscall.AccessFSReadFile)

	for _, path := range []string{"/usr/bin/node", "/usr/bin/npm"} {
		rule := findRule(ep.FilesystemRules, path)
		if rule == nil {
			t.Errorf("expected rule for %s, not found", path)
			continue
		}
		if rule.Access != expectedAccess {
			t.Errorf("path %s: access = %x, want %x", path, rule.Access, expectedAccess)
		}
	}
}

func TestLandlockTranslatePolicy_DenyRead(t *testing.T) {
	policy := newTestPolicy()
	policy.Filesystem.DenyRead = []string{"/etc/shadow", "/etc/passwd"}
	abi := newLandlockABI(3)

	ep, err := landlockTranslatePolicy(policy, abi)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	for _, path := range []string{"/etc/shadow", "/etc/passwd"} {
		entry := findDenyPath(ep.DenyPaths, path)
		if entry == nil {
			t.Errorf("expected deny entry for %s, not found", path)
			continue
		}
		if entry.Mode != denyRead {
			t.Errorf("path %s: mode = %d, want denyRead (%d)", path, entry.Mode, denyRead)
		}
	}
}

func TestLandlockTranslatePolicy_DenyWrite(t *testing.T) {
	policy := newTestPolicy()
	// DenyWrite is only effective within writable areas. Add /etc as writable
	// so the deny rule for /etc/hosts is not pruned as redundant.
	policy.Filesystem.AllowWrite = []string{"/etc/**"}
	policy.Filesystem.DenyWrite = []string{"/etc/hosts"}
	abi := newLandlockABI(3)

	ep, err := landlockTranslatePolicy(policy, abi)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	entry := findDenyPath(ep.DenyPaths, "/etc/hosts")
	if entry == nil {
		t.Fatal("expected deny entry for /etc/hosts, not found")
	}
	if entry.Mode != denyWrite {
		t.Errorf("mode = %d, want denyWrite (%d)", entry.Mode, denyWrite)
	}
}

func TestLandlockTranslatePolicy_DenyWriteSkippedWhenNotWritable(t *testing.T) {
	policy := newTestPolicy()
	// No AllowWrite for /etc, so deny_write for /etc/hosts is redundant
	// (Landlock already prevents writes).
	policy.Filesystem.DenyWrite = []string{"/etc/hosts"}
	abi := newLandlockABI(3)

	ep, err := landlockTranslatePolicy(policy, abi)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	entry := findDenyPath(ep.DenyPaths, "/etc/hosts")
	if entry != nil {
		t.Error("expected deny entry for /etc/hosts to be pruned (not in writable area)")
	}
}

func TestLandlockTranslatePolicy_DenyExec(t *testing.T) {
	policy := newTestPolicy()
	policy.Process.DenyExec = []string{"/usr/bin/curl", "/usr/bin/wget"}
	abi := newLandlockABI(3)

	ep, err := landlockTranslatePolicy(policy, abi)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	for _, path := range []string{"/usr/bin/curl", "/usr/bin/wget"} {
		found := false
		for _, p := range ep.DenyExecPaths {
			if p == path {
				found = true
				break
			}
		}
		if !found {
			t.Errorf("expected %s in DenyExecPaths, not found", path)
		}
	}
}

func TestLandlockTranslatePolicy_MandatoryDenies(t *testing.T) {
	policy := newTestPolicy()
	policy.Filesystem.AllowRead = []string{"/usr"}
	abi := newLandlockABI(3)

	ep, err := landlockTranslatePolicy(policy, abi)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	// Mandatory denies should always be present with denyBoth mode
	if len(ep.DenyPaths) == 0 {
		t.Fatal("expected mandatory deny paths, got none")
	}

	// Check that at least some mandatory denies have denyBoth mode
	hasDenyBoth := false
	for _, entry := range ep.DenyPaths {
		if entry.Mode == denyBoth {
			hasDenyBoth = true
			break
		}
	}
	if !hasDenyBoth {
		t.Error("expected at least one mandatory deny with denyBoth mode")
	}

	// Check that .env is in the mandatory denies (it should always be there)
	hasEnvDeny := false
	for _, entry := range ep.DenyPaths {
		if strings.HasSuffix(entry.Path, "/.env") && entry.Mode == denyBoth {
			hasEnvDeny = true
			break
		}
	}
	if !hasEnvDeny {
		t.Error("expected .env in mandatory deny paths with denyBoth mode")
	}
}

func TestLandlockTranslatePolicy_AllowReadSuppression(t *testing.T) {
	cwd, err := os.Getwd()
	require.NoError(t, err)
	cwdEnv := filepath.Clean(filepath.Join(cwd, ".env"))

	policy := newTestPolicy()
	policy.Filesystem.AllowRead = []string{cwdEnv}
	abi := newLandlockABI(3)

	ep, err := landlockTranslatePolicy(policy, abi)
	require.NoError(t, err)

	envEntry := findDenyPath(ep.DenyPaths, cwdEnv)
	require.NotNil(t, envEntry)
	assert.Equal(t, denyWrite, envEntry.Mode)
}

func TestLandlockTranslatePolicy_ImplicitRules(t *testing.T) {
	policy := newTestPolicy()
	policy.Filesystem.AllowRead = []string{"/usr"}
	abi := newLandlockABI(3)

	ep, err := landlockTranslatePolicy(policy, abi)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	readAccess := uint64(llsyscall.AccessFSReadFile | llsyscall.AccessFSReadDir | llsyscall.AccessFSExecute)

	// /proc must be present with read access
	procRule := findRule(ep.FilesystemRules, "/proc")
	if procRule == nil {
		t.Error("expected implicit /proc rule")
	} else if procRule.Access != readAccess {
		t.Errorf("/proc access = %x, want read access %x", procRule.Access, readAccess)
	}

	// /dev/null, /dev/zero, /dev/random, /dev/urandom must be present with read+write
	for _, dev := range []string{"/dev/null", "/dev/zero", "/dev/random", "/dev/urandom"} {
		rule := findRule(ep.FilesystemRules, dev)
		if rule == nil {
			t.Errorf("expected implicit rule for %s", dev)
			continue
		}
		// Should have both read and write access
		if rule.Access&readAccess != readAccess {
			t.Errorf("%s: missing read access flags", dev)
		}
		if rule.Access&uint64(llsyscall.AccessFSWriteFile) == 0 {
			t.Errorf("%s: missing write access flags", dev)
		}
	}

	// os.TempDir() must be present with write access
	tmpDir := os.TempDir()
	tmpRule := findRule(ep.FilesystemRules, tmpDir)
	if tmpRule == nil {
		t.Errorf("expected implicit rule for %s", tmpDir)
	} else if tmpRule.Access&uint64(llsyscall.AccessFSWriteFile) == 0 {
		t.Errorf("%s: missing write access flags", tmpDir)
	}
}

func TestLandlockTranslatePolicy_AllowPTY_True(t *testing.T) {
	policy := newTestPolicy()
	policy.AllowPTY = utils.PtrTo(true)
	policy.Filesystem.AllowRead = []string{"/usr"}
	abi := newLandlockABI(5) // V5 has IoctlDev

	ep, err := landlockTranslatePolicy(policy, abi)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	if !ep.AllowPTY {
		t.Error("expected AllowPTY to be true")
	}

	// Check /dev/pts
	ptsRule := findRule(ep.FilesystemRules, "/dev/pts")
	if ptsRule == nil {
		t.Fatal("expected rule for /dev/pts")
	}
	if ptsRule.Access&uint64(llsyscall.AccessFSReadFile) == 0 {
		t.Error("/dev/pts: missing read access")
	}
	if ptsRule.Access&uint64(llsyscall.AccessFSWriteFile) == 0 {
		t.Error("/dev/pts: missing write access")
	}
	// V5+ should have IoctlDev
	if ptsRule.Access&uint64(llsyscall.AccessFSIoctlDev) == 0 {
		t.Error("/dev/pts: missing IoctlDev access on V5+")
	}

	// Check /dev/ptmx
	ptmxRule := findRule(ep.FilesystemRules, "/dev/ptmx")
	if ptmxRule == nil {
		t.Fatal("expected rule for /dev/ptmx")
	}
	if ptmxRule.Access&uint64(llsyscall.AccessFSIoctlDev) == 0 {
		t.Error("/dev/ptmx: missing IoctlDev access on V5+")
	}
}

func TestLandlockTranslatePolicy_AllowPTY_Nil(t *testing.T) {
	policy := newTestPolicy()
	policy.AllowPTY = nil // nil means false
	policy.Filesystem.AllowRead = []string{"/usr"}
	abi := newLandlockABI(3)

	ep, err := landlockTranslatePolicy(policy, abi)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	if ep.AllowPTY {
		t.Error("expected AllowPTY to be false when nil")
	}

	// /dev/pts and /dev/ptmx should NOT be in rules
	if findRule(ep.FilesystemRules, "/dev/pts") != nil {
		t.Error("unexpected rule for /dev/pts when AllowPTY is nil")
	}
	if findRule(ep.FilesystemRules, "/dev/ptmx") != nil {
		t.Error("unexpected rule for /dev/ptmx when AllowPTY is nil")
	}
}

func TestLandlockTranslatePolicy_ProcExplicitAllow(t *testing.T) {
	policy := newTestPolicy()
	policy.Filesystem.AllowRead = []string{"/proc/cpuinfo"}
	abi := newLandlockABI(3)

	ep, err := landlockTranslatePolicy(policy, abi)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	if !ep.SkipPIDNamespace {
		t.Error("expected SkipPIDNamespace=true when /proc/cpuinfo is allowed")
	}
}

func TestLandlockTranslatePolicy_ProcSelfOnly(t *testing.T) {
	policy := newTestPolicy()
	policy.Filesystem.AllowRead = []string{"/proc/self/status", "/proc/self/fd"}
	abi := newLandlockABI(3)

	ep, err := landlockTranslatePolicy(policy, abi)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	if ep.SkipPIDNamespace {
		t.Error("expected SkipPIDNamespace=false when only /proc/self paths are allowed")
	}
}

func TestLandlockTranslatePolicy_ProcDenyDropped(t *testing.T) {
	policy := newTestPolicy()
	policy.Filesystem.DenyRead = []string{"/proc/kcore", "/etc/shadow"}
	abi := newLandlockABI(3)

	ep, err := landlockTranslatePolicy(policy, abi)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	// /proc/kcore should be dropped
	for _, entry := range ep.DenyPaths {
		if strings.HasPrefix(entry.Path, "/proc") {
			t.Errorf("expected /proc deny entries to be dropped, found: %s", entry.Path)
		}
	}

	// /etc/shadow should remain
	found := false
	for _, entry := range ep.DenyPaths {
		if entry.Path == "/etc/shadow" {
			found = true
			break
		}
	}
	if !found {
		t.Error("expected /etc/shadow deny entry to remain")
	}
}

func TestLandlockTranslatePolicy_AllowGitConfig_Nil(t *testing.T) {
	policy := newTestPolicy()
	policy.AllowGitConfig = nil // nil means false -> .git/config should be in mandatory denies
	policy.Filesystem.AllowRead = []string{"/usr"}
	abi := newLandlockABI(3)

	ep, err := landlockTranslatePolicy(policy, abi)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	// .git/config should be in deny paths
	hasGitConfigDeny := false
	for _, entry := range ep.DenyPaths {
		if strings.HasSuffix(entry.Path, ".git/config") {
			hasGitConfigDeny = true
			break
		}
	}
	if !hasGitConfigDeny {
		t.Error("expected .git/config in mandatory deny paths when AllowGitConfig is nil")
	}
}

func TestLandlockTranslatePolicy_AllowGitConfig_True(t *testing.T) {
	policy := newTestPolicy()
	policy.AllowGitConfig = utils.PtrTo(true)
	policy.Filesystem.AllowRead = []string{"/usr"}
	abi := newLandlockABI(3)

	ep, err := landlockTranslatePolicy(policy, abi)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	// .git/config should NOT be in deny paths
	for _, entry := range ep.DenyPaths {
		if strings.HasSuffix(entry.Path, ".git/config") {
			t.Error("expected .git/config to NOT be in mandatory deny paths when AllowGitConfig is true")
			break
		}
	}
}

func TestLandlockPolicyExplicitlyAllowsProc(t *testing.T) {
	tests := []struct {
		name      string
		readPaths []string
		writePaths []string
		want      bool
	}{
		{
			name:      "no proc paths",
			readPaths: []string{"/usr/lib", "/etc"},
			want:      false,
		},
		{
			name:      "/proc/self only",
			readPaths: []string{"/proc/self"},
			want:      false,
		},
		{
			name:      "/proc/self/status",
			readPaths: []string{"/proc/self/status"},
			want:      false,
		},
		{
			name:      "/proc/cpuinfo triggers",
			readPaths: []string{"/proc/cpuinfo"},
			want:      true,
		},
		{
			name:      "/proc/1/status triggers",
			readPaths: []string{"/proc/1/status"},
			want:      true,
		},
		{
			name:       "/proc in write paths triggers",
			writePaths: []string{"/proc/sys/kernel/randomize_va_space"},
			want:       true,
		},
		{
			name:      "/proc alone triggers",
			readPaths: []string{"/proc"},
			want:      true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			policy := newTestPolicy()
			policy.Filesystem.AllowRead = tt.readPaths
			policy.Filesystem.AllowWrite = tt.writePaths

			got := landlockPolicyExplicitlyAllowsProc(policy)
			if got != tt.want {
				t.Errorf("landlockPolicyExplicitlyAllowsProc() = %v, want %v", got, tt.want)
			}
		})
	}
}
