import test from "node:test";
import assert from "node:assert/strict";
import * as rendererModule from "../static/renderer-client.js";
const { RendererClient, RendererError } = rendererModule;
const TYPE = [0,1,2,4,8,16,32,64,128,256,512,1024,2048,4096,0x80000000,0xffffffff];

class WorkerMock extends EventTarget {
  messages=[]; transfers=[]; terminated=false;
  postMessage(message, transfer=[]) { this.messages.push(message); this.transfers.push(transfer); }
  terminate(){this.terminated=true;}
  reply(data) { this.dispatchEvent(new MessageEvent("message", { data })); }
}
function fixture() {
  const memory = new WebAssembly.Memory({initial:4, maximum:8, shared:true});
  const header = new Int32Array(memory.buffer, 0, 16);
  header.set([0x4e574159,2,1024,40,0,0]);
  const worker = new WorkerMock();
  const bridge = {memory,ringPtr:0,worker,freed:false,free(){this.freed=true;}};
  const client = new RendererClient(bridge);
  return {memory,header,worker,bridge,client};
}
async function imported(f) {
  const loading=f.client.replaceSceneGlb(new ArrayBuffer(8));
  f.worker.reply({type:"payload-ready",id:1});
  await Promise.resolve();
  f.worker.reply({type:"reply",request:1,ok:true,result:{meshes:[{handle:[7,3],defaultInstance:[8,5],defaultType:[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}]}});
  return (await loading)[0];
}
test("replaceSceneGlb is opcode 1", async()=>{const f=fixture(),pending=f.client.replaceSceneGlb(new ArrayBuffer(8));f.worker.reply({type:"payload-ready",id:1});await Promise.resolve();assert.equal(new Int32Array(f.memory.buffer,64,40)[1],1);f.worker.reply({type:"reply",request:1,ok:true,result:{meshes:[]}});assert.deepEqual(await pending,[]);});
test("scene replacement carries the framing mode in opcode 1",async()=>{const f=fixture(),pending=f.client.replaceSceneGlb(new ArrayBuffer(8),{framing:"interior"});f.worker.reply({type:"payload-ready",id:1});await Promise.resolve();assert.deepEqual([...new Int32Array(f.memory.buffer,64,40).slice(1,5)],[1,1,1,1]);f.worker.reply({type:"reply",request:1,ok:true,result:{meshes:[]}});await pending;await assert.rejects(f.client.replaceSceneGlb(new ArrayBuffer(8),{framing:"bad"}),TypeError)});
test("writes tagged fixed-slot protocol and resolves reply", async () => {
  const f=fixture(); const mesh=await imported(f);
  assert.equal(mesh.setVisible,undefined);
  assert.equal(mesh.setType,undefined);
  assert.equal(typeof mesh.defaultInstance.setType,"function");
  const pending=mesh.defaultInstance.setType(TYPE);
  const {memory,header,worker}=f;
  assert.equal(Atomics.load(header,5),2);
  const slot=new Int32Array(memory.buffer,64+160,40);
  assert.deepEqual([...slot.slice(0,21)].map(x=>x>>>0),[2,10,2,8,5,...TYPE]);
  worker.reply({type:"reply",request:2,ok:true,code:"OK"}); await pending;
});
test("maps stable errors and gates destroyed instances", async () => {
  const f=fixture(), mesh=await imported(f); const {worker}=f;
  const creating=mesh.createInstance(new Float32Array([1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]),{type:TYPE});
  worker.reply({type:"reply",request:2,ok:true,result:[4,2]}); const instance=await creating;
  assert.equal(instance.setVisible,undefined);
  const destroying=instance.destroy(); worker.reply({type:"reply",request:3,ok:true}); await destroying;
  assert.throws(()=>instance.setType(TYPE), error=>error instanceof RendererError&&error.code==="STALE_HANDLE");
});
test("rejects protocol mismatch", () => {
  const {memory,worker}=fixture(); new Int32Array(memory.buffer)[1]=1;
  assert.throws(()=>new RendererClient({memory,ringPtr:0,worker}), /PROTOCOL_MISMATCH/);
});
test("pending reply exists before ring publication", async () => {
  const f=fixture(), mesh=await imported(f); const {worker}=f;
  const pending=mesh.defaultInstance.setType(TYPE);
  worker.reply({type:"reply",request:2,ok:true});
  await pending;
});
test("profile snapshots have a dedicated getter", () => {
  const f=fixture(), snapshot={type:"profile-snapshot",available:true,epoch:3,passes:{forward:1.25}};
  const dispatch=globalThis.dispatchEvent; globalThis.dispatchEvent=()=>true;
  try { f.worker.reply(snapshot); assert.strictEqual(f.client.profile,snapshot); }
  finally { globalThis.dispatchEvent=dispatch; f.client.dispose(); }
});
test("worker failures and dispose reject every pending operation", async () => {
  const f=fixture(), mesh=await imported(f); const {worker,client,bridge}=f;
  const a=mesh.defaultInstance.setType(TYPE), b=mesh.defaultInstance.setType([...TYPE].reverse());
  worker.dispatchEvent(new Event("error"));
  await assert.rejects(a,/WORKER_ERROR/); await assert.rejects(b,/WORKER_ERROR/);
  client.dispose(); assert.equal(worker.terminated,true); assert.equal(bridge.freed,true);
});
test("import always releases staged payload when ring is full", async () => {
  const {header,worker,client}=fixture(); Atomics.store(header,5,1024);
  const loading=client.replaceSceneGlb(new ArrayBuffer(8));
  worker.reply({type:"payload-ready",id:1});
  await assert.rejects(loading,/RING_FULL/);
  assert.equal(worker.messages.at(-1).type,"payload-release");
});
test("does not export handle constructors or internal mutation methods", () => {
  const {client}=fixture();
  assert.equal(rendererModule.VISIBLE,undefined);
  assert.equal(rendererModule.Mesh,undefined);
  assert.equal(rendererModule.Instance,undefined);
  assert.equal(client._meshFlags,undefined);
  assert.equal(client._createInstance,undefined);
});
test("corrupt backlog closes and terminates the transport", async () => {
  const f=fixture(); Atomics.store(f.header,5,1025);
  const loading=f.client.replaceSceneGlb(new ArrayBuffer(8));
  // Payload staging must first acknowledge before enqueue sees corruption.
  f.worker.reply({type:"payload-ready",id:1});
  await assert.rejects(loading,/RING_CORRUPT/);
  assert.equal(Atomics.load(f.header,6),1);
  assert.equal(f.worker.terminated,true);
});
test("import rejects immediately after disposal", async () => {
  const f=fixture();
  f.client.dispose();
  await assert.rejects(f.client.replaceSceneGlb(new ArrayBuffer(8)),/DISPOSED/);
  assert.equal(f.worker.messages.length,0);
});
test("import rejects when disposed during asynchronous source loading", async () => {
  const f=fixture();
  const originalFetch=globalThis.fetch;
  let finishFetch;
  globalThis.fetch=()=>new Promise(resolve=>{finishFetch=resolve;});
  try {
    const loading=f.client.replaceSceneGlb("model.glb");
    f.client.dispose();
    finishFetch({arrayBuffer:async()=>new ArrayBuffer(8)});
    await assert.rejects(loading,/DISPOSED/);
    assert.equal(f.worker.messages.length,0);
  } finally {
    globalThis.fetch=originalFetch;
  }
});

test("compile transfers payload and waits for ready before opcode 7", async()=>{
  const f=fixture(), pending=f.client.compileGraph({schemaVersion:2});
  assert.equal(f.worker.transfers[0].length,1); assert.equal(Atomics.load(f.header,5),0);
  f.worker.reply({type:"payload-ready",id:1}); await Promise.resolve();
  assert.equal(new Int32Array(f.memory.buffer,64,40)[1],7);
  f.worker.reply({type:"reply",request:1,ok:true,result:{compiledId:[2,3]}});
  assert.deepEqual(await pending,{compiledId:[2,3]});
});
test("flat error reply preserves structured details", async()=>{
  const f=fixture(), pending=f.client.compileGraph({}); f.worker.reply({type:"payload-ready",id:1}); await Promise.resolve();
  f.worker.reply({type:"reply",request:1,ok:false,code:"GRAPH_INVALID_ID",details:{message:"bad",path:"graphId"}});
  await assert.rejects(pending,e=>e instanceof RendererError&&e.code==="GRAPH_INVALID_ID"&&e.details.path==="graphId"&&e.message==="bad");
});
test("compile releases payload after success", async()=>{const f=fixture(),p=f.client.compileGraph({});f.worker.reply({type:"payload-ready",id:1});await Promise.resolve();f.worker.reply({type:"reply",request:1,ok:true,result:{}});await p;assert.equal(f.worker.messages.at(-1).type,"payload-release");});
test("compile releases payload after backend error", async()=>{const f=fixture(),p=f.client.compileGraph({});f.worker.reply({type:"payload-ready",id:1});await Promise.resolve();f.worker.reply({type:"reply",request:1,ok:false,code:"X",details:{message:"x"}});await assert.rejects(p);assert.equal(f.worker.messages.at(-1).type,"payload-release");});
test("compile rejects circular and BigInt JSON", async()=>{const f=fixture(),x={};x.x=x;await assert.rejects(f.client.compileGraph(x),/circular/i);await assert.rejects(f.client.compileGraph({x:1n}),e=>e.code==="GRAPH_JSON_INVALID");});
test("compile rejects oversized encoding", async()=>{const f=fixture();await assert.rejects(f.client.compileGraph({x:"x".repeat(1024*1024)}),e=>e.code==="GRAPH_PAYLOAD_TOO_LARGE");assert.equal(f.worker.messages.length,0);});
test("drop graph emits opcode 8 and validates id", async()=>{const f=fixture(),p=f.client.dropCompiledGraph([9,4]);const slot=new Int32Array(f.memory.buffer,64,40);assert.deepEqual([...slot.slice(1,5)],[8,1,9,4]);f.worker.reply({type:"reply",request:1,ok:true});await p;assert.throws(()=>f.client.dropCompiledGraph([1]),TypeError);});
test("ring-full graph compile releases staged payload", async()=>{const f=fixture();Atomics.store(f.header,5,1024);const p=f.client.compileGraph({});f.worker.reply({type:"payload-ready",id:1});await assert.rejects(p,e=>e.code==="RING_FULL");assert.equal(f.worker.messages.at(-1).type,"payload-release");});
test("disposal while graph payload is pending releases and rejects", async()=>{const f=fixture(),p=f.client.compileGraph({});f.client.dispose();await assert.rejects(p,e=>e.code==="DISPOSED");assert.equal(f.worker.messages.at(-1).type,"payload-release");});
test("payload transfer uses the exact encoded ArrayBuffer", async()=>{const f=fixture(),graph={schemaVersion:2};const expected=new TextEncoder().encode(JSON.stringify(graph));const p=f.client.compileGraph(graph);const transferred=f.worker.transfers[0][0];assert.strictEqual(f.worker.messages[0].buffer,transferred);assert.deepEqual(new Uint8Array(transferred),expected);f.client.dispose();await assert.rejects(p);});
test("cycle error details are preserved exactly", async()=>{const f=fixture(),p=f.client.compileGraph({});f.worker.reply({type:"payload-ready",id:1});await Promise.resolve();const details={message:"cycle",kind:"cycle",edges:[{from:"a",resource:{id:"r",version:0},to:"b"}]};f.worker.reply({type:"reply",request:1,ok:false,code:"GRAPH_CYCLE",details});await assert.rejects(p,e=>e.details===details&&e.details.edges[0].from==="a");});
test("error without details leaves details undefined", async()=>{const f=fixture(),p=f.client.dropCompiledGraph([1,1]);f.worker.reply({type:"reply",request:1,ok:false,code:"STALE_GRAPH_ID"});await assert.rejects(p,e=>e instanceof RendererError&&e.details===undefined&&e.message==="STALE_GRAPH_ID");});
test("switch methods emit exact opcode 9 words", async()=>{const f=fixture();const a=f.client.switchCompiledGraph([9,4]);assert.deepEqual([...new Int32Array(f.memory.buffer,64,40).slice(1,7)],[9,1,1,9,4,0]);f.worker.reply({type:"reply",request:1,ok:true});await a;await new Promise(queueMicrotask);const b=f.client.switchToImmediate();assert.deepEqual([...new Int32Array(f.memory.buffer,224,40).slice(1,7)],[9,2,0,0,0,0]);f.worker.reply({type:"reply",request:2,ok:true});await b;assert.throws(()=>f.client.switchCompiledGraph([0,0]),TypeError);});
test("graph lifecycle FIFO recovers after failure", async()=>{const f=fixture();const a=f.client.switchCompiledGraph([1,1]),b=f.client.switchToImmediate();assert.equal(Atomics.load(f.header,5),1);f.worker.reply({type:"reply",request:1,ok:false,code:"X"});await assert.rejects(a);await new Promise(queueMicrotask);assert.equal(Atomics.load(f.header,5),2);f.worker.reply({type:"reply",request:2,ok:true});await b;});
test("dispose rejects queued graph lifecycle calls", async()=>{const f=fixture();const a=f.client.switchCompiledGraph([1,1]),b=f.client.switchToImmediate();f.client.dispose();await assert.rejects(a,/DISPOSED/);await assert.rejects(b,/DISPOSED/);});
