//go:build darwin
// +build darwin

package platform

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

	"github.com/safedep/dry/utils"
	"github.com/safedep/pmg/sandbox"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestSeatbeltTranslatorDarwinCommonTranslation(t *testing.T) {
	policy := &sandbox.SandboxPolicy{
		Name:            "test",
		Description:     "test",
		PackageManagers: []string{"npm"},
		Filesystem: sandbox.FilesystemPolicy{
			AllowRead:  []string{"/tmp"},
			AllowWrite: []string{"/tmp"},
			DenyRead:   []string{"/private/var"},
			DenyWrite:  []string{"/private/var"},
		},
		Network: sandbox.NetworkPolicy{
			AllowOutbound: []string{"*:*"},
		},
		Process: sandbox.ProcessPolicy{
			AllowExec: []string{"/bin/sh"},
			DenyExec:  []string{"/bin/bash"},
		},
	}

	translator := newSeatbeltPolicyTranslator()
	actual, err := translator.translate(policy, nil)
	assert.NoError(t, err)

	// Test common translation
	assert.Contains(t, actual, "(version 1)")
	assert.Contains(t, actual, fmt.Sprintf(";; PMG Sandbox Policy: %s", policy.Name))
	assert.Contains(t, actual, fmt.Sprintf(";; %s", policy.Description))
	assert.Contains(t, actual, ";; Generated by PMG sandbox system")

	// Test deny default - it should contain a message tag
	assert.Contains(t, actual, "(deny default (with message")

	// Allow reading /dev and /etc
	assert.Contains(t, actual, "(allow file-read* (subpath \"/dev\"))")
	assert.Contains(t, actual, "(allow file-read* (subpath \"/etc\"))")

	// Allow process fork
	assert.Contains(t, actual, "(allow process-fork)")
}

func TestSeatbeltTranslatorDarwinAlwaysAllowsFSEventsMachLookup(t *testing.T) {
	policy := &sandbox.SandboxPolicy{}

	translator := newSeatbeltPolicyTranslator()
	actual, err := translator.translate(policy, nil)
	require.NoError(t, err)

	assert.Contains(t, actual, `(global-name "com.apple.FSEvents")`)
}

