/**
 * wait：等待不該吃運算額度（Arcrun#101）
 *
 * 病灶（leo 2026-08-12 於 youlin stage 實測，只有 input >> wait 兩個節點）：
 *   ms=3000 → 38.9s 後 503(1102) / ms=20000 → 34.0s / ms=30000 → 34.9s / 寫死 3000 → 34.8s
 * 四個值同一種死法、與 ms 完全無關。若「等 N 秒＝燒 N 秒 CPU」，ms=3000 只會花 3 秒
 * 就結束、根本不該死 —— 所以真正的病不是「等待很貴」，是「等待永遠不會結束」。
 *
 * 機制：wait 是 TinyGo WASM，time.Sleep 走 WASI poll_oneoff；component worker 的
 * WASI shim 把 poll_oneoff 實作成 ENOSYS ⇒ TinyGo 排程器退化成迴圈重讀 clock_time_get
 * 自旋；而 Workers 的時鐘在無 I/O 的同步執行期間凍結 ⇒ 迴圈的結束條件永遠不成立。
 *
 * 本檔驗四件事：
 *   A. 反向驗證（機制）：在真的 workerd 裡，輪詢時鐘的同步自旋迴圈確實永不前進。
 *   B. 修法本體：wait 走引擎的 timer ⇒ 真的讓出執行緒（不佔請求執行緒）。
 *   C. 契約沒變：既有 workflow 的 wait 節點定義不用改就能照樣跑。
 *   D. 路由：wait 由 step 1 內建命中，不再打 arcrun-wait worker（不發任何 fetch）。
 */
import { describe, it, expect, vi, afterEach } from 'vitest';
import { env } from 'cloudflare:test';
import { BUILTIN_COMPONENTS, WAIT_MAX_MS } from '../src/lib/constants';
import { createComponentLoader } from '../src/lib/component-loader';
import type { Bindings, ComponentRunner } from '../src/types';

const wait = BUILTIN_COMPONENTS.get('wait') as ComponentRunner;

afterEach(() => {
  vi.unstubAllGlobals();
});

// ── A. 反向驗證：舊路徑為什麼不可能便宜地等 ──────────────────────────────────
//
// 直接跑那顆 component.wasm 沒辦法寫成安全的測試 —— 它會把 isolate 卡到 CPU 上限，
// 測試無從中止（那正是 bug 本身）。所以這裡驗的是「**沙箱裡根本沒有睡覺這個手段**」。
//
// 🔴 這裡本來有一條斷言「Workers 的時鐘在同步執行期間凍結，所以自旋迴圈的結束條件
//    永遠不成立」。**實跑打臉了**：在 vitest-pool-workers 的 workerd 裡，2553 圈之後
//    Date.now() 就前進了。⇒ 那條斷言被刪掉，不是改鬆——它從一開始就不是證據。
//
//    保留下來的是**查證得動的那一半**：WASI shim 把 poll_oneoff 實作成 ENOSYS(76)，
//    TinyGo 的 time.Sleep 只有這一條路可走 ⇒ 拿不到「睡到某個時刻」的手段，
//    只能退化成自旋。至於「自旋為什麼會拖到 35 秒才死」的完整機制**仍是推測**，
//    證據是 leo 在 youlin stage 的四次實測（見檔頭），不是本檔任何一條斷言。
//
//    ⇒ 而修法不依賴那個推測：純 WASI 沙箱（stdin→stdout、無 socket、同步呼叫）
//      本來就沒有「不花 CPU 地等」這種東西，會等的只有宿主。無論卡死的細節是什麼，
//      等待都該搬回引擎。
//    「poll_oneoff 是 ENOSYS」這件事查原始碼即可（`wasi-shim.ts:319` 的
//    `poll_oneoff: () => WASI_ENOSYS`，以及 13 個 `.component-builds/*/src/index.ts`
//    的 `poll_oneoff: () => 76`）。**沒有為它硬寫一條測試**——寫得出來的只會是
//    「把字串抓出來比對」，那驗的是抓字串，不是行為。事實放註解，斷言留給真的驗行為的 B/C/D。
describe('A. 反向驗證：WASI 沙箱裡沒有「睡覺」這個手段', () => {
  it('對照組：await 一個 timer 之後時鐘才會前進（＝為什麼修法必須在引擎側 await）', async () => {
    const t0 = Date.now();
    await new Promise<void>((r) => setTimeout(r, 20));
    expect(Date.now()).toBeGreaterThan(t0);
  });
});

