//go:build linux
// +build linux

package platform

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

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

func TestBubblewrapTranslatorBasicTranslation(t *testing.T) {
	policy := &sandbox.SandboxPolicy{
		Name:            "test",
		Description:     "test policy",
		PackageManagers: []string{"npm"},
		Filesystem: sandbox.FilesystemPolicy{
			AllowRead:  []string{"/tmp"},
			AllowWrite: []string{"/tmp"},
		},
		Network: sandbox.NetworkPolicy{
			AllowOutbound: []string{"registry.npmjs.org:443"},
		},
	}

	config := newDefaultBubblewrapConfig()
	translator := newBubblewrapPolicyTranslator(config)
	args, err := translator.translate(policy)
	require.NoError(t, err)
	require.NotEmpty(t, args)

	// Convert to string for easier assertion
	argsStr := argSliceToString(args)

	// Essential system paths should be mounted read-only
	assert.Contains(t, argsStr, "--ro-bind-try")
	assert.Contains(t, argsStr, "/usr")
	assert.Contains(t, argsStr, "/lib")

	// Essential devices should be mounted
	assert.Contains(t, argsStr, "--dev-bind-try")
	assert.Contains(t, argsStr, "/dev/null")

	// Proc filesystem should be mounted
	assert.Contains(t, argsStr, "--proc")
	assert.Contains(t, argsStr, "/proc")

	// User-specified paths should be mounted
	assert.Contains(t, argsStr, "/tmp")

	// Network should be allowed (no --unshare-net)
	assert.NotContains(t, argsStr, "--unshare-net")

	// Process isolation should be enabled
	assert.Contains(t, argsStr, "--unshare-pid")
	assert.Contains(t, argsStr, "--unshare-ipc")

	// Die with parent
	assert.Contains(t, argsStr, "--die-with-parent")
}

func TestBubblewrapTranslatorFilesystemRules(t *testing.T) {
	cases := []struct {
		name   string
		policy *sandbox.SandboxPolicy
		assert func(t *testing.T, args []string, err error, policy *sandbox.SandboxPolicy)
	}{
		{
			name: "simple read-only path",
			policy: &sandbox.SandboxPolicy{
				Filesystem: sandbox.FilesystemPolicy{
					AllowRead: []string{"/usr/local"},
				},
			},
			assert: func(t *testing.T, args []string, err error, policy *sandbox.SandboxPolicy) {
				require.NoError(t, err)
				argsStr := argSliceToString(args)
				// Should have read-only bind for the path
				assert.Contains(t, argsStr, "--ro-bind-try")
				assert.Contains(t, argsStr, "/usr/local")
			},
		},
		{
			name: "simple read-write path",
			policy: &sandbox.SandboxPolicy{
				Filesystem: sandbox.FilesystemPolicy{
					AllowWrite: []string{"/tmp/test"},
				},
			},
			assert: func(t *testing.T, args []string, err error, policy *sandbox.SandboxPolicy) {
				require.NoError(t, err)
				argsStr := argSliceToString(args)
				// Should have read-write bind for the path
				assert.Contains(t, argsStr, "--bind-try")
				assert.Contains(t, argsStr, "/tmp/test")
			},
		},
		{
			name: "variable expansion in paths",
			policy: &sandbox.SandboxPolicy{
				Filesystem: sandbox.FilesystemPolicy{
					AllowRead:  []string{"${HOME}/.npmrc"},
					AllowWrite: []string{"${CWD}/node_modules"},
				},
			},
			assert: func(t *testing.T, args []string, err error, policy *sandbox.SandboxPolicy) {
				require.NoError(t, err)
				argsStr := argSliceToString(args)

				homeDir, err := os.UserHomeDir()
				require.NoError(t, err)
				cwd, err := os.Getwd()
				require.NoError(t, err)

				// Variables should be expanded
				assert.Contains(t, argsStr, homeDir+"/.npmrc")
				assert.Contains(t, argsStr, cwd+"/node_modules")
			},
		},
		{
			name: "deny write with read-only bind",
			policy: &sandbox.SandboxPolicy{
				Filesystem: sandbox.FilesystemPolicy{
					DenyWrite: []string{"/etc/passwd"},
				},
			},
			assert: func(t *testing.T, args []string, err error, policy *sandbox.SandboxPolicy) {
				require.NoError(t, err)

				if _, err := os.Stat("/etc/passwd"); err == nil {
					assertNoDevNullMount(t, args, "/etc/passwd")
					assertReadBind(t, args, "/etc/passwd")
				}
			},
		},
		{
			name: "deny read masks file with dev null",
			policy: func() *sandbox.SandboxPolicy {
				dir := t.TempDir()
				path := filepath.Join(dir, "secret.txt")
				require.NoError(t, os.WriteFile(path, []byte("secret"), 0o600))
				return &sandbox.SandboxPolicy{
					Filesystem: sandbox.FilesystemPolicy{
						DenyRead: []string{path},
					},
				}
			}(),
			assert: func(t *testing.T, args []string, err error, policy *sandbox.SandboxPolicy) {
				require.NoError(t, err)
				assertDevNullMount(t, args, policy.Filesystem.DenyRead[0])
			},
		},
		{
			name: "deny read hides directory with tmpfs",
			policy: func() *sandbox.SandboxPolicy {
				dir := t.TempDir()
				require.NoError(t, os.WriteFile(filepath.Join(dir, "secret.txt"), []byte("secret"), 0o600))
				return &sandbox.SandboxPolicy{
					Filesystem: sandbox.FilesystemPolicy{
						DenyRead: []string{dir},
					},
				}
			}(),
			assert: func(t *testing.T, args []string, err error, policy *sandbox.SandboxPolicy) {
				require.NoError(t, err)
				assertTmpfsAt(t, args, policy.Filesystem.DenyRead[0])
			},
		},
		{
			name: "deny read globstar uses parent directory approximation",
			policy: func() *sandbox.SandboxPolicy {
				dir := t.TempDir()
				require.NoError(t, os.Mkdir(filepath.Join(dir, "nested"), 0o700))
				require.NoError(t, os.WriteFile(filepath.Join(dir, "nested", "secret.txt"), []byte("secret"), 0o600))
				return &sandbox.SandboxPolicy{
					Filesystem: sandbox.FilesystemPolicy{
						DenyRead: []string{filepath.Join(dir, "**")},
					},
				}
			}(),
			assert: func(t *testing.T, args []string, err error, policy *sandbox.SandboxPolicy) {
				require.NoError(t, err)
				parentDir := filepath.Dir(policy.Filesystem.DenyRead[0])
				assertTmpfsAt(t, args, parentDir)
				assertNoDevNullMount(t, args, filepath.Join(parentDir, "nested", "secret.txt"))
			},
		},
		{
			name: "multiple paths",
			policy: &sandbox.SandboxPolicy{
				Filesystem: sandbox.FilesystemPolicy{
					AllowRead: []string{
						"/usr/bin",
						"/usr/lib",
						"/var/log",
					},
					AllowWrite: []string{
						"/tmp/output",
						"/var/tmp/cache",
					},
				},
			},
			assert: func(t *testing.T, args []string, err error, policy *sandbox.SandboxPolicy) {
				require.NoError(t, err)
				argsStr := argSliceToString(args)

				// All read paths should be present
				assert.Contains(t, argsStr, "/usr/bin")
				assert.Contains(t, argsStr, "/usr/lib")
				assert.Contains(t, argsStr, "/var/log")

				// All write paths should be present
				assert.Contains(t, argsStr, "/tmp/output")
				assert.Contains(t, argsStr, "/var/tmp/cache")
			},
		},
	}

	for _, tt := range cases {
		t.Run(tt.name, func(t *testing.T) {
			config := newDefaultBubblewrapConfig()
			translator := newBubblewrapPolicyTranslator(config)
			args, err := translator.translate(tt.policy)
			tt.assert(t, args, err, tt.policy)
		})
	}
}