func TestSeatbeltTranslatorDarwinFilesystemTranslation(t *testing.T) {
	cases := []struct {
		name   string
		policy *sandbox.SandboxPolicy
		assert func(t *testing.T, actual string, err error)
	}{
		{
			name: "simple path",
			policy: &sandbox.SandboxPolicy{
				Filesystem: sandbox.FilesystemPolicy{
					AllowRead:  []string{"/tmp"},
					AllowWrite: []string{"/tmp"},
					DenyRead:   []string{"/private/var"},
					DenyWrite:  []string{"/private/var"},
				},
			},
			assert: func(t *testing.T, actual string, err error) {
				assert.NoError(t, err)
				assert.Contains(t, actual, "(allow file-read* (subpath \"/tmp\"))")
				assert.Contains(t, actual, "(allow file-write* (subpath \"/tmp\"))")
				assert.Contains(t, actual, "(deny file-read* (subpath \"/private/var\") (with message")
				assert.Contains(t, actual, "(deny file-write* (subpath \"/private/var\") (with message")
			},
		},
		{
			name: "glob pattern with /**",
			policy: &sandbox.SandboxPolicy{
				Filesystem: sandbox.FilesystemPolicy{
					AllowRead: []string{"/path/to/dir/**"},
				},
			},
			assert: func(t *testing.T, actual string, err error) {
				assert.NoError(t, err)
				// Should generate literal rule for parent directory
				assert.Contains(t, actual, `(allow file-read* (literal "/path/to/dir"))`)
				// Should use regex matching for glob patterns
				assert.Contains(t, actual, "(allow file-read* (regex #")
				assert.Contains(t, actual, "^/path/to/dir/.*$")
			},
		},
		{
			name: "glob pattern with /** and glob parent",
			policy: &sandbox.SandboxPolicy{
				Filesystem: sandbox.FilesystemPolicy{
					AllowWrite: []string{"/project/**/node_modules/**"},
				},
			},
			assert: func(t *testing.T, actual string, err error) {
				assert.NoError(t, err)
				// Parent still contains a glob, so the auto-allow must be a
				// regex rule — a literal with ** can never match a real path.
				assert.Contains(t, actual, `(allow file-write* (regex #"^/project/(.*/)?node_modules$"))`)
				assert.Contains(t, actual, `^/project/(.*/)?node_modules/.*$`)
				assert.NotContains(t, actual, `(literal "/project/**/node_modules")`)
			},
		},
		{
			name: "glob pattern with *.txt",
			policy: &sandbox.SandboxPolicy{
				Filesystem: sandbox.FilesystemPolicy{
					AllowRead: []string{"/path/to/*.txt"},
				},
			},
			assert: func(t *testing.T, actual string, err error) {
				assert.NoError(t, err)
				// Should use regex matching for glob patterns
				assert.Contains(t, actual, "(allow file-read* (regex #")
				assert.Contains(t, actual, `^/path/to/[^/]*\.txt$`)
			},
		},
		{
			name: "glob pattern with ? wildcard",
			policy: &sandbox.SandboxPolicy{
				Filesystem: sandbox.FilesystemPolicy{
					AllowWrite: []string{"/path/to/file?.log"},
				},
			},
			assert: func(t *testing.T, actual string, err error) {
				assert.NoError(t, err)
				// Should use regex matching for glob patterns
				assert.Contains(t, actual, "(allow file-write* (regex #")
				assert.Contains(t, actual, `^/path/to/file[^/]\.log$`)
			},
		},
		{
			name: "glob pattern in middle of path",
			policy: &sandbox.SandboxPolicy{
				Filesystem: sandbox.FilesystemPolicy{
					DenyRead: []string{"/path/*/subdir"},
				},
			},
			assert: func(t *testing.T, actual string, err error) {
				assert.NoError(t, err)
				// Should use regex matching for glob patterns
				assert.Contains(t, actual, "(deny file-read* (regex #")
				assert.Contains(t, actual, `^/path/[^/]*/subdir$`)
			},
		},
		{
			name: "glob pattern with bracket wildcard",
			policy: &sandbox.SandboxPolicy{
				Filesystem: sandbox.FilesystemPolicy{
					AllowRead: []string{"/tmp/test[123].txt"},
				},
			},
			assert: func(t *testing.T, actual string, err error) {
				assert.NoError(t, err)
				// Should use regex matching for glob patterns
				assert.Contains(t, actual, "(allow file-read* (regex #")
				assert.Contains(t, actual, `^/tmp/test[123]\.txt$`)
			},
		},
		{
			name: "write glob pattern ending with /** auto-allows parent dir",
			policy: &sandbox.SandboxPolicy{
				Filesystem: sandbox.FilesystemPolicy{
					AllowWrite: []string{"/path/to/dir/**"},
				},
			},
			assert: func(t *testing.T, actual string, err error) {
				assert.NoError(t, err)
				// Should generate literal rule for parent directory
				assert.Contains(t, actual, `(allow file-write* (literal "/path/to/dir"))`)
				// Should also generate regex rule for contents
				assert.Contains(t, actual, `(allow file-write* (regex #"^/path/to/dir/.*$"))`)
			},
		},
		{
			name: "write glob pattern with ** in middle not auto-allowed",
			policy: &sandbox.SandboxPolicy{
				Filesystem: sandbox.FilesystemPolicy{
					AllowWrite: []string{"/path/**/subdir"},
				},
			},
			assert: func(t *testing.T, actual string, err error) {
				assert.NoError(t, err)
				// Should only generate regex rule, no literal parent
				assert.Contains(t, actual, `(allow file-write* (regex #`)
				assert.NotContains(t, actual, `(allow file-write* (literal "/path")`)
			},
		},
		{
			name: "write glob pattern with variable expansion and /**",
			policy: &sandbox.SandboxPolicy{
				Filesystem: sandbox.FilesystemPolicy{
					AllowWrite: []string{"${HOME}/.npm/**"},
				},
			},
			assert: func(t *testing.T, actual string, err error) {
				assert.NoError(t, err)
				homeDir := os.Getenv("HOME")
				expectedLiteral := fmt.Sprintf(`(allow file-write* (literal "%s/.npm"))`, homeDir)
				// Check for the literal parent directory rule
				assert.Contains(t, actual, expectedLiteral)
				// Check for the regex rule (escaped dot)
				assert.Contains(t, actual, `\.npm/.*$`)
			},
		},
		{
			name: "multiple /** patterns all auto-allow parents",
			policy: &sandbox.SandboxPolicy{
				Filesystem: sandbox.FilesystemPolicy{
					AllowWrite: []string{
						"/path/one/**",
						"/path/two/**",
						"/path/three/**",
					},
				},
			},
			assert: func(t *testing.T, actual string, err error) {
				assert.NoError(t, err)
				assert.Contains(t, actual, `(allow file-write* (literal "/path/one"))`)
				assert.Contains(t, actual, `(allow file-write* (literal "/path/two"))`)
				assert.Contains(t, actual, `(allow file-write* (literal "/path/three"))`)
			},
		},
		{
			name: "edge case /** pattern at root",
			policy: &sandbox.SandboxPolicy{
				Filesystem: sandbox.FilesystemPolicy{
					AllowWrite: []string{"/**"},
				},
			},
			assert: func(t *testing.T, actual string, err error) {
				assert.NoError(t, err)
				// Should allow writing to root (empty string after trimming becomes "/")
				assert.Contains(t, actual, `(allow file-write* (literal "/"))`)
				assert.Contains(t, actual, `(allow file-write* (regex #"^/.*$"))`)
			},
		},
		{
			name: "dangerous file paths are blocked by default",
			policy: &sandbox.SandboxPolicy{
				Filesystem: sandbox.FilesystemPolicy{
					DenyRead:  []string{"/tmp/does-not-exist"},
					DenyWrite: []string{"/tmp/does-not-exist"},
				},
			},
			assert: func(t *testing.T, actual string, err error) {
				assert.NoError(t, err)

				homeDir, err := os.UserHomeDir()
				assert.NoError(t, err)

				assert.Contains(t, actual, fmt.Sprintf("(deny file-read* (subpath \"%s/.ssh\") (with message", homeDir))
				assert.Contains(t, actual, fmt.Sprintf("(deny file-write* (subpath \"%s/.ssh\") (with message", homeDir))
				assert.Contains(t, actual, fmt.Sprintf("(deny file-read* (subpath \"%s/.kube\") (with message", homeDir))
				assert.Contains(t, actual, fmt.Sprintf("(deny file-write* (subpath \"%s/.kube\") (with message", homeDir))
			},
		},
	}

	for _, tt := range cases {
		t.Run(tt.name, func(t *testing.T) {
			translator := newSeatbeltPolicyTranslator()
			actual, err := translator.translate(tt.policy, nil)
			tt.assert(t, actual, err)
		})
	}
}

