/**
 * @license
 * Copyright 2026 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { buildBwrapArgs } from './bwrapArgsBuilder.js';
import fs from 'node:fs';
import * as shellUtils from '../../utils/shell-utils.js';
import os from 'node:os';
import {} from '../../services/sandboxManager.js';
vi.mock('node:fs', async () => {
    const actual = await vi.importActual('node:fs');
    return {
        ...actual,
        default: {
            // @ts-expect-error - Property 'default' does not exist on type 'typeof import("node:fs")'
            ...actual.default,
            existsSync: vi.fn(() => true),
            realpathSync: vi.fn((p) => p.toString()),
            statSync: vi.fn(() => ({ isDirectory: () => true })),
            mkdirSync: vi.fn(),
            mkdtempSync: vi.fn((prefix) => prefix + 'mocked'),
            openSync: vi.fn(),
            closeSync: vi.fn(),
            writeFileSync: vi.fn(),
            readdirSync: vi.fn(() => []),
            chmodSync: vi.fn(),
            unlinkSync: vi.fn(),
            rmSync: vi.fn(),
        },
        existsSync: vi.fn(() => true),
        realpathSync: vi.fn((p) => p.toString()),
        statSync: vi.fn(() => ({ isDirectory: () => true })),
        mkdirSync: vi.fn(),
        mkdtempSync: vi.fn((prefix) => prefix + 'mocked'),
        openSync: vi.fn(),
        closeSync: vi.fn(),
        writeFileSync: vi.fn(),
        readdirSync: vi.fn(() => []),
        chmodSync: vi.fn(),
        unlinkSync: vi.fn(),
        rmSync: vi.fn(),
    };
});
vi.mock('../../utils/shell-utils.js', async (importOriginal) => {
    const actual = await importOriginal();
    return {
        ...actual,
        spawnAsync: vi.fn(() => Promise.resolve({ status: 0, stdout: Buffer.from('') })),
        initializeShellParsers: vi.fn(),
        isStrictlyApproved: vi.fn().mockResolvedValue(true),
    };
});
describe.skipIf(os.platform() === 'win32')('buildBwrapArgs', () => {
    const workspace = '/home/user/workspace';
    const createResolvedPaths = (overrides = {}) => ({
        workspace: {
            original: workspace,
            resolved: workspace,
        },
        forbidden: [],
        globalIncludes: [],
        policyAllowed: [],
        policyRead: [],
        policyWrite: [],
        ...overrides,
    });
    beforeEach(() => {
        vi.clearAllMocks();
        vi.mocked(fs.existsSync).mockReturnValue(true);
        vi.mocked(fs.realpathSync).mockImplementation((p) => p.toString());
    });
    afterEach(() => {
        vi.restoreAllMocks();
    });
    const defaultOptions = {
        resolvedPaths: createResolvedPaths(),
        workspaceWrite: false,
        networkAccess: false,
        maskFilePath: '/tmp/mask',
        isReadOnlyCommand: false,
    };
    it('should correctly format the base arguments', async () => {
        const args = await buildBwrapArgs(defaultOptions);
        expect(args).toEqual([
            '--unshare-all',
            '--new-session',
            '--die-with-parent',
            '--ro-bind',
            '/',
            '/',
            '--dev',
            '/dev',
            '--proc',
            '/proc',
            '--tmpfs',
            '/tmp',
            '--ro-bind-try',
            workspace,
            workspace,
            '--ro-bind',
            `${workspace}/.gitignore`,
            `${workspace}/.gitignore`,
            '--ro-bind',
            `${workspace}/.geminiignore`,
            `${workspace}/.geminiignore`,
            '--ro-bind',
            `${workspace}/.git`,
            `${workspace}/.git`,
        ]);
    });
    it('binds workspace read-write when workspaceWrite is true', async () => {
        const args = await buildBwrapArgs({
            ...defaultOptions,
            workspaceWrite: true,
        });
        expect(args).toContain('--bind-try');
        const bindIndex = args.indexOf('--bind-try');
        expect(args[bindIndex + 1]).toBe(workspace);
    });
    it('maps network permissions to --share-net', async () => {
        const args = await buildBwrapArgs({
            ...defaultOptions,
            networkAccess: true,
        });
        expect(args).toContain('--share-net');
    });
    it('maps explicit write permissions to --bind-try', async () => {
        const args = await buildBwrapArgs({
            ...defaultOptions,
            resolvedPaths: createResolvedPaths({
                policyWrite: ['/home/user/workspace/out/dir'],
            }),
        });
        const index = args.indexOf('--bind-try');
        expect(index).not.toBe(-1);
        expect(args[index + 1]).toBe('/home/user/workspace/out/dir');
    });
    it('should protect both the symlink and the real path of governance files', async () => {
        const args = await buildBwrapArgs({
            ...defaultOptions,
            resolvedPaths: createResolvedPaths({
                workspace: {
                    original: workspace,
                    resolved: '/shared/global-workspace',
                },
            }),
        });
        expect(args).toContain('--ro-bind');
        expect(args).toContain(`${workspace}/.gitignore`);
        expect(args).toContain('/shared/global-workspace/.gitignore');
    });
    it('should parameterize allowed paths', async () => {
        const args = await buildBwrapArgs({
            ...defaultOptions,
            resolvedPaths: createResolvedPaths({
                policyAllowed: ['/tmp/cache', '/opt/tools'],
            }),
        });
        expect(args).toContain('--bind-try');
        expect(args[args.indexOf('/tmp/cache') - 1]).toBe('--bind-try');
        expect(args[args.indexOf('/opt/tools') - 1]).toBe('--bind-try');
    });
    it('should bind the parent directory of a non-existent path with --bind-try when isReadOnlyCommand is false', async () => {
        vi.mocked(fs.existsSync).mockImplementation((p) => {
            if (p === '/home/user/workspace/new-file.txt')
                return false;
            return true;
        });
        const args = await buildBwrapArgs({
            ...defaultOptions,
            isReadOnlyCommand: false,
            resolvedPaths: createResolvedPaths({
                policyAllowed: ['/home/user/workspace/new-file.txt'],
            }),
        });
        const parentDir = '/home/user/workspace';
        const bindIndex = args.lastIndexOf(parentDir);
        expect(bindIndex).not.toBe(-1);
        expect(args[bindIndex - 2]).toBe('--bind-try');
    });
    it('should bind the parent directory of a non-existent path with --ro-bind-try when isReadOnlyCommand is true', async () => {
        vi.mocked(fs.existsSync).mockImplementation((p) => {
            if (p === '/home/user/workspace/new-file.txt')
                return false;
            return true;
        });
        const args = await buildBwrapArgs({
            ...defaultOptions,
            isReadOnlyCommand: true,
            resolvedPaths: createResolvedPaths({
                policyAllowed: ['/home/user/workspace/new-file.txt'],
            }),
        });
        const parentDir = '/home/user/workspace';
        const bindIndex = args.lastIndexOf(parentDir);
        expect(bindIndex).not.toBe(-1);
        expect(args[bindIndex - 2]).toBe('--ro-bind-try');
    });
    it('should parameterize forbidden paths and explicitly deny them', async () => {
        vi.mocked(fs.statSync).mockImplementation((p) => {
            if (p.toString().includes('cache')) {
                return { isDirectory: () => true };
            }
            return { isDirectory: () => false };
        });
        const args = await buildBwrapArgs({
            ...defaultOptions,
            resolvedPaths: createResolvedPaths({
                forbidden: ['/tmp/cache', '/opt/secret.txt'],
            }),
        });
        const cacheIndex = args.indexOf('/tmp/cache');
        expect(args[cacheIndex - 1]).toBe('--tmpfs');
        const secretIndex = args.indexOf('/opt/secret.txt');
        expect(args[secretIndex - 2]).toBe('--ro-bind');
        expect(args[secretIndex - 1]).toBe('/dev/null');
    });
    it('handles resolved forbidden paths', async () => {
        vi.mocked(fs.statSync).mockImplementation(() => ({ isDirectory: () => false }));
        const args = await buildBwrapArgs({
            ...defaultOptions,
            resolvedPaths: createResolvedPaths({
                forbidden: ['/opt/real-target.txt'],
            }),
        });
        const secretIndex = args.indexOf('/opt/real-target.txt');
        expect(args[secretIndex - 2]).toBe('--ro-bind');
        expect(args[secretIndex - 1]).toBe('/dev/null');
    });
    it('masks directory paths with tmpfs', async () => {
        vi.mocked(fs.statSync).mockImplementation(() => ({ isDirectory: () => true }));
        const args = await buildBwrapArgs({
            ...defaultOptions,
            resolvedPaths: createResolvedPaths({
                forbidden: ['/opt/real-dir'],
            }),
        });
        const idx = args.indexOf('/opt/real-dir');
        expect(args[idx - 1]).toBe('--tmpfs');
    });
    it('should apply forbidden paths after allowed paths', async () => {
        vi.mocked(fs.statSync).mockImplementation(() => ({ isDirectory: () => true }));
        const args = await buildBwrapArgs({
            ...defaultOptions,
            resolvedPaths: createResolvedPaths({
                policyAllowed: ['/tmp/conflict'],
                forbidden: ['/tmp/conflict'],
            }),
        });
        const bindIndex = args.findIndex((a, i) => a === '--bind-try' && args[i + 1] === '/tmp/conflict');
        const tmpfsIndex = args.findIndex((a, i) => a === '--tmpfs' && args[i + 1] === '/tmp/conflict');
        expect(bindIndex).toBeGreaterThan(-1);
        expect(tmpfsIndex).toBeGreaterThan(bindIndex);
        expect(args[tmpfsIndex + 1]).toBe('/tmp/conflict');
    });
    it('blocks .env and .env.* files', async () => {
        vi.mocked(shellUtils.spawnAsync).mockImplementation((cmd, args) => {
            if (cmd === 'find' && args?.[0] === workspace) {
                return Promise.resolve({
                    status: 0,
                    stdout: Buffer.from(`${workspace}/.env\0${workspace}/.env.local\0`),
                });
            }
            return Promise.resolve({
                status: 0,
                stdout: Buffer.from(''),
            });
        });
        const args = await buildBwrapArgs(defaultOptions);
        expect(args).toContain(`${workspace}/.env`);
        expect(args).toContain(`${workspace}/.env.local`);
        const envIndex = args.indexOf(`${workspace}/.env`);
        expect(args[envIndex - 2]).toBe('--bind');
        expect(args[envIndex - 1]).toBe('/tmp/mask');
    });
    it('scans globalIncludes for secret files', async () => {
        const includeDir = '/opt/tools';
        vi.mocked(shellUtils.spawnAsync).mockImplementation((cmd, args) => {
            if (cmd === 'find' && args?.[0] === includeDir) {
                return Promise.resolve({
                    status: 0,
                    stdout: Buffer.from(`${includeDir}/.env\0`),
                });
            }
            return Promise.resolve({
                status: 0,
                stdout: Buffer.from(''),
            });
        });
        const args = await buildBwrapArgs({
            ...defaultOptions,
            resolvedPaths: createResolvedPaths({
                globalIncludes: [includeDir],
            }),
        });
        expect(args).toContain(`${includeDir}/.env`);
        const envIndex = args.indexOf(`${includeDir}/.env`);
        expect(args[envIndex - 2]).toBe('--bind');
    });
    it('binds git worktree directories if present', async () => {
        const worktreeGitDir = '/path/to/worktree/.git';
        const mainGitDir = '/path/to/main/.git';
        const args = await buildBwrapArgs({
            ...defaultOptions,
            resolvedPaths: createResolvedPaths({
                gitWorktree: {
                    worktreeGitDir,
                    mainGitDir,
                },
            }),
        });
        expect(args).toContain(worktreeGitDir);
        expect(args).toContain(mainGitDir);
        expect(args[args.indexOf(worktreeGitDir) - 1]).toBe('--ro-bind-try');
        expect(args[args.indexOf(mainGitDir) - 1]).toBe('--ro-bind-try');
    });
    it('enforces read-only binding for git worktrees even if workspaceWrite is true', async () => {
        const worktreeGitDir = '/path/to/worktree/.git';
        const args = await buildBwrapArgs({
            ...defaultOptions,
            workspaceWrite: true,
            resolvedPaths: createResolvedPaths({
                gitWorktree: {
                    worktreeGitDir,
                },
            }),
        });
        expect(args[args.indexOf(worktreeGitDir) - 1]).toBe('--ro-bind-try');
    });
    it('git worktree read-only bindings should override previous policyWrite bindings', async () => {
        const worktreeGitDir = '/custom/worktree/.git';
        const args = await buildBwrapArgs({
            ...defaultOptions,
            resolvedPaths: createResolvedPaths({
                policyWrite: ['/custom/worktree'],
                gitWorktree: {
                    worktreeGitDir,
                },
            }),
        });
        const writeBindIndex = args.indexOf('/custom/worktree');
        const worktreeBindIndex = args.lastIndexOf(worktreeGitDir);
        expect(writeBindIndex).toBeGreaterThan(-1);
        expect(worktreeBindIndex).toBeGreaterThan(-1);
        expect(worktreeBindIndex).toBeGreaterThan(writeBindIndex);
    });
});
//# sourceMappingURL=bwrapArgsBuilder.test.js.map