func TestBubblewrapTranslatorGlobPatterns(t *testing.T) {
	// Create a temporary directory structure for testing glob expansion
	tmpDir := t.TempDir()

	// Create test files
	require.NoError(t, os.MkdirAll(filepath.Join(tmpDir, "subdir1"), 0755))
	require.NoError(t, os.MkdirAll(filepath.Join(tmpDir, "subdir2"), 0755))
	require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "file1.txt"), []byte("test"), 0644))
	require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "file2.log"), []byte("test"), 0644))

	cases := []struct {
		name   string
		policy *sandbox.SandboxPolicy
		assert func(t *testing.T, args []string, err error)
	}{
		{
			name: "glob pattern with *",
			policy: &sandbox.SandboxPolicy{
				Filesystem: sandbox.FilesystemPolicy{
					AllowRead: []string{tmpDir + "/*.txt"},
				},
			},
			assert: func(t *testing.T, args []string, err error) {
				require.NoError(t, err)
				argsStr := argSliceToString(args)

				// Should expand to concrete file
				assert.Contains(t, argsStr, "file1.txt")
				// Should NOT match .log files
				assert.NotContains(t, argsStr, "file2.log")
			},
		},
		{
			name: "glob pattern with ** (recursive)",
			policy: &sandbox.SandboxPolicy{
				Filesystem: sandbox.FilesystemPolicy{
					AllowWrite: []string{tmpDir + "/**"},
				},
			},
			assert: func(t *testing.T, args []string, err error) {
				require.NoError(t, err)
				assertWriteBind(t, args, tmpDir)
				assertNoWriteBind(t, args, filepath.Join(tmpDir, "subdir1"))
				assertNoWriteBind(t, args, filepath.Join(tmpDir, "subdir2"))
			},
		},
		{
			name: "non-existent glob pattern",
			policy: &sandbox.SandboxPolicy{
				Filesystem: sandbox.FilesystemPolicy{
					AllowRead: []string{"/nonexistent/path/**"},
				},
			},
			assert: func(t *testing.T, args []string, err error) {
				require.NoError(t, err)
				argsStr := argSliceToString(args)

				// Should still include the base path (even if doesn't exist)
				assert.Contains(t, argsStr, "/nonexistent/path")
			},
		},
	}

	for _, tt := range cases {
		t.Run(tt.name, func(t *testing.T) {
			config := newDefaultBubblewrapConfig()
			translator := newBubblewrapPolicyTranslator(config)
			args, err := translator.translate(tt.policy)
			tt.assert(t, args, err)
		})
	}
}