func TestSeatbeltTranslatorDarwinProcessTranslation(t *testing.T) {
	cases := []struct {
		name   string
		policy *sandbox.SandboxPolicy
		assert func(t *testing.T, actual string, err error)
	}{
		{
			name: "literal exec path",
			policy: &sandbox.SandboxPolicy{
				Process: sandbox.ProcessPolicy{
					AllowExec: []string{"/bin/sh"},
					DenyExec:  []string{"/bin/bash"},
				},
			},
			assert: func(t *testing.T, actual string, err error) {
				assert.NoError(t, err)
				assert.Contains(t, actual, "(allow process-exec* (literal \"/bin/sh\"))")
				assert.Contains(t, actual, "(deny process-exec* (literal \"/bin/bash\") (with message")
			},
		},
		{
			name: "glob pattern with /** for exec",
			policy: &sandbox.SandboxPolicy{
				Process: sandbox.ProcessPolicy{
					AllowExec: []string{"/usr/local/bin/**"},
				},
			},
			assert: func(t *testing.T, actual string, err error) {
				assert.NoError(t, err)
				// Should use regex matching for glob patterns
				assert.Contains(t, actual, "(allow process-exec* (regex #")
				assert.Contains(t, actual, "^/usr/local/bin/.*$")
			},
		},
		{
			name: "glob pattern with * wildcard for exec",
			policy: &sandbox.SandboxPolicy{
				Process: sandbox.ProcessPolicy{
					AllowExec: []string{"/usr/bin/python*"},
				},
			},
			assert: func(t *testing.T, actual string, err error) {
				assert.NoError(t, err)
				// Should use regex matching for glob patterns
				assert.Contains(t, actual, "(allow process-exec* (regex #")
				assert.Contains(t, actual, `^/usr/bin/python[^/]*$`)
			},
		},
	}

	for _, tt := range cases {
		t.Run(tt.name, func(t *testing.T) {
			translator := newSeatbeltPolicyTranslator()
			actual, err := translator.translate(tt.policy, nil)
			tt.assert(t, actual, err)
		})
	}
}

