/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
import { spawn, exec, execFile, execSync } from 'node:child_process';
import os from 'node:os';
import fs from 'node:fs';
import path from 'node:path';
import { randomBytes } from 'node:crypto';
import { start_sandbox } from './sandbox.js';
import {
  FatalSandboxError,
  homedir,
  type SandboxConfig,
} from '@google/gemini-cli-core';
import { createMockSandboxConfig } from '@google/gemini-cli-test-utils';
import { EventEmitter } from 'node:events';

const { mockedHomedir, mockedGetContainerPath, mockedExecCommands } =
  vi.hoisted(() => ({
    mockedHomedir: vi.fn().mockReturnValue('/home/user'),
    mockedGetContainerPath: vi.fn().mockImplementation((p: string) => p),
    mockedExecCommands: [] as string[],
  }));

vi.mock('./sandboxUtils.js', async (importOriginal) => {
  const actual = await importOriginal<typeof import('./sandboxUtils.js')>();
  return {
    ...actual,
    getContainerPath: mockedGetContainerPath,
  };
});

vi.mock('node:child_process');
vi.mock('node:os');
vi.mock('node:fs');
vi.mock('node:crypto', () => ({
  randomBytes: vi.fn().mockReturnValue(Buffer.from('a1b2c3d4e5f6', 'hex')),
}));
vi.mock('node:util', async (importOriginal) => {
  const actual = await importOriginal<typeof import('node:util')>();
  return {
    ...actual,
    promisify: (fn: (...args: unknown[]) => unknown) => {
      if (fn === exec) {
        return async (cmd: string) => {
          mockedExecCommands.push(cmd);
          if (cmd === 'id -u' || cmd === 'id -g') {
            return { stdout: '1000', stderr: '' };
          }
          if (cmd.includes('curl')) {
            return { stdout: '', stderr: '' };
          }
          if (cmd.includes('getconf DARWIN_USER_CACHE_DIR')) {
            return { stdout: '/tmp/cache', stderr: '' };
          }
          return { stdout: '', stderr: '' };
        };
      }
      if (fn === execFile) {
        return async (file: string, args: string[]) => {
          if (file === 'lxc' && args[0] === 'list') {
            const output = process.env['TEST_LXC_LIST_OUTPUT'];
            if (output === 'throw') {
              throw new Error('lxc command not found');
            }
            return { stdout: output ?? '[]', stderr: '' };
          }
          if (
            file === 'lxc' &&
            args[0] === 'config' &&
            args[1] === 'device' &&
            args[2] === 'add'
          ) {
            return { stdout: '', stderr: '' };
          }
          return { stdout: '', stderr: '' };
        };
      }
      return actual.promisify(fn);
    },
  };
});

vi.mock('@google/gemini-cli-core', async (importOriginal) => {
  const actual =
    await importOriginal<typeof import('@google/gemini-cli-core')>();
  return {
    ...actual,
    debugLogger: {
      log: vi.fn(),
      debug: vi.fn(),
      warn: vi.fn(),
    },
    coreEvents: {
      emitFeedback: vi.fn(),
    },
    FatalSandboxError: class extends Error {
      constructor(message: string) {
        super(message);
        this.name = 'FatalSandboxError';
      }
    },
    GEMINI_DIR: '.gemini',
    homedir: mockedHomedir,
  };
});