func TestBubblewrapTranslatorNetworkIsolation(t *testing.T) {
	cases := []struct {
		name   string
		policy *sandbox.SandboxPolicy
		assert func(t *testing.T, args []string, err error)
	}{
		{
			name: "network allowed with allow rules",
			policy: &sandbox.SandboxPolicy{
				Network: sandbox.NetworkPolicy{
					AllowOutbound: []string{"registry.npmjs.org:443"},
				},
			},
			assert: func(t *testing.T, args []string, err error) {
				require.NoError(t, err)
				argsStr := argSliceToString(args)

				// Should NOT have --unshare-net (network allowed)
				assert.NotContains(t, argsStr, "--unshare-net")
			},
		},
		{
			name: "network isolated with deny all",
			policy: &sandbox.SandboxPolicy{
				Network: sandbox.NetworkPolicy{
					DenyOutbound: []string{"*:*"},
				},
			},
			assert: func(t *testing.T, args []string, err error) {
				require.NoError(t, err)
				argsStr := argSliceToString(args)

				// Should have --unshare-net (network denied)
				assert.Contains(t, argsStr, "--unshare-net")
			},
		},
		{
			name: "network isolated by default when no rules",
			policy: &sandbox.SandboxPolicy{
				Network: sandbox.NetworkPolicy{},
			},
			assert: func(t *testing.T, args []string, err error) {
				require.NoError(t, err)
				argsStr := argSliceToString(args)

				// With default config (unshareNetworkByDefault: true), should isolate
				assert.Contains(t, argsStr, "--unshare-net")
			},
		},
		{
			name: "network allowed when config disables default isolation",
			policy: &sandbox.SandboxPolicy{
				Network: sandbox.NetworkPolicy{},
			},
			assert: func(t *testing.T, args []string, err error) {
				require.NoError(t, err)
				// This test needs a custom config, so we can't assert here
				// Just verify no error
			},
		},
	}

	for _, tt := range cases {
		t.Run(tt.name, func(t *testing.T) {
			config := newDefaultBubblewrapConfig()
			translator := newBubblewrapPolicyTranslator(config)
			args, err := translator.translate(tt.policy)
			tt.assert(t, args, err)
		})
	}
}

func TestBubblewrapTranslatorPTYSupport(t *testing.T) {
	t.Run("PTY disabled by default", func(t *testing.T) {
		policy := &sandbox.SandboxPolicy{
			AllowPTY: utils.PtrTo(false),
		}

		config := newDefaultBubblewrapConfig()
		translator := newBubblewrapPolicyTranslator(config)
		args, err := translator.translate(policy)
		require.NoError(t, err)

		argsStr := argSliceToString(args)

		// Should NOT have PTY device bindings
		assert.NotContains(t, argsStr, "/dev/pts")
		assert.NotContains(t, argsStr, "/dev/ptmx")
	})

	t.Run("PTY enabled when requested", func(t *testing.T) {
		policy := &sandbox.SandboxPolicy{
			AllowPTY: utils.PtrTo(true),
		}

		config := newDefaultBubblewrapConfig()
		translator := newBubblewrapPolicyTranslator(config)
		args, err := translator.translate(policy)
		require.NoError(t, err)

		argsStr := argSliceToString(args)

		// Should have PTY device bindings
		assert.Contains(t, argsStr, "/dev/pts")
		assert.Contains(t, argsStr, "/dev/ptmx")
		assert.Contains(t, argsStr, "--dev-bind-try")
	})
}

func TestBubblewrapTranslatorMandatoryDenies(t *testing.T) {
	// Create temp directory with some dangerous files
	tmpDir := t.TempDir()
	sshDir := filepath.Join(tmpDir, ".ssh")
	require.NoError(t, os.MkdirAll(sshDir, 0700))
	require.NoError(t, os.WriteFile(filepath.Join(sshDir, "id_rsa"), []byte("fake key"), 0600))

	policy := &sandbox.SandboxPolicy{
		Filesystem: sandbox.FilesystemPolicy{
			// Even with broad write permissions...
			AllowWrite: []string{tmpDir + "/**"},
		},
	}

	config := newDefaultBubblewrapConfig()
	translator := newBubblewrapPolicyTranslator(config)
	args, err := translator.translate(policy)
	require.NoError(t, err)

	argsStr := argSliceToString(args)

	// Mandatory deny patterns should be present
	// Note: The actual paths depend on the current working directory and home
	// We just verify that /dev/null mounting is used
	assert.Contains(t, argsStr, "--ro-bind")
	assert.Contains(t, argsStr, "/dev/null")
}