func TestExtractBaseDir(t *testing.T) {
	cases := []struct {
		name     string
		pattern  string
		expected string
	}{
		{
			name:     "no glob pattern",
			pattern:  "/path/to/file",
			expected: "/path/to/file",
		},
		{
			name:     "glob pattern with /**",
			pattern:  "/path/to/**",
			expected: "/path/to",
		},
		{
			name:     "glob pattern with *.txt",
			pattern:  "/path/to/*.txt",
			expected: "/path/to",
		},
		{
			name:     "glob pattern with ? wildcard",
			pattern:  "/path/to/file?.log",
			expected: "/path/to",
		},
		{
			name:     "glob pattern in middle of path",
			pattern:  "/path/*/subdir",
			expected: "/path",
		},
		{
			name:     "glob pattern with bracket wildcard",
			pattern:  "/tmp/test[123].txt",
			expected: "/tmp",
		},
		{
			name:     "glob pattern at root",
			pattern:  "/*.txt",
			expected: "/",
		},
		{
			name:     "multiple glob patterns",
			pattern:  "/path/*/sub/*.txt",
			expected: "/path",
		},
		{
			name:     "complex glob with multiple wildcards",
			pattern:  "/usr/bin/python*",
			expected: "/usr/bin",
		},
	}

	for _, tt := range cases {
		t.Run(tt.name, func(t *testing.T) {
			actual := extractBaseDir(tt.pattern)
			assert.Equal(t, tt.expected, actual)
		})
	}
}

func TestGetAncestorDirectories(t *testing.T) {
	cases := []struct {
		name     string
		path     string
		expected []string
	}{
		{
			name:     "deep path",
			path:     "/private/tmp/test/file.txt",
			expected: []string{"/private/tmp/test", "/private/tmp", "/private"},
		},
		{
			name:     "two level path",
			path:     "/tmp/file.txt",
			expected: []string{"/tmp"},
		},
		{
			name:     "root level path",
			path:     "/file.txt",
			expected: []string{},
		},
		{
			name:     "directory path",
			path:     "/usr/local/bin",
			expected: []string{"/usr/local", "/usr"},
		},
		{
			name:     "path with trailing slash",
			path:     "/path/to/dir/",
			expected: []string{"/path/to/dir", "/path/to", "/path"},
		},
	}

	for _, tt := range cases {
		t.Run(tt.name, func(t *testing.T) {
			actual := getAncestorDirectories(tt.path)
			assert.Equal(t, tt.expected, actual)
		})
	}
}