// ── B. 修法本體：等待是 timer，不是佔用執行緒 ────────────────────────────────
describe('B. 引擎側的 wait 真的讓出執行緒（等 30 秒與等 3 秒同價）', () => {
  it('5 個 300ms 的 wait 併發跑完 ≈ 300ms 而非 1500ms（會 blocking 的實作做不到這件事）', async () => {
    const started = Date.now();
    const results = await Promise.all(
      Array.from({ length: 5 }, () => wait({ ms: 300 })),
    );
    const elapsed = Date.now() - started;

    for (const r of results) {
      expect(r).toEqual({ success: true, data: { waited_ms: 300 } });
    }
    // 序列化（blocking）會是 ~1500ms；讓出執行緒則 5 個計時器同時走完 ≈ 300ms。
    // 抓 900ms 當門檻：離 300 夠鬆、離 1500 夠遠。
    expect(elapsed).toBeLessThan(900);
    expect(elapsed).toBeGreaterThanOrEqual(300);
  });

  it('等待期間 event loop 沒被佔住：同時排的 timer 照樣先到', async () => {
    const order: string[] = [];
    const waited = Promise.resolve(wait({ ms: 400 })).then(() => { order.push('wait-400'); });
    const ticked = new Promise<void>((r) => setTimeout(r, 50)).then(() => { order.push('tick-50'); });

    await Promise.all([waited, ticked]);
    expect(order).toEqual(['tick-50', 'wait-400']);
  });
});

// ── C. 契約沒變：既有 wait 節點定義不用改 ────────────────────────────────────
//
// 逐條對 registry/components/wait/component.contract.yaml 的 gherkin_tests。
describe('C. I/O 契約與 WASM 版一致（既有 workflow 不必改定義）', () => {
  it('contract gherkin：等待 100ms → waited_ms:100', async () => {
    expect(await wait({ ms: 100 })).toEqual({ success: true, data: { waited_ms: 100 } });
  });

  it('contract gherkin：ms 為 0 時失敗（不是靜靜跳過）', async () => {
    expect(await wait({ ms: 0 })).toEqual({ success: false, error: 'ms 必須大於 0' });
  });

  it('ms 缺漏 / 負數 / 非數字，一律誠實回 success:false，不假裝等過', async () => {
    for (const bad of [undefined, null, -1, 'abc', {}, []]) {
      expect(await wait({ ms: bad })).toEqual({ success: false, error: 'ms 必須大於 0' });
    }
  });

  it('contract gherkin：ms=99999 截斷為上限 30000（不是報錯、也不是真的等 99 秒）', async () => {
    // 不真的等 30 秒：換掉 setTimeout，攔下引擎「要求等多久」再立刻放行。
    const asked: number[] = [];
    vi.stubGlobal('setTimeout', ((fn: () => void, delay?: number) => {
      asked.push(Number(delay));
      fn();
      return 0 as unknown as ReturnType<typeof setTimeout>;
    }) as unknown as typeof setTimeout);

    expect(await wait({ ms: 99999 })).toEqual({ success: true, data: { waited_ms: WAIT_MAX_MS } });
    expect(asked).toEqual([WAIT_MAX_MS]);
    expect(WAIT_MAX_MS).toBe(30000);   // 紅線：上限不准為了閃避資源限制被調小
  });

  it('ms=30000 一路走到底也只是「排一個 30 秒的 timer」，沒有任何同步佔用', async () => {
    const asked: number[] = [];
    vi.stubGlobal('setTimeout', ((fn: () => void, delay?: number) => {
      asked.push(Number(delay));
      fn();
      return 0 as unknown as ReturnType<typeof setTimeout>;
    }) as unknown as typeof setTimeout);

    expect(await wait({ ms: 30000 })).toEqual({ success: true, data: { waited_ms: 30000 } });
    expect(asked).toEqual([30000]);
  });

  it('context 照契約透傳，並補上 waited_ms', async () => {
    const r = await wait({ ms: 5, context: { order_id: 'A-1', payload: { n: 2 } } });
    expect(r).toEqual({
      success: true,
      data: { order_id: 'A-1', payload: { n: 2 }, waited_ms: 5 },
    });
  });

  it('node.data 經 interpolateData 後 ms 會是字串 —— 收得下（WASM 版在這裡直接 unmarshal 失敗）', async () => {
    expect(await wait({ ms: '250' })).toEqual({ success: true, data: { waited_ms: 250 } });
  });
});

// ── D. 路由：不再打 arcrun-wait worker ───────────────────────────────────────
describe('D. component-loader 把 wait 解到內建 runner（step 1），不發任何 fetch', () => {
  it('loader("wait") 跑起來不會對外送出任何請求', async () => {
    const fakeEnv = { ...env, WORKER_SUBDOMAIN: 'test-sub' } as unknown as Bindings;
    const fetchSpy = vi.fn(async () => new Response('{}', { status: 200 }));
    vi.stubGlobal('fetch', fetchSpy);

    const runner = await createComponentLoader(fakeEnv)('wait');
    const r = await runner({ ms: 10 });

    expect(r).toEqual({ success: true, data: { waited_ms: 10 } });
    // 修法前這裡會打 arcrun-wait.test-sub.workers.dev（SVC_WAIT 未綁時的 fallback），
    // 那顆 worker 就是會燒到 1102 的那顆。
    expect(fetchSpy).not.toHaveBeenCalled();
  });

  it('wait 仍在「執行期真的解析得動」的清單裡（/cypher/search 查得到）', async () => {
    const { RUNTIME_NATIVE_COMPONENT_IDS } = await import('../src/lib/component-loader');
    expect(RUNTIME_NATIVE_COMPONENT_IDS.has('wait')).toBe(true);
  });
});