func TestBubblewrapTranslatorGitConfigDeny(t *testing.T) {
	t.Run("git config denied by default", func(t *testing.T) {
		policy := &sandbox.SandboxPolicy{
			AllowGitConfig: utils.PtrTo(false),
		}

		config := newDefaultBubblewrapConfig()
		translator := newBubblewrapPolicyTranslator(config)
		_, err := translator.translate(policy)
		require.NoError(t, err)

		// Git config should be in deny patterns
		// (we can't easily assert the exact args without creating a .git directory)
	})

	t.Run("git config allowed when explicitly set", func(t *testing.T) {
		policy := &sandbox.SandboxPolicy{
			AllowGitConfig: utils.PtrTo(true),
		}

		config := newDefaultBubblewrapConfig()
		translator := newBubblewrapPolicyTranslator(config)
		_, err := translator.translate(policy)
		require.NoError(t, err)

		// Should succeed without adding git config to deny patterns
	})
}

func TestBubblewrapConfigDefaults(t *testing.T) {
	config := newDefaultBubblewrapConfig()

	// Essential system paths
	assert.NotEmpty(t, config.essentialSystemPaths)
	assert.Contains(t, config.essentialSystemPaths, "/usr")
	assert.Contains(t, config.essentialSystemPaths, "/lib")

	// Essential devices
	assert.NotEmpty(t, config.essentialDevices)
	assert.Contains(t, config.essentialDevices, "/dev/null")
	assert.Contains(t, config.essentialDevices, "/dev/random")

	// Glob limits
	assert.Equal(t, 5, config.maxGlobDepth)
	assert.Equal(t, 10000, config.maxGlobPaths)

	// Isolation settings
	assert.True(t, config.unshareNetworkByDefault)
	assert.True(t, config.unsharePID)
	assert.True(t, config.unshareIPC)
	assert.True(t, config.dieWithParent)
}

func TestBubblewrapConfigEssentialPaths(t *testing.T) {
	config := newDefaultBubblewrapConfig()

	// Get essential system paths (filters out non-existent)
	paths := config.getEssentialSystemPaths()
	assert.NotEmpty(t, paths)

	// All returned paths should exist
	for _, path := range paths {
		_, err := os.Stat(path)
		assert.NoError(t, err, "Essential path %s should exist", path)
	}
}

func TestBubblewrapConfigEssentialDevices(t *testing.T) {
	config := newDefaultBubblewrapConfig()

	// Get essential devices (filters out non-existent)
	devices := config.getEssentialDevices()
	assert.NotEmpty(t, devices)

	// All returned devices should exist
	for _, device := range devices {
		_, err := os.Stat(device)
		assert.NoError(t, err, "Essential device %s should exist", device)
	}
}

func TestBubblewrapTranslatorProcessDenyWriteRule(t *testing.T) {
	t.Run("existing file is mounted read-only", func(t *testing.T) {
		tmpDir := t.TempDir()

		// Create a test file that exists
		testFile := filepath.Join(tmpDir, "existing.txt")
		require.NoError(t, os.WriteFile(testFile, []byte("test"), 0644))

		policy := &sandbox.SandboxPolicy{
			Filesystem: sandbox.FilesystemPolicy{
				DenyWrite: []string{testFile},
			},
		}

		config := newDefaultBubblewrapConfig()
		translator := newBubblewrapPolicyTranslator(config)
		args, err := translator.translate(policy)
		require.NoError(t, err)

		assertNoDevNullMount(t, args, testFile)
		assertReadBind(t, args, testFile)
	})

	t.Run("non-existent file is skipped to avoid creating empty files", func(t *testing.T) {
		tmpDir := t.TempDir()

		// Non-existent file - should be skipped because bwrap requires a real
		// mount target for file-level read-only bind overrides.
		nonExistentPath := filepath.Join(tmpDir, ".env")

		policy := &sandbox.SandboxPolicy{
			Filesystem: sandbox.FilesystemPolicy{
				AllowWrite: []string{tmpDir}, // Allow writes to tmpDir
				DenyWrite:  []string{nonExistentPath},
			},
		}

		config := newDefaultBubblewrapConfig()
		translator := newBubblewrapPolicyTranslator(config)
		args, err := translator.translate(policy)
		require.NoError(t, err)

		argsStr := argSliceToString(args)

		// Non-existent file should NOT be in args
		// Using --ro-bind /dev/null on non-existent paths creates empty files
		assert.NotContains(t, argsStr, nonExistentPath)
	})

	t.Run("existing directory is mounted read-only", func(t *testing.T) {
		tmpDir := t.TempDir()

		// Create a directory
		testDir := filepath.Join(tmpDir, "secrets")
		require.NoError(t, os.MkdirAll(testDir, 0755))

		policy := &sandbox.SandboxPolicy{
			Filesystem: sandbox.FilesystemPolicy{
				DenyWrite: []string{testDir},
			},
		}

		config := newDefaultBubblewrapConfig()
		translator := newBubblewrapPolicyTranslator(config)
		args, err := translator.translate(policy)
		require.NoError(t, err)

		argsStr := argSliceToString(args)

		// Existing directory should be mounted read-only
		assert.Contains(t, argsStr, "--ro-bind-try")
		assert.Contains(t, argsStr, testDir)
	})
}

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

	config := newDefaultBubblewrapConfig()
	translator := newBubblewrapPolicyTranslator(config)
	args, err := translator.translate(policy)
	require.NoError(t, err)

	argsStr := argSliceToString(args)

	// Tmpdir should be mounted as writable
	tmpDir := os.TempDir()
	assert.Contains(t, argsStr, "--bind")
	assert.Contains(t, argsStr, tmpDir)
}