func TestGenerateMoveBlockingRules(t *testing.T) {
	cases := []struct {
		name     string
		patterns []string
		logTag   string
		assert   func(t *testing.T, rules []string)
	}{
		{
			name:     "single literal path",
			patterns: []string{"/sensitive/data"},
			logTag:   "test",
			assert: func(t *testing.T, rules []string) {
				// Should block moving the path itself
				assert.Contains(t, rules, fmt.Sprintf("(deny file-write-unlink (subpath \"/sensitive/data\") (with message \"%s\"))", seatbeltLogMessage("test", "file-write-unlink", "/sensitive/data")))
				// Should block moving the parent directory
				assert.Contains(t, rules, fmt.Sprintf("(deny file-write-unlink (literal \"/sensitive\") (with message \"%s\"))", seatbeltLogMessage("test", "file-write-unlink", "/sensitive")))
			},
		},
		{
			name:     "glob pattern",
			patterns: []string{"/path/to/*.txt"},
			logTag:   "test",
			assert: func(t *testing.T, rules []string) {
				// Should block moving the base directory
				assert.Contains(t, rules, fmt.Sprintf("(deny file-write-unlink (subpath \"/path/to\") (with message \"%s\"))", seatbeltLogMessage("test", "file-write-unlink", "/path/to")))
				// Should block moving ancestor directories
				assert.Contains(t, rules, fmt.Sprintf("(deny file-write-unlink (literal \"/path\") (with message \"%s\"))", seatbeltLogMessage("test", "file-write-unlink", "/path")))
			},
		},
		{
			name:     "multiple paths",
			patterns: []string{"/tmp/test", "/var/log/app"},
			logTag:   "test",
			assert: func(t *testing.T, rules []string) {
				assert.Contains(t, rules, fmt.Sprintf("(deny file-write-unlink (subpath \"/tmp/test\") (with message \"%s\"))", seatbeltLogMessage("test", "file-write-unlink", "/tmp/test")))
				assert.Contains(t, rules, fmt.Sprintf("(deny file-write-unlink (literal \"/tmp\") (with message \"%s\"))", seatbeltLogMessage("test", "file-write-unlink", "/tmp")))
				assert.Contains(t, rules, fmt.Sprintf("(deny file-write-unlink (subpath \"/var/log/app\") (with message \"%s\"))", seatbeltLogMessage("test", "file-write-unlink", "/var/log/app")))
				assert.Contains(t, rules, fmt.Sprintf("(deny file-write-unlink (literal \"/var/log\") (with message \"%s\"))", seatbeltLogMessage("test", "file-write-unlink", "/var/log")))
				assert.Contains(t, rules, fmt.Sprintf("(deny file-write-unlink (literal \"/var\") (with message \"%s\"))", seatbeltLogMessage("test", "file-write-unlink", "/var")))
			},
		},
		{
			name:     "deep nested path",
			patterns: []string{"/a/b/c/d/e/file.txt"},
			logTag:   "test",
			assert: func(t *testing.T, rules []string) {
				// Should have rules for all ancestors
				assert.Contains(t, rules, fmt.Sprintf("(deny file-write-unlink (literal \"/a/b/c/d/e\") (with message \"%s\"))", seatbeltLogMessage("test", "file-write-unlink", "/a/b/c/d/e")))
				assert.Contains(t, rules, fmt.Sprintf("(deny file-write-unlink (literal \"/a/b/c/d\") (with message \"%s\"))", seatbeltLogMessage("test", "file-write-unlink", "/a/b/c/d")))
				assert.Contains(t, rules, fmt.Sprintf("(deny file-write-unlink (literal \"/a/b/c\") (with message \"%s\"))", seatbeltLogMessage("test", "file-write-unlink", "/a/b/c")))
				assert.Contains(t, rules, fmt.Sprintf("(deny file-write-unlink (literal \"/a/b\") (with message \"%s\"))", seatbeltLogMessage("test", "file-write-unlink", "/a/b")))
				assert.Contains(t, rules, fmt.Sprintf("(deny file-write-unlink (literal \"/a\") (with message \"%s\"))", seatbeltLogMessage("test", "file-write-unlink", "/a")))
			},
		},
		{
			name:     "root level path",
			patterns: []string{"/file"},
			logTag:   "test",
			assert: func(t *testing.T, rules []string) {
				// Should only have the file itself, no ancestors
				assert.Contains(t, rules, fmt.Sprintf("(deny file-write-unlink (subpath \"/file\") (with message \"%s\"))", seatbeltLogMessage("test", "file-write-unlink", "/file")))
				// Should not contain root as ancestor
				for _, rule := range rules {
					assert.NotContains(t, rule, "(deny file-write-unlink (literal \"/\"))")
				}
			},
		},
	}

	for _, tt := range cases {
		t.Run(tt.name, func(t *testing.T) {
			rules := generateMoveBlockingRules(tt.patterns, tt.logTag)
			tt.assert(t, rules)
		})
	}
}

func TestFilesystemTranslationWithMoveProtection(t *testing.T) {
	policy := &sandbox.SandboxPolicy{
		Name:            "test",
		Description:     "test with move protection",
		PackageManagers: []string{"npm"},
		Filesystem: sandbox.FilesystemPolicy{
			DenyRead:  []string{"/private/sensitive"},
			DenyWrite: []string{"/usr/local/bin"},
		},
	}

	translator := newSeatbeltPolicyTranslator()
	translator.enableMoveBlockingMitigation = true

	actual, err := translator.translate(policy, nil)
	assert.NoError(t, err)

	// Should contain deny read rule
	assert.Contains(t, actual, "(deny file-read* (subpath \"/private/sensitive\") (with message")

	// Should contain move protection for deny read
	assert.Contains(t, actual, ";; Prevent bypassing read restrictions via file movement")
	assert.Contains(t, actual, "(deny file-write-unlink (subpath \"/private/sensitive\") (with message")
	assert.Contains(t, actual, "(deny file-write-unlink (literal \"/private\") (with message")

	// Should contain deny write rule
	assert.Contains(t, actual, "(deny file-write* (subpath \"/usr/local/bin\") (with message")

	// Should contain move protection for deny write
	assert.Contains(t, actual, ";; Prevent bypassing write restrictions via file movement")
	assert.Contains(t, actual, "(deny file-write-unlink (subpath \"/usr/local/bin\") (with message")
	assert.Contains(t, actual, "(deny file-write-unlink (literal \"/usr/local\") (with message")
	assert.Contains(t, actual, "(deny file-write-unlink (literal \"/usr\") (with message")
}

