// vsix-sandbox.test.mjs — Tests for the VSIX sandbox wrapper and worker.

import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { spawnSync } from 'node:child_process';
import {
  buildSandboxProfile,
  buildBwrapArgs,
  buildSandboxedWorker,
  runVsixWorker,
  runPluginWorker,
  DEFAULT_VSIX_WORKER_PATH,
  DEFAULT_JETBRAINS_WORKER_PATH,
  __testing,
} from '../../scanners/lib/vsix-sandbox.mjs';

describe('vsix-sandbox — buildSandboxProfile', () => {
  it('returns null on non-darwin', () => {
    if (process.platform === 'darwin') return; // Not applicable here.
    const profile = buildSandboxProfile('/tmp');
    assert.equal(profile, null);
  });

  it('returns a valid profile string on macOS when sandbox-exec exists', () => {
    if (process.platform !== 'darwin') return;
    const has = spawnSync('which', ['sandbox-exec'], { encoding: 'utf8' });
    if (has.status !== 0) return;
    const profile = buildSandboxProfile('/tmp');
    assert.ok(profile, 'expected profile string on macOS');
    assert.match(profile, /\(version 1\)/);
    assert.match(profile, /\(deny file-write\*\)/);
    assert.match(profile, /\(allow file-write\* \(subpath /);
  });
});

describe('vsix-sandbox — buildBwrapArgs', () => {
  it('returns null on non-linux', () => {
    if (process.platform === 'linux') return;
    const args = buildBwrapArgs('/tmp', ['/bin/true']);
    assert.equal(args, null);
  });
});

describe('vsix-sandbox — buildSandboxedWorker', () => {
  it('returns sandbox-exec on macOS, bwrap on Linux, or null fallback', () => {
    const { cmd, args, sandbox } = buildSandboxedWorker('/tmp', ['--url', 'https://x', '--tmpdir', '/tmp']);
    assert.ok(cmd);
    assert.ok(Array.isArray(args));
    if (process.platform === 'darwin') {
      const has = spawnSync('which', ['sandbox-exec'], { encoding: 'utf8' });
      if (has.status === 0) {
        assert.equal(sandbox, 'sandbox-exec');
        assert.equal(cmd, 'sandbox-exec');
        assert.equal(args[0], '-p');
      }
    } else if (process.platform === 'linux') {
      // Could be 'bwrap' or null depending on availability — both are valid.
      assert.ok(sandbox === 'bwrap' || sandbox === null);
    } else {
      assert.equal(sandbox, null);
      assert.equal(cmd, 'node');
    }
  });

  it('always includes the worker path and forwarded args', () => {
    const { args } = buildSandboxedWorker('/tmp', ['--url', 'https://example/', '--tmpdir', '/tmp']);
    const joined = args.join(' ');
    assert.match(joined, /vsix-fetch-worker\.mjs/);
    assert.match(joined, /--url https:\/\/example\//);
    assert.match(joined, /--tmpdir \/tmp/);
  });

  it('honors explicit workerPath parameter (Step 10 generalization)', () => {
    const custom = '/path/to/other-worker.mjs';
    const { args } = buildSandboxedWorker('/tmp', ['--foo'], custom);
    const joined = args.join(' ');
    // Must contain the custom path, and NOT the default VSIX worker.
    assert.ok(joined.includes(custom), `expected custom worker in args: ${joined}`);
    assert.ok(!joined.includes('vsix-fetch-worker.mjs'),
      `custom worker should have replaced default: ${joined}`);
    assert.match(joined, /--foo/);
  });

  it('defaults to DEFAULT_VSIX_WORKER_PATH when workerPath omitted', () => {
    const { args } = buildSandboxedWorker('/tmp', ['--url', 'https://x/', '--tmpdir', '/tmp']);
    assert.ok(args.some((a) => a === DEFAULT_VSIX_WORKER_PATH),
      `expected DEFAULT_VSIX_WORKER_PATH in args: ${args.join(' ')}`);
  });

  it('exports DEFAULT_JETBRAINS_WORKER_PATH (Step 10 constant for Step 12)', () => {
    assert.ok(typeof DEFAULT_JETBRAINS_WORKER_PATH === 'string');
    assert.match(DEFAULT_JETBRAINS_WORKER_PATH, /jetbrains-fetch-worker\.mjs$/);
  });
});

describe('vsix-sandbox — runPluginWorker (generalized runner)', () => {
  it('is exported as a function', () => {
    assert.equal(typeof runPluginWorker, 'function');
  });
});

describe('vsix-sandbox — runVsixWorker (live worker, no network)', () => {
  it('handles non-HTTPS URL: worker exits with ok:false and a fetch error', async () => {
    const dir = await mkdtemp(join(tmpdir(), 'llm-sec-vsix-test-'));
    try {
      const { ok, payload, sandbox } = await runVsixWorker('http://example.com/foo.vsix', dir);
      assert.equal(ok, false);
      assert.ok(payload.error, 'expected error message');
      assert.match(payload.error, /fetch failed|HTTPS|unsupported/i);
      // Sandbox may be 'sandbox-exec', 'bwrap', or null on Windows. All valid.
      assert.ok(sandbox === 'sandbox-exec' || sandbox === 'bwrap' || sandbox === null);
    } finally {
      await rm(dir, { recursive: true, force: true });
    }
  });

  it('handles unsupported URL kind: worker exits with ok:false', async () => {
    const dir = await mkdtemp(join(tmpdir(), 'llm-sec-vsix-test-'));
    try {
      const { ok, payload } = await runVsixWorker('https://example.com/random.zip', dir);
      assert.equal(ok, false);
      assert.match(payload.error, /unsupported URL|fetch failed/i);
    } finally {
      await rm(dir, { recursive: true, force: true });
    }
  });

  it('rejects when no --url or --tmpdir is provided (worker arg validation)', async () => {
    // Construct a minimal direct worker call without any args.
    const { spawn } = await import('node:child_process');
    const child = spawn('node', [__testing.WORKER_PATH], { stdio: ['ignore', 'pipe', 'pipe'] });
    let out = '';
    child.stdout.on('data', (c) => { out += c.toString('utf8'); });
    const code = await new Promise((resolve) => child.on('close', resolve));
    assert.equal(code, 1);
    const parsed = JSON.parse(out.trim());
    assert.equal(parsed.ok, false);
    assert.match(parsed.error, /missing --url or --tmpdir/);
  });
});