func TestExpandGlobstarPattern(t *testing.T) {
	tmpDir := t.TempDir()

	// Create a directory structure
	require.NoError(t, os.MkdirAll(filepath.Join(tmpDir, "dir1", "subdir"), 0755))
	require.NoError(t, os.MkdirAll(filepath.Join(tmpDir, "dir2"), 0755))
	require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "file.txt"), []byte("test"), 0644))
	require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "dir1", "file2.txt"), []byte("test"), 0644))

	config := newDefaultBubblewrapConfig()
	translator := newBubblewrapPolicyTranslator(config)

	cases := []struct {
		name     string
		pattern  string
		maxDepth int
		maxPaths int
		assert   func(t *testing.T, matches []string, err error)
	}{
		{
			name:     "simple globstar",
			pattern:  tmpDir + "/**",
			maxDepth: 3,
			maxPaths: 100,
			assert: func(t *testing.T, matches []string, err error) {
				require.NoError(t, err)
				assert.NotEmpty(t, matches)
				// Should include base directory
				assert.Contains(t, matches, tmpDir)
			},
		},
		{
			name:     "globstar with depth limit",
			pattern:  tmpDir + "/**",
			maxDepth: 1,
			maxPaths: 100,
			assert: func(t *testing.T, matches []string, err error) {
				require.NoError(t, err)
				// Should be limited by depth
				for _, match := range matches {
					rel, err := filepath.Rel(tmpDir, match)
					require.NoError(t, err)
					depth := len(filepath.SplitList(rel))
					assert.LessOrEqual(t, depth, 2) // Base + 1 level
				}
			},
		},
		{
			name:     "globstar with count limit",
			pattern:  tmpDir + "/**",
			maxDepth: 10,
			maxPaths: 2,
			assert: func(t *testing.T, matches []string, err error) {
				require.NoError(t, err)
				// Should be limited by count
				assert.LessOrEqual(t, len(matches), 2)
			},
		},
		{
			name:     "non-existent base path",
			pattern:  "/nonexistent/path/**",
			maxDepth: 3,
			maxPaths: 100,
			assert: func(t *testing.T, matches []string, err error) {
				require.NoError(t, err)
				// Should return the base path even if it doesn't exist
				assert.Contains(t, matches, "/nonexistent/path")
			},
		},
	}

	for _, tt := range cases {
		t.Run(tt.name, func(t *testing.T) {
			matches, err := translator.expandGlobstarPattern(tt.pattern, tt.maxDepth, tt.maxPaths)
			tt.assert(t, matches, err)
		})
	}
}

// TestGlobFallbackThreshold verifies coarse-grained fallback behavior when patterns match too many paths
func TestGlobFallbackThreshold(t *testing.T) {
	tmpDir := t.TempDir()

	// Create 150 files (exceeds threshold of 100)
	for i := 0; i < 150; i++ {
		filePath := filepath.Join(tmpDir, fmt.Sprintf("file%d.txt", i))
		require.NoError(t, os.WriteFile(filePath, []byte("test"), 0644))
	}

	config := newDefaultBubblewrapConfig()
	translator := newBubblewrapPolicyTranslator(config)

	policy := &sandbox.SandboxPolicy{
		Name: "test-fallback",
		Filesystem: sandbox.FilesystemPolicy{
			AllowRead: []string{tmpDir + "/*.txt"},
		},
	}

	args, err := translator.translate(policy)
	require.NoError(t, err)

	argsStr := argSliceToString(args)

	// Should bind parent directory (tmpDir), not individual files
	assert.Contains(t, argsStr, tmpDir)

	// Should NOT contain individual file paths (fallback to parent dir)
	assert.NotContains(t, argsStr, "file1.txt")
	assert.NotContains(t, argsStr, "file50.txt")
	assert.NotContains(t, argsStr, "file100.txt")

	// Verify total argument count is reasonable (coarse-grained fallback should prevent explosion)
	assert.Less(t, len(args), 300, "Coarse-grained fallback should prevent argument explosion")
}