func TestPTYSupport(t *testing.T) {
	t.Run("PTY disabled by default", func(t *testing.T) {
		policy := &sandbox.SandboxPolicy{
			Name:            "test",
			Description:     "test without PTY",
			PackageManagers: []string{"npm"},
			AllowPTY:        utils.PtrTo(false),
		}

		translator := newSeatbeltPolicyTranslator()
		actual, err := translator.translate(policy, nil)
		assert.NoError(t, err)

		// Should NOT contain PTY rules
		assert.NotContains(t, actual, "(allow pseudo-tty)")
		assert.NotContains(t, actual, "/dev/ptmx")
	})

	t.Run("PTY enabled when requested", func(t *testing.T) {
		policy := &sandbox.SandboxPolicy{
			Name:            "test",
			Description:     "test with PTY",
			PackageManagers: []string{"npm"},
			AllowPTY:        utils.PtrTo(true),
		}

		translator := newSeatbeltPolicyTranslator()
		actual, err := translator.translate(policy, nil)
		assert.NoError(t, err)

		// Should contain PTY rules
		assert.Contains(t, actual, ";; Pseudo-terminal (PTY) support")
		assert.Contains(t, actual, "(allow pseudo-tty)")
		assert.Contains(t, actual, "(allow file-ioctl")
		assert.Contains(t, actual, "(literal \"/dev/ptmx\")")
		assert.Contains(t, actual, "(regex #\"^/dev/ttys\")")
		assert.Contains(t, actual, "(allow file-read* file-write*")
	})
}

func TestNetworkBindSupport(t *testing.T) {
	t.Run("AllowNetworkBind true with no AllowBind generates localhost-only bind rule", func(t *testing.T) {
		policy := &sandbox.SandboxPolicy{
			Name:             "test",
			Description:      "test with network bind",
			PackageManagers:  []string{"npm"},
			AllowNetworkBind: utils.PtrTo(true),
		}

		translator := newSeatbeltPolicyTranslator()
		actual, err := translator.translate(policy, nil)
		assert.NoError(t, err)

		assert.Contains(t, actual, ";; Local network bind (localhost only)")
		assert.Contains(t, actual, `(allow network-bind (local ip "localhost:*"))`)
		assert.Contains(t, actual, `(allow network* (local ip "localhost:*"))`)
		// Should not contain any explicit bind addresses
		assert.NotContains(t, actual, `(allow network-bind (local ip "0.0.0.0:*"))`)
	})

	t.Run("AllowNetworkBind true with AllowBind generates both rules", func(t *testing.T) {
		policy := &sandbox.SandboxPolicy{
			Name:             "test",
			Description:      "test with network bind and explicit addresses",
			PackageManagers:  []string{"npm"},
			AllowNetworkBind: utils.PtrTo(true),
			Network: sandbox.NetworkPolicy{
				AllowBind: []string{"0.0.0.0:*"},
			},
		}

		translator := newSeatbeltPolicyTranslator()
		actual, err := translator.translate(policy, nil)
		assert.NoError(t, err)

		// Localhost bind from AllowNetworkBind
		assert.Contains(t, actual, `(allow network-bind (local ip "localhost:*"))`)
		// Explicit bind address
		assert.Contains(t, actual, `(allow network-bind (local ip "0.0.0.0:*"))`)
	})

	t.Run("AllowNetworkBind false with AllowBind generates only explicit bind rule", func(t *testing.T) {
		policy := &sandbox.SandboxPolicy{
			Name:             "test",
			Description:      "test with explicit bind only",
			PackageManagers:  []string{"npm"},
			AllowNetworkBind: utils.PtrTo(false),
			Network: sandbox.NetworkPolicy{
				AllowBind: []string{"0.0.0.0:*"},
			},
		}

		translator := newSeatbeltPolicyTranslator()
		actual, err := translator.translate(policy, nil)
		assert.NoError(t, err)

		// Should NOT contain localhost bind
		assert.NotContains(t, actual, ";; Local network bind (localhost only)")
		assert.NotContains(t, actual, `(allow network-bind (local ip "localhost:*"))`)
		// Should contain explicit bind address
		assert.Contains(t, actual, `(allow network-bind (local ip "0.0.0.0:*"))`)
	})

	t.Run("AllowNetworkBind nil with no AllowBind generates no bind rules", func(t *testing.T) {
		policy := &sandbox.SandboxPolicy{
			Name:            "test",
			Description:     "test without network bind",
			PackageManagers: []string{"npm"},
		}

		translator := newSeatbeltPolicyTranslator()
		actual, err := translator.translate(policy, nil)
		assert.NoError(t, err)

		assert.NotContains(t, actual, "network-bind")
	})
}

