import { mkdtempSync, symlinkSync, rmSync, mkdirSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { buildBwrapArgs, buildSandboxEnv, checkBwrapAvailable } from './sandbox.js';

describe('buildBwrapArgs', () => {
  const workspace = '/var/lib/maestro/workspaces/local/42';

  it('returns bwrap command with workspace bind-mounted read-write', () => {
    const args = buildBwrapArgs('echo hello', workspace);
    // workspace should be a --bind (rw), not --ro-bind
    const bindIdx = args.indexOf('--bind');
    expect(bindIdx).toBeGreaterThanOrEqual(0);
    expect(args[bindIdx + 1]).toBe(workspace);
    expect(args[bindIdx + 2]).toBe(workspace);
  });

  it('mounts system directories read-only', () => {
    const args = buildBwrapArgs('ls', workspace);
    const roBind = (src: string) => {
      const idx = args.indexOf(src);
      // the token before source should be '--ro-bind'
      return idx > 0 && args[idx - 1] === '--ro-bind';
    };
    expect(roBind('/usr')).toBe(true);
    expect(roBind('/bin')).toBe(true);
    expect(roBind('/lib')).toBe(true);
    expect(roBind('/etc')).toBe(true);
  });

  it('provides private /tmp as tmpfs', () => {
    const args = buildBwrapArgs('ls', workspace);
    const tmpfsIdx = args.indexOf('--tmpfs');
    expect(tmpfsIdx).toBeGreaterThanOrEqual(0);
    expect(args[tmpfsIdx + 1]).toBe('/tmp');
  });

  it('sets cwd to workspace', () => {
    const args = buildBwrapArgs('ls', workspace);
    const chdirIdx = args.indexOf('--chdir');
    expect(chdirIdx).toBeGreaterThanOrEqual(0);
    expect(args[chdirIdx + 1]).toBe(workspace);
  });

  it('includes --die-with-parent for cleanup', () => {
    const args = buildBwrapArgs('ls', workspace);
    expect(args).toContain('--die-with-parent');
  });

  it('wraps user command in bash -c', () => {
    const args = buildBwrapArgs('echo "hello world" | grep hello', workspace);
    const bashIdx = args.lastIndexOf('/bin/bash');
    expect(bashIdx).toBeGreaterThanOrEqual(0);
    expect(args[bashIdx + 1]).toBe('-c');
    expect(args[bashIdx + 2]).toBe('echo "hello world" | grep hello');
  });

  it('does NOT expose parent directories of workspace', () => {
    const args = buildBwrapArgs('ls', workspace);
    const allBindSrcs: string[] = [];
    for (let i = 0; i < args.length; i++) {
      if (args[i] === '--bind' || args[i] === '--ro-bind') {
        allBindSrcs.push(args[i + 1]);
      }
    }
    // /var/lib/maestro/workspaces/ should NOT be mounted
    expect(allBindSrcs).not.toContain('/var/lib/maestro/workspaces');
    expect(allBindSrcs).not.toContain('/var/lib/maestro/workspaces/');
    expect(allBindSrcs).not.toContain('/var/lib/maestro/workspaces/local');
    // /home should not be mounted
    expect(allBindSrcs).not.toContain('/home');
  });

  it('bind-mounts /lib64 read-only when it exists', () => {
    // /lib64 may or may not exist; buildBwrapArgs should include
    // conditional mounts via --ro-bind-try
    const args = buildBwrapArgs('ls', workspace);
    const idx = args.indexOf('/lib64');
    if (idx >= 0) {
      expect(args[idx - 1] === '--ro-bind' || args[idx - 2] === '--ro-bind-try').toBe(true);
    }
  });
});

describe('buildBwrapArgs extraReadOnlyBinds', () => {
  const workspace = '/var/lib/maestro/workspaces/local/42';
  let tempBase: string;
  let realDir: string;
  let symlinkDir: string;

  beforeAll(() => {
    tempBase = mkdtempSync(join(tmpdir(), 'sandbox-test-'));
    realDir = join(tempBase, 'skills');
    mkdirSync(realDir, { recursive: true });
    symlinkDir = join(tempBase, 'skills-link');
    symlinkSync(realDir, symlinkDir);
  });

  afterAll(() => {
    rmSync(tempBase, { recursive: true, force: true });
  });

  it('includes --ro-bind entries for extraReadOnlyBinds with existing directories', () => {
    const args = buildBwrapArgs('ls', workspace, [
      { src: realDir, dest: '/skills' },
    ]);

    // Find --ro-bind with dest '/skills'
    let found = false;
    for (let i = 0; i < args.length - 2; i++) {
      if (args[i] === '--ro-bind' && args[i + 2] === '/skills') {
        found = true;
        expect(args[i + 1]).toBe(realDir);
        break;
      }
    }
    expect(found).toBe(true);
  });

  it('skips binds with non-existent src paths', () => {
    const args = buildBwrapArgs('ls', workspace, [
      { src: '/nonexistent/path/that/does/not/exist', dest: '/skills' },
    ]);

    // '/skills' should NOT appear as a ro-bind dest
    for (let i = 0; i < args.length - 2; i++) {
      if (args[i] === '--ro-bind' && args[i + 2] === '/skills') {
        expect.fail('non-existent src should have been skipped');
      }
    }
  });

  it('resolves symlinks in src via realpath', () => {
    const args = buildBwrapArgs('ls', workspace, [
      { src: symlinkDir, dest: '/skills' },
    ]);

    // Should resolve the symlink and use the real path
    let found = false;
    for (let i = 0; i < args.length - 2; i++) {
      if (args[i] === '--ro-bind' && args[i + 2] === '/skills') {
        found = true;
        // src should be the resolved realDir, not the symlink path
        expect(args[i + 1]).toBe(realDir);
        break;
      }
    }
    expect(found).toBe(true);
  });

  it('works with no extraReadOnlyBinds (backward compat)', () => {
    // No third argument
    const args1 = buildBwrapArgs('ls', workspace);
    expect(args1).toContain('--ro-bind');
    expect(args1).toContain('/bin/bash');

    // Explicit undefined
    const args2 = buildBwrapArgs('ls', workspace, undefined);
    expect(args2).toEqual(args1);

    // Empty array
    const args3 = buildBwrapArgs('ls', workspace, []);
    expect(args3).toEqual(args1);
  });

  it('places extra ro-bind entries after system RO_BIND_DIRS but before --proc', () => {
    const args = buildBwrapArgs('ls', workspace, [
      { src: realDir, dest: '/skills' },
    ]);

    // Find the position of our extra bind
    let extraBindIdx = -1;
    for (let i = 0; i < args.length - 2; i++) {
      if (args[i] === '--ro-bind' && args[i + 2] === '/skills') {
        extraBindIdx = i;
        break;
      }
    }
    expect(extraBindIdx).toBeGreaterThan(0);

    // Find --proc position
    const procIdx = args.indexOf('--proc');
    expect(procIdx).toBeGreaterThan(extraBindIdx);

    // Find last system RO_BIND position (check /etc as it's the last in RO_BIND_DIRS)
    let lastSystemRoBindIdx = -1;
    for (let i = 0; i < args.length - 2; i++) {
      if (args[i] === '--ro-bind' && args[i + 2] !== '/skills') {
        lastSystemRoBindIdx = i;
      }
    }
    // Extra binds should come after the last system ro-bind that isn't ours
    // (the last system one before the extra one)
    expect(extraBindIdx).toBeGreaterThan(lastSystemRoBindIdx);
  });

  it('skips src paths that are files, not directories', () => {
    // Create a regular file
    const filePath = join(tempBase, 'not-a-dir.txt');
    require('fs').writeFileSync(filePath, 'hello');

    const args = buildBwrapArgs('ls', workspace, [
      { src: filePath, dest: '/skills' },
    ]);

    // '/skills' should NOT appear as a ro-bind dest
    for (let i = 0; i < args.length - 2; i++) {
      if (args[i] === '--ro-bind' && args[i + 2] === '/skills') {
        expect.fail('file src (not directory) should have been skipped');
      }
    }
  });
});

describe('buildSandboxEnv', () => {
  it('keeps only the allowlist and excludes secrets', () => {
    const env = buildSandboxEnv(
      { PATH: '/usr/bin', LANG: 'en_US.UTF-8', MCP_ENCRYPTION_KEY: 'secret',
        OLLAMA_BASE_URL: 'http://x', DB_PATH: '/data/x.db', WORKTREE_DIR: '/w' },
      '/work/ws',
    );
    expect(env.PATH).toBe('/usr/bin');
    expect(env.LANG).toBe('en_US.UTF-8');
    expect(env.HOME).toBe('/work/ws');
    expect(env.TERM).toBe('dumb');
    expect(env.MCP_ENCRYPTION_KEY).toBeUndefined();
    expect(env.OLLAMA_BASE_URL).toBeUndefined();
    expect(env.DB_PATH).toBeUndefined();
    expect(env.WORKTREE_DIR).toBeUndefined();
  });
  it('falls back to C.UTF-8 when LANG absent', () => {
    expect(buildSandboxEnv({ PATH: '/usr/bin' }, '/work/ws').LANG).toBe('C.UTF-8');
  });
});

describe('buildBwrapArgs sandboxing', () => {
  it('clears env, sets only allowlisted vars, and unshares network', () => {
    const args = buildBwrapArgs('echo hi', '/work/ws',
      undefined, { PATH: '/usr/bin', MCP_ENCRYPTION_KEY: 'secret' });
    expect(args).toContain('--clearenv');
    expect(args).toContain('--unshare-net');
    expect(args.join(' ')).not.toContain('secret');
    const i = args.indexOf('--setenv');
    expect(i).toBeGreaterThan(-1);
    expect(args).toContain('HOME');
  });

  it('unshares network by default (allowNetwork omitted)', () => {
    const args = buildBwrapArgs('echo hi', '/work/ws');
    expect(args).toContain('--unshare-net');
  });

  it('keeps host network when allowNetwork=true (drops only --unshare-net)', () => {
    const args = buildBwrapArgs('echo hi', '/work/ws', undefined, process.env, true);
    expect(args).not.toContain('--unshare-net');
    // all other isolations remain
    expect(args).toContain('--unshare-user');
    expect(args).toContain('--unshare-ipc');
    expect(args).toContain('--unshare-pid');
    expect(args).toContain('--unshare-uts');
    expect(args).toContain('--unshare-cgroup');
  });

  it('unshares network when allowNetwork=false (explicit)', () => {
    const args = buildBwrapArgs('echo hi', '/work/ws', undefined, process.env, false);
    expect(args).toContain('--unshare-net');
  });
});

describe('checkBwrapAvailable', () => {
  it('returns an object with ok boolean and message', async () => {
    const result = await checkBwrapAvailable();
    expect(result).toHaveProperty('ok');
    expect(typeof result.ok).toBe('boolean');
    if (!result.ok) {
      expect(result).toHaveProperty('reason');
      expect(typeof result.reason).toBe('string');
    }
  });
});