// TestGlobFallbackThresholdGlobstar tests fallback with ** globstar patterns
func TestGlobFallbackThresholdGlobstar(t *testing.T) {
	tmpDir := t.TempDir()

	// Create deep directory structure with 200 files (exceeds threshold)
	for i := 0; i < 10; i++ {
		subDir := filepath.Join(tmpDir, fmt.Sprintf("dir%d", i))
		require.NoError(t, os.MkdirAll(subDir, 0755))
		for j := 0; j < 20; j++ {
			filePath := filepath.Join(subDir, fmt.Sprintf("file%d.txt", j))
			require.NoError(t, os.WriteFile(filePath, []byte("test"), 0644))
		}
	}

	config := newDefaultBubblewrapConfig()
	translator := newBubblewrapPolicyTranslator(config)

	policy := &sandbox.SandboxPolicy{
		Name: "test-fallback-globstar",
		Filesystem: sandbox.FilesystemPolicy{
			AllowWrite: []string{tmpDir + "/**"},
		},
	}

	args, err := translator.translate(policy)
	require.NoError(t, err)

	argsStr := argSliceToString(args)

	// Should bind parent directory (tmpDir)
	assert.Contains(t, argsStr, tmpDir)

	// Should NOT contain individual subdirectory paths (fallback to parent)
	assert.NotContains(t, argsStr, "dir1")
	assert.NotContains(t, argsStr, "dir5")

	// Verify total argument count is reasonable
	assert.Less(t, len(args), 300, "Coarse-grained fallback should prevent argument explosion")
}

// TestBubblewrapAllowWriteGlobstarBindsParentOnly verifies globstar allow_write rules
// bind the parent tree (e.g. .venv) instead of per-file mounts that break pip in-project venvs.
// See https://github.com/safedep/pmg/issues/315
func TestBubblewrapAllowWriteGlobstarBindsParentOnly(t *testing.T) {
	tmpDir := t.TempDir()
	venvDir := filepath.Join(tmpDir, ".venv")
	binDir := filepath.Join(venvDir, "bin")
	require.NoError(t, os.MkdirAll(binDir, 0755))
	pythonPath := filepath.Join(binDir, "python")
	require.NoError(t, os.Symlink("/usr/bin/python3", pythonPath))

	// Populate enough shallow files that old logic would fine-grain bind without hitting fallback.
	for i := 0; i < 30; i++ {
		path := filepath.Join(venvDir, fmt.Sprintf("file%d.txt", i))
		require.NoError(t, os.WriteFile(path, []byte("x"), 0644))
	}

	config := newDefaultBubblewrapConfig()
	translator := newBubblewrapPolicyTranslator(config)

	policy := &sandbox.SandboxPolicy{
		Name: "test-venv-write",
		Filesystem: sandbox.FilesystemPolicy{
			AllowRead:  []string{tmpDir + "/**"},
			AllowWrite: []string{venvDir + "/**"},
		},
	}

	args, err := translator.translate(policy)
	require.NoError(t, err)

	assertWriteBind(t, args, venvDir)
	assertNoWriteBind(t, args, pythonPath)
	assertReadBind(t, args, pythonPath)
}

// TestGlobNoFallbackSmallPattern tests that small patterns don't trigger fallback
func TestGlobNoFallbackSmallPattern(t *testing.T) {
	tmpDir := t.TempDir()

	// Create only 10 files (below threshold)
	for i := 0; i < 10; i++ {
		filePath := filepath.Join(tmpDir, fmt.Sprintf("file%d.txt", i))
		require.NoError(t, os.WriteFile(filePath, []byte("test"), 0644))
	}

	config := newDefaultBubblewrapConfig()
	translator := newBubblewrapPolicyTranslator(config)

	policy := &sandbox.SandboxPolicy{
		Name: "test-no-fallback",
		Filesystem: sandbox.FilesystemPolicy{
			AllowRead: []string{tmpDir + "/*.txt"},
		},
	}

	args, err := translator.translate(policy)
	require.NoError(t, err)

	argsStr := argSliceToString(args)

	// Should bind individual files (no fallback)
	assert.Contains(t, argsStr, "file0.txt")
	assert.Contains(t, argsStr, "file5.txt")
}

// TestTotalArgsLimit verifies global argument limit warning
func TestTotalArgsLimit(t *testing.T) {
	// Create policy with many patterns that would exceed limit
	policy := &sandbox.SandboxPolicy{
		Name: "test-args-limit",
		Filesystem: sandbox.FilesystemPolicy{
			AllowRead: make([]string, 1000), // 1000 patterns
		},
	}

	// Fill with literal paths to avoid glob expansion
	for i := 0; i < 1000; i++ {
		policy.Filesystem.AllowRead[i] = fmt.Sprintf("/tmp/path%d", i)
	}

	config := newDefaultBubblewrapConfig()
	config.totalArgsLimit = 500 // Set low for testing
	translator := newBubblewrapPolicyTranslator(config)

	args, err := translator.translate(policy)
	require.NoError(t, err) // Should not error, just warn

	// Verify args were generated despite exceeding limit
	assert.Greater(t, len(args), config.totalArgsLimit)
}

