// ide-extension-url.test.mjs — Integration tests for `/security ide-scan <url>`.
// Mocks global.fetch so we never hit real Marketplace / OpenVSX endpoints.

import { describe, it, before, after } from 'node:test';
import assert from 'node:assert/strict';
import { resetCounter } from '../../scanners/lib/output.mjs';
import { scan } from '../../scanners/ide-extension-scanner.mjs';
import { buildZip } from '../lib/build-zip.mjs';

const realFetch = globalThis.fetch;

function mockResponse(buffer, { status = 200 } = {}) {
  const stream = new ReadableStream({
    start(controller) { controller.enqueue(buffer); controller.close(); },
  });
  return new Response(stream, { status, headers: { 'content-type': 'application/octet-stream' } });
}

function jsonResponse(obj) {
  return new Response(JSON.stringify(obj), {
    status: 200,
    headers: { 'content-type': 'application/json' },
  });
}

function buildBenignVsix() {
  const pkg = JSON.stringify({
    publisher: 'anthropic',
    name: 'claude-code',
    version: '1.0.0',
    engines: { vscode: '^1.80.0' },
    main: './extension.js',
    activationEvents: ['onCommand:claude.hello'],
    categories: ['Other'],
  });
  return buildZip([
    { name: 'extension.vsixmanifest', data: '<PackageManifest></PackageManifest>' },
    { name: 'extension/package.json', data: pkg },
    { name: 'extension/extension.js', data: 'module.exports = { activate(){} };' },
  ]);
}

function installFetchRouter(routes) {
  globalThis.fetch = async (url) => {
    const handler = routes(url);
    if (!handler) throw new Error(`unrouted fetch: ${url}`);
    return handler;
  };
}

describe('ide-extension-scanner — URL mode', () => {
  before(() => resetCounter());
  after(() => { globalThis.fetch = realFetch; });

  it('rejects unsupported URL with a warning, no extensions scanned', async () => {
    installFetchRouter(() => null);
    const env = await scan('https://example.com/random.zip', { vscodeOnly: true, useSandbox: false });
    assert.equal(env.extensions.length, 0);
    assert.ok(env.meta.warnings.some(w => /unsupported URL/i.test(w)));
    assert.equal(env.meta.source, null);
  });

  it('reports github URL as unsupported in v6.4.0', async () => {
    installFetchRouter(() => null);
    const env = await scan('https://github.com/anthropic/claude-code', { vscodeOnly: true, useSandbox: false });
    assert.equal(env.extensions.length, 0);
    assert.ok(env.meta.warnings.some(w => /GitHub repo URLs/i.test(w)));
  });

  it('fetches OpenVSX VSIX and scans the extracted extension', async () => {
    const vsix = buildBenignVsix();
    let metaCalled = false;
    let downloadCalled = false;
    installFetchRouter((url) => {
      if (url.endsWith('/latest')) {
        metaCalled = true;
        return jsonResponse({ version: '1.0.0' });
      }
      if (url.includes('/file/') && url.endsWith('.vsix')) {
        downloadCalled = true;
        return mockResponse(vsix);
      }
      return null;
    });

    const env = await scan('https://open-vsx.org/extension/anthropic/claude-code', { vscodeOnly: true, useSandbox: false });
    assert.ok(metaCalled, 'expected metadata fetch for latest version');
    assert.ok(downloadCalled, 'expected VSIX download');
    assert.equal(env.extensions.length, 1);
    assert.equal(env.extensions[0].id, 'anthropic.claude-code');
    assert.equal(env.extensions[0].version, '1.0.0');
    assert.ok(env.meta.source);
    assert.equal(env.meta.source.type, 'url');
    assert.equal(env.meta.source.publisher, 'anthropic');
    assert.equal(env.meta.source.name, 'claude-code');
    assert.equal(env.meta.source.version, '1.0.0');
    assert.match(env.meta.source.sha256, /^[a-f0-9]{64}$/);
    assert.equal(env.meta.target, 'https://open-vsx.org/extension/anthropic/claude-code');
  });

  it('fetches Marketplace VSIX directly without metadata round-trip', async () => {
    const vsix = buildBenignVsix();
    let downloads = 0;
    installFetchRouter((url) => {
      if (url.includes('Microsoft.VisualStudio.Services.VSIXPackage')) {
        downloads++;
        return mockResponse(vsix);
      }
      return null;
    });

    const env = await scan('https://marketplace.visualstudio.com/items?itemName=anthropic.claude-code', { vscodeOnly: true, useSandbox: false });
    assert.equal(downloads, 1);
    assert.equal(env.extensions.length, 1);
    assert.equal(env.extensions[0].id, 'anthropic.claude-code');
    assert.equal(env.meta.source.type, 'url');
    assert.equal(env.meta.source.requestedUrl?.includes('VSIXPackage'), true);
  });

  it('cleans up temp dir even when extraction fails', async () => {
    // Return a non-zip body so extract throws.
    installFetchRouter(() => mockResponse(Buffer.from('not a zip at all')));
    const env = await scan('https://example.com/bad.vsix', { vscodeOnly: true, useSandbox: false });
    assert.equal(env.extensions.length, 0);
    assert.ok(env.meta.warnings.some(w => /malformed VSIX/.test(w)));
  });

  it('rejects zip-slip VSIX as malformed', async () => {
    const evil = buildZip([
      { name: 'extension/package.json', data: '{}' },
      { name: '../escape.txt', data: 'pwned' },
    ]);
    installFetchRouter(() => mockResponse(evil));
    const env = await scan('https://example.com/evil.vsix', { vscodeOnly: true, useSandbox: false });
    assert.equal(env.extensions.length, 0);
    assert.ok(env.meta.warnings.some(w => /malformed VSIX/.test(w) && /traversal/.test(w)));
  });

  it('handles fetch network failure cleanly', async () => {
    installFetchRouter(() => { throw new Error('ECONNREFUSED'); });
    const env = await scan('https://open-vsx.org/extension/foo/bar', { vscodeOnly: true, useSandbox: false });
    assert.equal(env.extensions.length, 0);
    assert.ok(env.meta.warnings.some(w => /URL fetch\/extract failed/.test(w)));
  });
});