func TestSeatbeltTranslatorDarwinLogTag(t *testing.T) {
	translator := newSeatbeltPolicyTranslator()
	assert.NotEmpty(t, translator.LogTag())
	assert.Contains(t, seatbeltLogMessage(translator.LogTag(), "file-read", "/tmp/test"), "run="+translator.LogTag())

	translator = &seatbeltPolicyTranslator{logTag: "test"}
	assert.Equal(t, "test", translator.LogTag())
}

func TestSeatbeltMandatoryDenySuppression(t *testing.T) {
	cwd, err := os.Getwd()
	require.NoError(t, err)

	t.Run("allow_read with literal CWD .env removes file-read* deny but keeps file-write* deny", func(t *testing.T) {
		policy := &sandbox.SandboxPolicy{
			Name: "test",
			Filesystem: sandbox.FilesystemPolicy{
				AllowRead: []string{filepath.Join(cwd, ".env")},
			},
		}
		out := translateFilesystemForTest(t, policy)

		assert.NotContains(t, out, fmt.Sprintf(`(deny file-read* (subpath "%s")`, filepath.Join(cwd, ".env")))
		assert.Contains(t, out, fmt.Sprintf(`(deny file-write* (subpath "%s")`, filepath.Join(cwd, ".env")))
	})

	t.Run("allow_write with literal CWD .env removes file-write* deny but keeps file-read* deny", func(t *testing.T) {
		policy := &sandbox.SandboxPolicy{
			Name: "test",
			Filesystem: sandbox.FilesystemPolicy{
				AllowWrite: []string{filepath.Join(cwd, ".env")},
			},
		}
		out := translateFilesystemForTest(t, policy)

		assert.NotContains(t, out, fmt.Sprintf(`(deny file-write* (subpath "%s")`, filepath.Join(cwd, ".env")))
		assert.Contains(t, out, fmt.Sprintf(`(deny file-read* (subpath "%s")`, filepath.Join(cwd, ".env")))
	})

	t.Run("broad glob does NOT remove mandatory denies", func(t *testing.T) {
		policy := &sandbox.SandboxPolicy{
			Name: "test",
			Filesystem: sandbox.FilesystemPolicy{
				AllowRead: []string{filepath.Join(cwd, "**")},
			},
		}
		out := translateFilesystemForTest(t, policy)

		home, err := os.UserHomeDir()
		require.NoError(t, err)

		assert.Contains(t, out, fmt.Sprintf(`(deny file-read* (subpath "%s")`, filepath.Join(cwd, ".env")))
		assert.Contains(t, out, fmt.Sprintf(`(deny file-read* (subpath "%s")`, filepath.Join(home, ".env")))
		assert.Contains(t, out, fmt.Sprintf(`(deny file-read* (subpath "%s")`, filepath.Join(cwd, ".ssh")))
	})
}

func translateFilesystemForTest(t *testing.T, policy *sandbox.SandboxPolicy) string {
	t.Helper()
	tr := newSeatbeltPolicyTranslator()
	var sb strings.Builder
	require.NoError(t, tr.translateFilesystem(policy, &sb))
	return sb.String()
}