// TestExtractParentDir tests the extractParentDir helper function
func TestExtractParentDir(t *testing.T) {
	cases := []struct {
		name     string
		pattern  string
		expected string
	}{
		{
			name:     "double star pattern",
			pattern:  "/home/user/node_modules/**",
			expected: "/home/user/node_modules",
		},
		{
			name:     "single star pattern",
			pattern:  "/tmp/*.txt",
			expected: "/tmp",
		},
		{
			name:     "middle glob",
			pattern:  "/usr/lib/*.so",
			expected: "/usr/lib",
		},
		{
			name:     "complex glob",
			pattern:  "/home/user/.cache/**/*.log",
			expected: "/home/user/.cache",
		},
		{
			name:     "file-level glob with dot",
			pattern:  "/home/user/project/package.json.*",
			expected: "/home/user/project",
		},
		{
			name:     "file-level glob in CWD",
			pattern:  "/home/user/project/*.lock",
			expected: "/home/user/project",
		},
		{
			name:     "question mark glob",
			pattern:  "/tmp/file?.txt",
			expected: "/tmp",
		},
		{
			name:     "bracket glob",
			pattern:  "/usr/lib/lib[abc].so",
			expected: "/usr/lib",
		},
		{
			name:     "no glob",
			pattern:  "/home/user/file.txt",
			expected: "/home/user/file.txt",
		},
		{
			name:     "trailing double star",
			pattern:  "/home/user/cache/**",
			expected: "/home/user/cache",
		},
		{
			name:     "trailing single star",
			pattern:  "/var/log/*",
			expected: "/var/log",
		},
	}

	for _, tt := range cases {
		t.Run(tt.name, func(t *testing.T) {
			config := newDefaultBubblewrapConfig()
			translator := newBubblewrapPolicyTranslator(config)
			result := translator.extractParentDir(tt.pattern)
			assert.Equal(t, tt.expected, result)
		})
	}
}

func TestExtractGlobstarWriteBaseDir(t *testing.T) {
	cases := []struct {
		name     string
		pattern  string
		expected string
	}{
		{
			name:     "suffix globstar (profiles)",
			pattern:  "/home/user/.venv/**",
			expected: "/home/user/.venv",
		},
		{
			name:     "in-pattern globstars",
			pattern:  "/a/b/**/d/**/e",
			expected: "/a/b",
		},
		{
			name:     "middle globstar with file suffix",
			pattern:  "/usr/lib/**/*.so",
			expected: "/usr/lib",
		},
	}

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

func TestBubblewrapGlobstarWriteMultiSegmentBind(t *testing.T) {
	tmpDir := t.TempDir()
	baseDir := filepath.Join(tmpDir, "a", "b")
	require.NoError(t, os.MkdirAll(baseDir, 0755))

	pattern := filepath.Join(tmpDir, "a", "b", "**", "d", "**", "e")
	config := newDefaultBubblewrapConfig()
	translator := newBubblewrapPolicyTranslator(config)

	policy := &sandbox.SandboxPolicy{
		Name: "test-multi-globstar-write",
		Filesystem: sandbox.FilesystemPolicy{
			AllowWrite: []string{pattern},
		},
	}

	args, err := translator.translate(policy)
	require.NoError(t, err)
	assertWriteBind(t, args, baseDir)
}

// Helper function to convert arg slice to string for easier assertion
func argSliceToString(args []string) string {
	result := ""
	for _, arg := range args {
		result += arg + " "
	}

	return result
}

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

	t.Run("read-side opt-out preserves real ro-bind and skips tmpfs and /dev/null", func(t *testing.T) {
		// Real .env in an isolated CWD so processDenyRule does not skip the
		// path as non-existent.
		dir := t.TempDir()
		envPath := filepath.Join(dir, ".env")
		require.NoError(t, os.WriteFile(envPath, []byte("X=1\n"), 0o600))

		origCwd, err := os.Getwd()
		require.NoError(t, err)
		require.NoError(t, os.Chdir(dir))
		t.Cleanup(func() {
			_ = os.Chdir(origCwd)
		})

		policy := &sandbox.SandboxPolicy{
			Name: "test",
			Filesystem: sandbox.FilesystemPolicy{
				AllowRead: []string{envPath},
			},
		}
		args := translateForTest(t, policy)

		assertNoTmpfsAt(t, args, envPath)
		// /dev/null overlay would mask reads; allow_read --ro-bind already
		// denies writes via EROFS, so the mandatory write deny is redundant.
		assertNoDevNullMount(t, args, envPath)
		assertReadBind(t, args, envPath)
	})

	t.Run("user deny_write preserves read for paths also in allow_read", func(t *testing.T) {
		dir := t.TempDir()
		envPath := filepath.Join(dir, ".env")
		require.NoError(t, os.WriteFile(envPath, []byte("X=1\n"), 0o600))

		origCwd, err := os.Getwd()
		require.NoError(t, err)
		require.NoError(t, os.Chdir(dir))
		t.Cleanup(func() {
			_ = os.Chdir(origCwd)
		})

		policy := &sandbox.SandboxPolicy{
			Name: "test",
			Filesystem: sandbox.FilesystemPolicy{
				AllowRead:  []string{envPath},
				AllowWrite: []string{dir},
				DenyWrite:  []string{envPath},
			},
		}
		args := translateForTest(t, policy)

		assertNoDevNullMount(t, args, envPath)
		assertReadOnlyBindAfterWritableBind(t, args, envPath, dir)
	})

	t.Run("write-side opt-out skips both tmpfs and /dev/null for that path", func(t *testing.T) {
		policy := &sandbox.SandboxPolicy{
			Name: "test",
			Filesystem: sandbox.FilesystemPolicy{
				AllowWrite: []string{filepath.Join(cwd, ".env")},
			},
		}
		args := translateForTest(t, policy)

		assertNoTmpfsAt(t, args, filepath.Join(cwd, ".env"))
		assertNoDevNullMount(t, args, filepath.Join(cwd, ".env"))
	})

	t.Run("no opt-out: tmpfs fires for the path", func(t *testing.T) {
		// tmpfs only fires for paths that exist on the host; assert at the
		// GetMandatoryDenyPatterns level instead of the translator output.
		r := util.GetMandatoryDenyPatterns(util.MandatoryDenyOptions{})
		assert.Contains(t, r.DenyRead, filepath.Join(cwd, ".env"))
		assert.Contains(t, r.DenyWrite, filepath.Join(cwd, ".env"))
	})
}