describe('sandbox', () => {
  const originalEnv = process.env;
  const originalArgv = process.argv;
  let mockProcessIn: {
    pause: ReturnType<typeof vi.fn>;
    resume: ReturnType<typeof vi.fn>;
    isTTY: boolean;
  };

  beforeEach(() => {
    vi.clearAllMocks();
    mockedExecCommands.length = 0;
    process.env = { ...originalEnv };
    process.argv = [...originalArgv];
    mockProcessIn = {
      pause: vi.fn(),
      resume: vi.fn(),
      isTTY: true,
    };
    Object.defineProperty(process, 'stdin', {
      value: mockProcessIn,
      writable: true,
    });
    vi.mocked(os.platform).mockReturnValue('linux');
    vi.mocked(os.homedir).mockReturnValue('/home/user');
    vi.mocked(os.tmpdir).mockReturnValue('/tmp');
    vi.mocked(fs.existsSync).mockReturnValue(true);
    vi.mocked(fs.realpathSync).mockImplementation((p) => p as string);
    vi.mocked(execSync).mockReturnValue(Buffer.from(''));
  });

  afterEach(() => {
    process.env = originalEnv;
    process.argv = originalArgv;
    vi.unstubAllEnvs();
  });

  describe('start_sandbox', () => {
    it('should handle macOS seatbelt (sandbox-exec)', async () => {
      vi.mocked(os.platform).mockReturnValue('darwin');
      const config: SandboxConfig = createMockSandboxConfig({
        command: 'sandbox-exec',
        image: 'some-image',
      });

      interface MockProcess extends EventEmitter {
        stdout: EventEmitter;
        stderr: EventEmitter;
      }
      const mockSpawnProcess = new EventEmitter() as MockProcess;
      mockSpawnProcess.stdout = new EventEmitter();
      mockSpawnProcess.stderr = new EventEmitter();
      vi.mocked(spawn).mockReturnValue(
        mockSpawnProcess as unknown as ReturnType<typeof spawn>,
      );

      const promise = start_sandbox(config, [], undefined, ['arg1']);

      setTimeout(() => {
        mockSpawnProcess.emit('close', 0);
      }, 10);

      await expect(promise).resolves.toBe(0);
      expect(spawn).toHaveBeenCalledWith(
        'sandbox-exec',
        expect.arrayContaining([
          '-f',
          expect.stringContaining('sandbox-macos-permissive-open.sb'),
        ]),
        expect.objectContaining({ stdio: 'inherit' }),
      );
    });

    it('should resolve custom seatbelt profile from user home directory', async () => {
      vi.mocked(os.platform).mockReturnValue('darwin');
      vi.stubEnv('SEATBELT_PROFILE', 'custom-test');
      vi.mocked(fs.existsSync).mockImplementation((p) =>
        String(p).includes(
          path.join(homedir(), '.gemini', 'sandbox-macos-custom-test.sb'),
        ),
      );
      const config: SandboxConfig = createMockSandboxConfig({
        command: 'sandbox-exec',
        image: 'some-image',
      });

      interface MockProcess extends EventEmitter {
        stdout: EventEmitter;
        stderr: EventEmitter;
      }
      const mockSpawnProcess = new EventEmitter() as MockProcess;
      mockSpawnProcess.stdout = new EventEmitter();
      mockSpawnProcess.stderr = new EventEmitter();
      vi.mocked(spawn).mockReturnValue(
        mockSpawnProcess as unknown as ReturnType<typeof spawn>,
      );

      const promise = start_sandbox(config, [], undefined, ['arg1']);

      setTimeout(() => {
        mockSpawnProcess.emit('close', 0);
      }, 10);

      await expect(promise).resolves.toBe(0);
      expect(spawn).toHaveBeenCalledWith(
        'sandbox-exec',
        expect.any(Array),
        expect.objectContaining({ stdio: 'inherit' }),
      );
      const spawnArgs = vi.mocked(spawn).mock.calls[0]?.[1];
      expect(spawnArgs).toEqual(
        expect.arrayContaining(['-f', expect.any(String)]),
      );
      const profileArg = spawnArgs?.[spawnArgs.indexOf('-f') + 1];
      expect(profileArg).toEqual(
        expect.stringContaining(
          path.join(homedir(), '.gemini', 'sandbox-macos-custom-test.sb'),
        ),
      );
    });

    it('should fall back to project .gemini directory when user profile is missing', async () => {
      vi.mocked(os.platform).mockReturnValue('darwin');
      vi.stubEnv('SEATBELT_PROFILE', 'custom-test');
      vi.mocked(fs.existsSync).mockImplementation((p) => {
        const s = String(p);
        return (
          s.includes(path.join('.gemini', 'sandbox-macos-custom-test.sb')) &&
          !s.includes(path.join(homedir(), '.gemini'))
        );
      });
      const config: SandboxConfig = createMockSandboxConfig({
        command: 'sandbox-exec',
        image: 'some-image',
      });

      interface MockProcess extends EventEmitter {
        stdout: EventEmitter;
        stderr: EventEmitter;
      }
      const mockSpawnProcess = new EventEmitter() as MockProcess;
      mockSpawnProcess.stdout = new EventEmitter();
      mockSpawnProcess.stderr = new EventEmitter();
      vi.mocked(spawn).mockReturnValue(
        mockSpawnProcess as unknown as ReturnType<typeof spawn>,
      );

      const promise = start_sandbox(config, [], undefined, ['arg1']);

      setTimeout(() => {
        mockSpawnProcess.emit('close', 0);
      }, 10);

      await expect(promise).resolves.toBe(0);
      expect(spawn).toHaveBeenCalledWith(
        'sandbox-exec',
        expect.any(Array),
        expect.objectContaining({ stdio: 'inherit' }),
      );
      const spawnArgs = vi.mocked(spawn).mock.calls[0]?.[1];
      expect(spawnArgs).toEqual(
        expect.arrayContaining(['-f', expect.any(String)]),
      );
      const profileArg = spawnArgs?.[spawnArgs.indexOf('-f') + 1];
      expect(profileArg).toEqual(
        expect.stringContaining(
          path.join('.gemini', 'sandbox-macos-custom-test.sb'),
        ),
      );
      expect(profileArg).not.toContain(homedir());
    });

    it('should throw FatalSandboxError if seatbelt profile is missing', async () => {
      vi.mocked(os.platform).mockReturnValue('darwin');
      vi.mocked(fs.existsSync).mockReturnValue(false);
      const config: SandboxConfig = createMockSandboxConfig({
        command: 'sandbox-exec',
        image: 'some-image',
      });

      await expect(start_sandbox(config)).rejects.toThrow(FatalSandboxError);
    });

    it('should handle Docker execution', async () => {
      const config: SandboxConfig = createMockSandboxConfig({
        command: 'docker',
        image: 'gemini-cli-sandbox',
      });

      // Mock image check to return true (image exists)
      interface MockProcessWithStdout extends EventEmitter {
        stdout: EventEmitter;
      }
      const mockImageCheckProcess = new EventEmitter() as MockProcessWithStdout;
      mockImageCheckProcess.stdout = new EventEmitter();
      vi.mocked(spawn).mockImplementationOnce((_cmd, args) => {
        if (args && args[0] === 'images') {
          setTimeout(() => {
            mockImageCheckProcess.stdout.emit('data', Buffer.from('image-id'));
            mockImageCheckProcess.emit('close', 0);
          }, 1);
          return mockImageCheckProcess as unknown as ReturnType<typeof spawn>;
        }
        return new EventEmitter() as unknown as ReturnType<typeof spawn>; // fallback
      });

      const mockSpawnProcess = new EventEmitter() as unknown as ReturnType<
        typeof spawn
      >;
      mockSpawnProcess.on = vi.fn().mockImplementation((event, cb) => {
        if (event === 'close') {
          setTimeout(() => cb(0), 10);
        }
        return mockSpawnProcess;
      });
      vi.mocked(spawn).mockImplementationOnce((cmd, args) => {
        if (cmd === 'docker' && args && args[0] === 'run') {
          return mockSpawnProcess;
        }
        return new EventEmitter() as unknown as ReturnType<typeof spawn>;
      });

      const promise = start_sandbox(config, [], undefined, ['arg1']);

      await expect(promise).resolves.toBe(0);
      expect(spawn).toHaveBeenCalledWith(
        'docker',
        expect.arrayContaining(['run', '-i', '--rm', '--init']),
        expect.objectContaining({ stdio: 'inherit' }),
      );

      const containerName = 'gemini-cli-sandbox-a1b2c3d4e5f6';
      expect(randomBytes).toHaveBeenCalledWith(6);
      expect(mockedExecCommands).not.toEqual(
        expect.arrayContaining([expect.stringContaining('ps -a --format')]),
      );
      expect(spawn).toHaveBeenNthCalledWith(
        2,
        'docker',
        expect.arrayContaining([
          '--name',
          containerName,
          '--hostname',
          containerName,
          '--env',
          `SANDBOX=${containerName}`,
        ]),
        expect.objectContaining({ stdio: 'inherit' }),
      );
    });

    it('should preserve the integration-test prefix for random container names', async () => {
      const config: SandboxConfig = createMockSandboxConfig({
        command: 'docker',
        image: 'gemini-cli-sandbox',
      });
      process.env['GEMINI_CLI_INTEGRATION_TEST'] = 'true';

      interface MockProcessWithStdout extends EventEmitter {
        stdout: EventEmitter;
      }
      const mockImageCheckProcess = new EventEmitter() as MockProcessWithStdout;
      mockImageCheckProcess.stdout = new EventEmitter();
      vi.mocked(spawn).mockImplementationOnce(() => {
        setTimeout(() => {
          mockImageCheckProcess.stdout.emit('data', Buffer.from('image-id'));
          mockImageCheckProcess.emit('close', 0);
        }, 1);
        return mockImageCheckProcess as unknown as ReturnType<typeof spawn>;
      });

      const mockSpawnProcess = new EventEmitter() as unknown as ReturnType<
        typeof spawn
      >;
      mockSpawnProcess.on = vi.fn().mockImplementation((event, cb) => {
        if (event === 'close') {
          setTimeout(() => cb(0), 10);
        }
        return mockSpawnProcess;
      });
      vi.mocked(spawn).mockImplementationOnce(() => mockSpawnProcess);

      await expect(
        start_sandbox(config, [], undefined, ['arg1']),
      ).resolves.toBe(0);

      const containerName = 'gemini-cli-integration-test-a1b2c3d4e5f6';
      expect(randomBytes).toHaveBeenCalledWith(6);
      expect(spawn).toHaveBeenNthCalledWith(
        2,
        'docker',
        expect.arrayContaining([
          '--name',
          containerName,
          '--hostname',
          containerName,
          '--env',
          `SANDBOX=${containerName}`,
        ]),
        expect.objectContaining({ stdio: 'inherit' }),
      );
    });

    it('should pull image if missing', async () => {
      const config: SandboxConfig = createMockSandboxConfig({
        command: 'docker',
        image: 'missing-image',
      });

      // 1. Image check fails
      interface MockProcessWithStdout extends EventEmitter {
        stdout: EventEmitter;
      }
      const mockImageCheckProcess1 =
        new EventEmitter() as MockProcessWithStdout;
      mockImageCheckProcess1.stdout = new EventEmitter();
      vi.mocked(spawn).mockImplementationOnce(() => {
        setTimeout(() => {
          mockImageCheckProcess1.emit('close', 0);
        }, 1);
        return mockImageCheckProcess1 as unknown as ReturnType<typeof spawn>;
      });

      // 2. Pull image succeeds
      interface MockProcessWithStdoutStderr extends EventEmitter {
        stdout: EventEmitter;
        stderr: EventEmitter;
      }
      const mockPullProcess = new EventEmitter() as MockProcessWithStdoutStderr;
      mockPullProcess.stdout = new EventEmitter();
      mockPullProcess.stderr = new EventEmitter();
      vi.mocked(spawn).mockImplementationOnce(() => {
        setTimeout(() => {
          mockPullProcess.emit('close', 0);
        }, 1);
        return mockPullProcess as unknown as ReturnType<typeof spawn>;
      });

      // 3. Image check succeeds
      const mockImageCheckProcess2 =
        new EventEmitter() as MockProcessWithStdout;
      mockImageCheckProcess2.stdout = new EventEmitter();
      vi.mocked(spawn).mockImplementationOnce(() => {
        setTimeout(() => {
          mockImageCheckProcess2.stdout.emit('data', Buffer.from('image-id'));
          mockImageCheckProcess2.emit('close', 0);
        }, 1);
        return mockImageCheckProcess2 as unknown as ReturnType<typeof spawn>;
      });

      // 4. Docker run
      const mockSpawnProcess = new EventEmitter() as unknown as ReturnType<
        typeof spawn
      >;
      mockSpawnProcess.on = vi.fn().mockImplementation((event, cb) => {
        if (event === 'close') {
          setTimeout(() => cb(0), 10);
        }
        return mockSpawnProcess;
      });
      vi.mocked(spawn).mockImplementationOnce(() => mockSpawnProcess);

      const promise = start_sandbox(config, [], undefined, ['arg1']);

      await expect(promise).resolves.toBe(0);
      expect(spawn).toHaveBeenCalledWith(
        'docker',
        expect.arrayContaining(['pull', 'missing-image']),
        expect.any(Object),
      );
    });

    it('should throw if image pull fails', async () => {
      const config: SandboxConfig = createMockSandboxConfig({
        command: 'docker',
        image: 'missing-image',
      });

      // 1. Image check fails
      interface MockProcessWithStdout extends EventEmitter {
        stdout: EventEmitter;
      }
      const mockImageCheckProcess1 =
        new EventEmitter() as MockProcessWithStdout;
      mockImageCheckProcess1.stdout = new EventEmitter();
      vi.mocked(spawn).mockImplementationOnce(() => {
        setTimeout(() => {
          mockImageCheckProcess1.emit('close', 0);
        }, 1);
        return mockImageCheckProcess1 as unknown as ReturnType<typeof spawn>;
      });

      // 2. Pull image fails
      interface MockProcessWithStdoutStderr extends EventEmitter {
        stdout: EventEmitter;
        stderr: EventEmitter;
      }
      const mockPullProcess = new EventEmitter() as MockProcessWithStdoutStderr;
      mockPullProcess.stdout = new EventEmitter();
      mockPullProcess.stderr = new EventEmitter();
      vi.mocked(spawn).mockImplementationOnce(() => {
        setTimeout(() => {
          mockPullProcess.emit('close', 1);
        }, 1);
        return mockPullProcess as unknown as ReturnType<typeof spawn>;
      });

      await expect(start_sandbox(config)).rejects.toThrow(FatalSandboxError);
    });

    it('should mount volumes correctly', async () => {
      const config: SandboxConfig = createMockSandboxConfig({
        command: 'docker',
        image: 'gemini-cli-sandbox',
      });
      process.env['SANDBOX_MOUNTS'] = '/host/path:/container/path:ro';
      vi.mocked(fs.existsSync).mockReturnValue(true); // For mount path check

      // Mock image check to return true
      interface MockProcessWithStdout extends EventEmitter {
        stdout: EventEmitter;
      }
      const mockImageCheckProcess = new EventEmitter() as MockProcessWithStdout;
      mockImageCheckProcess.stdout = new EventEmitter();
      vi.mocked(spawn).mockImplementationOnce(() => {
        setTimeout(() => {
          mockImageCheckProcess.stdout.emit('data', Buffer.from('image-id'));
          mockImageCheckProcess.emit('close', 0);
        }, 1);
        return mockImageCheckProcess as unknown as ReturnType<typeof spawn>;
      });

      const mockSpawnProcess = new EventEmitter() as unknown as ReturnType<
        typeof spawn
      >;
      mockSpawnProcess.on = vi.fn().mockImplementation((event, cb) => {
        if (event === 'close') {
          setTimeout(() => cb(0), 10);
        }
        return mockSpawnProcess;
      });
      vi.mocked(spawn).mockImplementationOnce(() => mockSpawnProcess);

      await start_sandbox(config);

      // The first call is 'docker images -q ...'
      expect(spawn).toHaveBeenNthCalledWith(
        1,
        'docker',
        expect.arrayContaining(['images', '-q']),
      );

      // The second call is 'docker run ...'
      expect(spawn).toHaveBeenNthCalledWith(
        2,
        'docker',
        expect.arrayContaining([
          'run',
          '--volume',
          '/host/path:/container/path:ro',
          '--volume',
          expect.stringMatching(/[\\/]home[\\/]user[\\/]\.gemini/),
        ]),
        expect.any(Object),
      );
    });

    it('should handle allowedPaths in Docker', async () => {
      const config: SandboxConfig = createMockSandboxConfig({
        command: 'docker',
        image: 'gemini-cli-sandbox',
        allowedPaths: ['/extra/path'],
      });
      vi.mocked(fs.existsSync).mockReturnValue(true);

      // Mock image check to return true
      interface MockProcessWithStdout extends EventEmitter {
        stdout: EventEmitter;
      }
      const mockImageCheckProcess = new EventEmitter() as MockProcessWithStdout;
      mockImageCheckProcess.stdout = new EventEmitter();
      vi.mocked(spawn).mockImplementationOnce(() => {
        setTimeout(() => {
          mockImageCheckProcess.stdout.emit('data', Buffer.from('image-id'));
          mockImageCheckProcess.emit('close', 0);
        }, 1);
        return mockImageCheckProcess as unknown as ReturnType<typeof spawn>;
      });

      const mockSpawnProcess = new EventEmitter() as unknown as ReturnType<
        typeof spawn
      >;
      mockSpawnProcess.on = vi.fn().mockImplementation((event, cb) => {
        if (event === 'close') {
          setTimeout(() => cb(0), 10);
        }
        return mockSpawnProcess;
      });
      vi.mocked(spawn).mockImplementationOnce(() => mockSpawnProcess);

      await start_sandbox(config);

      expect(spawn).toHaveBeenCalledWith(
        'docker',
        expect.arrayContaining(['--volume', '/extra/path:/extra/path:ro']),
        expect.any(Object),
      );
    });

    it('should handle networkAccess: false in Docker', async () => {
      const config: SandboxConfig = createMockSandboxConfig({
        command: 'docker',
        image: 'gemini-cli-sandbox',
        networkAccess: false,
      });

      // Mock image check
      interface MockProcessWithStdout extends EventEmitter {
        stdout: EventEmitter;
      }
      const mockImageCheckProcess = new EventEmitter() as MockProcessWithStdout;
      mockImageCheckProcess.stdout = new EventEmitter();
      vi.mocked(spawn).mockImplementationOnce(() => {
        setTimeout(() => {
          mockImageCheckProcess.stdout.emit('data', Buffer.from('image-id'));
          mockImageCheckProcess.emit('close', 0);
        }, 1);
        return mockImageCheckProcess as unknown as ReturnType<typeof spawn>;
      });

      const mockSpawnProcess = new EventEmitter() as unknown as ReturnType<
        typeof spawn
      >;
      mockSpawnProcess.on = vi.fn().mockImplementation((event, cb) => {
        if (event === 'close') {
          setTimeout(() => cb(0), 10);
        }
        return mockSpawnProcess;
      });
      vi.mocked(spawn).mockImplementationOnce(() => mockSpawnProcess);

      await start_sandbox(config);

      expect(execSync).toHaveBeenCalledWith(
        expect.stringContaining('network create --internal gemini-cli-sandbox'),
        expect.any(Object),
      );
      expect(spawn).toHaveBeenCalledWith(
        'docker',
        expect.arrayContaining(['--network', 'gemini-cli-sandbox']),
        expect.any(Object),
      );
    });

    it('should handle allowedPaths in macOS seatbelt', async () => {
      vi.mocked(os.platform).mockReturnValue('darwin');
      const config: SandboxConfig = createMockSandboxConfig({
        command: 'sandbox-exec',
        image: 'some-image',
        allowedPaths: ['/Users/user/extra'],
      });
      vi.mocked(fs.existsSync).mockReturnValue(true);

      interface MockProcess extends EventEmitter {
        stdout: EventEmitter;
        stderr: EventEmitter;
      }
      const mockSpawnProcess = new EventEmitter() as MockProcess;
      mockSpawnProcess.stdout = new EventEmitter();
      mockSpawnProcess.stderr = new EventEmitter();
      vi.mocked(spawn).mockReturnValue(
        mockSpawnProcess as unknown as ReturnType<typeof spawn>,
      );

      const promise = start_sandbox(config);
      setTimeout(() => mockSpawnProcess.emit('close', 0), 10);
      await promise;

      // Check that the extra path is passed as an INCLUDE_DIR_X argument
      expect(spawn).toHaveBeenCalledWith(
        'sandbox-exec',
        expect.arrayContaining(['INCLUDE_DIR_0=/Users/user/extra']),
        expect.any(Object),
      );
    });

    it('should pass through GOOGLE_GEMINI_BASE_URL and GOOGLE_VERTEX_BASE_URL', async () => {
      const config: SandboxConfig = createMockSandboxConfig({
        command: 'docker',
        image: 'gemini-cli-sandbox',
      });
      process.env['GOOGLE_GEMINI_BASE_URL'] = 'http://gemini.proxy';
      process.env['GOOGLE_VERTEX_BASE_URL'] = 'http://vertex.proxy';

      // Mock image check to return true
      interface MockProcessWithStdout extends EventEmitter {
        stdout: EventEmitter;
      }
      const mockImageCheckProcess = new EventEmitter() as MockProcessWithStdout;
      mockImageCheckProcess.stdout = new EventEmitter();
      vi.mocked(spawn).mockImplementationOnce(() => {
        setTimeout(() => {
          mockImageCheckProcess.stdout.emit('data', Buffer.from('image-id'));
          mockImageCheckProcess.emit('close', 0);
        }, 1);
        return mockImageCheckProcess as unknown as ReturnType<typeof spawn>;
      });

      const mockSpawnProcess = new EventEmitter() as unknown as ReturnType<
        typeof spawn
      >;
      mockSpawnProcess.on = vi.fn().mockImplementation((event, cb) => {
        if (event === 'close') {
          setTimeout(() => cb(0), 10);
        }
        return mockSpawnProcess;
      });
      vi.mocked(spawn).mockImplementationOnce(() => mockSpawnProcess);

      await start_sandbox(config);

      expect(spawn).toHaveBeenCalledWith(
        'docker',
        expect.arrayContaining([
          '--env',
          'GOOGLE_GEMINI_BASE_URL=http://gemini.proxy',
          '--env',
          'GOOGLE_VERTEX_BASE_URL=http://vertex.proxy',
        ]),
        expect.any(Object),
      );
    });

    it('should handle user creation on Linux if needed', async () => {
      const config: SandboxConfig = createMockSandboxConfig({
        command: 'docker',
        image: 'gemini-cli-sandbox',
      });
      process.env['SANDBOX_SET_UID_GID'] = 'true';
      vi.mocked(os.platform).mockReturnValue('linux');
      vi.mocked(execSync).mockImplementation((cmd) => {
        if (cmd === 'id -u') return Buffer.from('1000');
        if (cmd === 'id -g') return Buffer.from('1000');
        return Buffer.from('');
      });

      // Mock image check to return true
      interface MockProcessWithStdout extends EventEmitter {
        stdout: EventEmitter;
      }
      const mockImageCheckProcess = new EventEmitter() as MockProcessWithStdout;
      mockImageCheckProcess.stdout = new EventEmitter();
      vi.mocked(spawn).mockImplementationOnce(() => {
        setTimeout(() => {
          mockImageCheckProcess.stdout.emit('data', Buffer.from('image-id'));
          mockImageCheckProcess.emit('close', 0);
        }, 1);
        return mockImageCheckProcess as unknown as ReturnType<typeof spawn>;
      });

      const mockSpawnProcess = new EventEmitter() as unknown as ReturnType<
        typeof spawn
      >;
      mockSpawnProcess.on = vi.fn().mockImplementation((event, cb) => {
        if (event === 'close') {
          setTimeout(() => cb(0), 10);
        }
        return mockSpawnProcess;
      });
      vi.mocked(spawn).mockImplementationOnce(() => mockSpawnProcess);

      await start_sandbox(config);

      expect(spawn).toHaveBeenCalledWith(
        'docker',
        expect.arrayContaining(['--user', 'root', '--env', 'HOME=/home/user']),
        expect.any(Object),
      );
      // Check that the entrypoint command includes useradd/groupadd
      const args = vi.mocked(spawn).mock.calls[1][1] as string[];
      const entrypointCmd = args[args.length - 1];
      expect(entrypointCmd).toContain('groupadd');
      expect(entrypointCmd).toContain('useradd');
      expect(entrypointCmd).toContain('su -p gemini');
    });

    it('should register and unregister proxy exit handlers', async () => {
      vi.stubEnv('GEMINI_SANDBOX_PROXY_COMMAND', 'some-proxy-cmd');
      const config: SandboxConfig = createMockSandboxConfig({
        command: 'docker',
        image: 'gemini-cli-sandbox',
      });

      const onSpy = vi.spyOn(process, 'on');
      const offSpy = vi.spyOn(process, 'off');

      interface MockProcessWithStdout extends EventEmitter {
        stdout: EventEmitter;
      }

      vi.mocked(spawn).mockImplementation((cmd, args) => {
        const a = args as string[];
        if (cmd === 'docker' && a && a[0] === 'images') {
          const mockImageCheckProcess =
            new EventEmitter() as MockProcessWithStdout;
          mockImageCheckProcess.stdout = new EventEmitter();
          setTimeout(() => {
            mockImageCheckProcess.stdout.emit('data', Buffer.from('image-id'));
            mockImageCheckProcess.emit('close', 0);
          }, 1);
          return mockImageCheckProcess as unknown as ReturnType<typeof spawn>;
        }
        if (cmd === 'docker' && a && a[0] === 'run') {
          const mockSpawnProcess = new EventEmitter() as unknown as ReturnType<
            typeof spawn
          >;
          mockSpawnProcess.on = vi.fn().mockImplementation((event, cb) => {
            if (event === 'close') {
              if (a.includes('gemini-cli-sandbox-proxy')) {
                // Proxy container shouldn't exit during the test
              } else {
                setTimeout(() => cb(0), 10);
              }
            }
            return mockSpawnProcess;
          });
          return mockSpawnProcess;
        }
        return new EventEmitter() as unknown as ReturnType<typeof spawn>;
      });

      await start_sandbox(config);

      expect(onSpy).toHaveBeenCalledWith('exit', expect.any(Function));
      expect(onSpy).toHaveBeenCalledWith('SIGINT', expect.any(Function));
      expect(onSpy).toHaveBeenCalledWith('SIGTERM', expect.any(Function));

      expect(offSpy).toHaveBeenCalledWith('exit', expect.any(Function));
      expect(offSpy).toHaveBeenCalledWith('SIGINT', expect.any(Function));
      expect(offSpy).toHaveBeenCalledWith('SIGTERM', expect.any(Function));

      onSpy.mockRestore();
      offSpy.mockRestore();
    });

    describe('LXC sandbox', () => {
      const LXC_RUNNING = JSON.stringify([
        { name: 'gemini-sandbox', status: 'Running' },
      ]);
      const LXC_STOPPED = JSON.stringify([
        { name: 'gemini-sandbox', status: 'Stopped' },
      ]);

      beforeEach(() => {
        delete process.env['TEST_LXC_LIST_OUTPUT'];
      });

      it('should run lxc exec with correct args for a running container', async () => {
        process.env['TEST_LXC_LIST_OUTPUT'] = LXC_RUNNING;
        const config: SandboxConfig = createMockSandboxConfig({
          command: 'lxc',
          image: 'gemini-sandbox',
        });

        const mockSpawnProcess = new EventEmitter() as unknown as ReturnType<
          typeof spawn
        >;
        mockSpawnProcess.on = vi.fn().mockImplementation((event, cb) => {
          if (event === 'close') {
            setTimeout(() => cb(0), 10);
          }
          return mockSpawnProcess;
        });

        vi.mocked(spawn).mockImplementation((cmd) => {
          if (cmd === 'lxc') {
            return mockSpawnProcess;
          }
          return new EventEmitter() as unknown as ReturnType<typeof spawn>;
        });

        const promise = start_sandbox(config, [], undefined, ['arg1']);
        await expect(promise).resolves.toBe(0);

        expect(spawn).toHaveBeenCalledWith(
          'lxc',
          expect.arrayContaining(['exec', 'gemini-sandbox', '--cwd']),
          expect.objectContaining({ stdio: 'inherit' }),
        );
      });

      it('should throw FatalSandboxError if lxc list fails', async () => {
        process.env['TEST_LXC_LIST_OUTPUT'] = 'throw';
        const config: SandboxConfig = createMockSandboxConfig({
          command: 'lxc',
          image: 'gemini-sandbox',
        });

        await expect(start_sandbox(config)).rejects.toThrow(
          /Failed to query LXC container/,
        );
      });

      it('should throw FatalSandboxError if container is not running', async () => {
        process.env['TEST_LXC_LIST_OUTPUT'] = LXC_STOPPED;
        const config: SandboxConfig = createMockSandboxConfig({
          command: 'lxc',
          image: 'gemini-sandbox',
        });

        await expect(start_sandbox(config)).rejects.toThrow(/is not running/);
      });

      it('should throw FatalSandboxError if container is not found in list', async () => {
        process.env['TEST_LXC_LIST_OUTPUT'] = '[]';
        const config: SandboxConfig = createMockSandboxConfig({
          command: 'lxc',
          image: 'gemini-sandbox',
        });

        await expect(start_sandbox(config)).rejects.toThrow(/not found/);
      });
    });
  });

  describe('gVisor (runsc)', () => {
    it('should use docker with --runtime=runsc on Linux', async () => {
      vi.mocked(os.platform).mockReturnValue('linux');
      const config: SandboxConfig = createMockSandboxConfig({
        command: 'runsc',
        image: 'gemini-cli-sandbox',
      });

      // Mock image check
      interface MockProcessWithStdout extends EventEmitter {
        stdout: EventEmitter;
      }
      const mockImageCheckProcess = new EventEmitter() as MockProcessWithStdout;
      mockImageCheckProcess.stdout = new EventEmitter();
      vi.mocked(spawn).mockImplementationOnce(() => {
        setTimeout(() => {
          mockImageCheckProcess.stdout.emit('data', Buffer.from('image-id'));
          mockImageCheckProcess.emit('close', 0);
        }, 1);
        return mockImageCheckProcess as unknown as ReturnType<typeof spawn>;
      });

      // Mock docker run
      const mockSpawnProcess = new EventEmitter() as unknown as ReturnType<
        typeof spawn
      >;
      mockSpawnProcess.on = vi.fn().mockImplementation((event, cb) => {
        if (event === 'close') {
          setTimeout(() => cb(0), 10);
        }
        return mockSpawnProcess;
      });
      vi.mocked(spawn).mockImplementationOnce(() => mockSpawnProcess);

      await start_sandbox(config, [], undefined, ['arg1']);

      // Verify docker (not runsc) is called for image check
      expect(spawn).toHaveBeenNthCalledWith(
        1,
        'docker',
        expect.arrayContaining(['images', '-q', 'gemini-cli-sandbox']),
      );

      // Verify docker run includes --runtime=runsc
      expect(spawn).toHaveBeenNthCalledWith(
        2,
        'docker',
        expect.arrayContaining(['run', '--runtime=runsc']),
        expect.objectContaining({ stdio: 'inherit' }),
      );
    });
  });
});