func TestTranslateNetworkLockdown(t *testing.T) {
	rt := &sandbox.ExecutionContext{ProxyAddr: "127.0.0.1:54321"}
	basePolicy := func() *sandbox.SandboxPolicy {
		return &sandbox.SandboxPolicy{
			Name:            "lockdown-translate",
			PackageManagers: []string{"golang"},
			Filesystem: sandbox.FilesystemPolicy{
				AllowRead:  []string{"/tmp"},
				AllowWrite: []string{"/tmp"},
			},
			Network: sandbox.NetworkPolicy{
				AllowOutbound: []string{"proxy.golang.org:443"},
			},
			NetworkViaProxyOnly: utils.PtrTo(true),
		}
	}

	denyMarker := `|kind=network-outbound|target=direct"))`
	proxyAllow := `(allow network-outbound (remote ip "localhost:54321"))`
	blanketAllow := "(allow network-outbound)\n"
	dnsAllow := `(allow network-outbound (remote unix-socket (path-literal "/var/run/mDNSResponder")))`
	dnsAllowPrivate := `(allow network-outbound (remote unix-socket (path-literal "/private/var/run/mDNSResponder")))`
	dnsMachAllow := `(allow mach-lookup (global-name "com.apple.dnssd.service"))`
	dnsPortAllow := `(allow network-outbound (remote ip "*:53"))`
	bindRule := `(allow network* (local ip "localhost:*"))`
	loopbackOutboundAllow := `(allow network-outbound (remote ip "localhost:*"))`

	tests := []struct {
		name   string
		rt     *sandbox.ExecutionContext
		mutate func(*sandbox.SandboxPolicy)
		assert func(t *testing.T, out string)
	}{
		{
			name:   "lockdown base",
			rt:     rt,
			mutate: func(p *sandbox.SandboxPolicy) {},
			assert: func(t *testing.T, out string) {
				assert.Contains(t, out, denyMarker)
				assert.Contains(t, out, proxyAllow)
				assert.NotContains(t, out, "mDNSResponder")
				assert.NotContains(t, out, blanketAllow)
			},
		},
		{
			name:   "allow_direct_dns reopens mDNSResponder and port 53",
			rt:     rt,
			mutate: func(p *sandbox.SandboxPolicy) { p.AllowDirectDNS = utils.PtrTo(true) },
			assert: func(t *testing.T, out string) {
				assert.Contains(t, out, dnsAllow)
				assert.Contains(t, out, dnsAllowPrivate)
				assert.Contains(t, out, dnsMachAllow)
				assert.Contains(t, out, dnsPortAllow)
			},
		},
		{
			name:   "allow_network_bind rules come after the lockdown deny",
			rt:     rt,
			mutate: func(p *sandbox.SandboxPolicy) { p.AllowNetworkBind = utils.PtrTo(true) },
			assert: func(t *testing.T, out string) {
				assert.Contains(t, out, denyMarker)
				assert.Contains(t, out, bindRule)
				assert.Contains(t, out, loopbackOutboundAllow)
				denyIdx := strings.Index(out, denyMarker)
				require.GreaterOrEqual(t, denyIdx, 0)
				assert.Greater(t, strings.Index(out, bindRule), denyIdx, "bind rules must come after the lockdown deny (SBPL last-match-wins)")
				assert.Greater(t, strings.Index(out, loopbackOutboundAllow), denyIdx, "loopback outbound allow must come after the lockdown deny")
			},
		},
		{
			name:   "render without proxy context stays deny-only and documents the runtime allow",
			rt:     nil,
			mutate: func(p *sandbox.SandboxPolicy) {},
			assert: func(t *testing.T, out string) {
				assert.Contains(t, out, denyMarker)
				assert.Contains(t, out, "Rendered without a running PMG proxy")
				// The runtime allow is documented in an SBPL comment; assert no
				// actual (non-comment) allow rule is emitted.
				assert.NotContains(t, out, "\n(allow network-outbound (remote ip")
			},
		},
		{
			name:   "lockdown off keeps blanket allow",
			rt:     rt,
			mutate: func(p *sandbox.SandboxPolicy) { p.NetworkViaProxyOnly = nil },
			assert: func(t *testing.T, out string) {
				assert.Contains(t, out, blanketAllow)
				assert.NotContains(t, out, denyMarker)
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			policy := basePolicy()
			tt.mutate(policy)

			translator := newSeatbeltPolicyTranslator()
			out, err := translator.translate(policy, tt.rt)
			require.NoError(t, err)

			tt.assert(t, out)
		})
	}
}