func translateForTest(t *testing.T, policy *sandbox.SandboxPolicy) []string {
	t.Helper()
	tr := newBubblewrapPolicyTranslator(newDefaultBubblewrapConfig())
	args, err := tr.translate(policy)
	require.NoError(t, err)
	return args
}

func assertNoTmpfsAt(t *testing.T, args []string, path string) {
	t.Helper()
	for i := 0; i+1 < len(args); i++ {
		if args[i] == "--tmpfs" && args[i+1] == path {
			t.Fatalf("expected no --tmpfs at %q, but found one", path)
		}
	}
}

func assertTmpfsAt(t *testing.T, args []string, path string) {
	t.Helper()
	for i := 0; i+1 < len(args); i++ {
		if args[i] == "--tmpfs" && args[i+1] == path {
			return
		}
	}

	t.Fatalf("expected --tmpfs at %q, but none found", path)
}

func assertNoDevNullMount(t *testing.T, args []string, path string) {
	t.Helper()
	for i := 0; i+2 < len(args); i++ {
		if (args[i] == "--ro-bind" || args[i] == "--bind") && args[i+1] == "/dev/null" && args[i+2] == path {
			t.Fatalf("expected no /dev/null mount at %q, but found one", path)
		}
	}
}

func assertDevNullMount(t *testing.T, args []string, path string) {
	t.Helper()
	for i := 0; i+2 < len(args); i++ {
		if args[i] == "--ro-bind" && args[i+1] == "/dev/null" && args[i+2] == path {
			return
		}
	}

	t.Fatalf("expected /dev/null mount at %q, but none found", path)
}

func assertReadBind(t *testing.T, args []string, path string) {
	t.Helper()
	for i := 0; i+2 < len(args); i++ {
		if (args[i] == "--ro-bind" || args[i] == "--ro-bind-try") && args[i+1] == path && args[i+2] == path {
			return
		}
	}
	t.Fatalf("expected --ro-bind %q %q, not found in args: %v", path, path, args)
}

func assertWriteBind(t *testing.T, args []string, path string) {
	t.Helper()
	for i := 0; i+2 < len(args); i++ {
		if (args[i] == "--bind" || args[i] == "--bind-try") && args[i+1] == path && args[i+2] == path {
			return
		}
	}
	t.Fatalf("expected --bind-try %q %q, not found in args: %v", path, path, args)
}

func assertNoWriteBind(t *testing.T, args []string, path string) {
	t.Helper()
	for i := 0; i+2 < len(args); i++ {
		if (args[i] == "--bind" || args[i] == "--bind-try") && args[i+1] == path && args[i+2] == path {
			t.Fatalf("unexpected writable bind at %q in args: %v", path, args)
		}
	}
}

func assertReadOnlyBindAfterWritableBind(t *testing.T, args []string, readOnlyPath string, writablePath string) {
	t.Helper()

	writableBindIndex := -1
	readOnlyBindIndex := -1
	for i := 0; i+2 < len(args); i++ {
		if (args[i] == "--bind" || args[i] == "--bind-try") && args[i+1] == writablePath && args[i+2] == writablePath {
			writableBindIndex = i
		}
		if (args[i] == "--ro-bind" || args[i] == "--ro-bind-try") && args[i+1] == readOnlyPath && args[i+2] == readOnlyPath {
			readOnlyBindIndex = i
		}
	}

	require.NotEqual(t, -1, writableBindIndex, "expected writable bind for %q in args: %v", writablePath, args)
	require.NotEqual(t, -1, readOnlyBindIndex, "expected read-only bind for %q in args: %v", readOnlyPath, args)
	assert.Greater(t, readOnlyBindIndex, writableBindIndex, "deny_write read-only bind must override earlier writable parent bind")
}
