import { describe, expect, it } from "vitest";
import { KdlWorkerClient, type KdlWorkerLike } from "../../src/kdl/kdlClient.js";
import type { NativeKdlModule } from "../../src/kdl/nativeModule.js";
import { KdlStructuredError, type KdlRpcRequest, type KdlRpcResponse } from "../../src/kdl/rpc.js";
import { createKdlWorkerRuntime } from "../../src/kdl/runtime.js";
import { dispatchKdlRpcRequest } from "../../src/kdl/workerRpc.js";

class FakeWorker implements KdlWorkerLike {
  readonly sent: Array<KdlRpcRequest<unknown[]>> = [];
  terminated = false;
  private messageListeners = new Set<(event: MessageEvent<KdlRpcResponse>) => void>();
  private errorListeners = new Set<(event: ErrorEvent) => void>();

  postMessage(message: KdlRpcRequest<unknown[]>): void {
    this.sent.push(message);
  }

  terminate(): void {
    this.terminated = true;
  }

  addEventListener(type: "message", listener: (event: MessageEvent<KdlRpcResponse>) => void): void;
  addEventListener(type: "error", listener: (event: ErrorEvent) => void): void;
  addEventListener(type: "message" | "error", listener: unknown): void {
    if (type === "message") {
      this.messageListeners.add(listener as (event: MessageEvent<KdlRpcResponse>) => void);
      return;
    }
    this.errorListeners.add(listener as (event: ErrorEvent) => void);
  }

  removeEventListener(type: "message", listener: (event: MessageEvent<KdlRpcResponse>) => void): void;
  removeEventListener(type: "error", listener: (event: ErrorEvent) => void): void;
  removeEventListener(type: "message" | "error", listener: unknown): void {
    if (type === "message") {
      this.messageListeners.delete(listener as (event: MessageEvent<KdlRpcResponse>) => void);
      return;
    }
    this.errorListeners.delete(listener as (event: ErrorEvent) => void);
  }

  emitResponse(response: KdlRpcResponse): void {
    const event = { data: response } as MessageEvent<KdlRpcResponse>;
    for (const listener of this.messageListeners) {
      listener(event);
    }
  }

  emitError(message: string): void {
    const event = { message, error: new Error(message) } as ErrorEvent;
    for (const listener of this.errorListeners) {
      listener(event);
    }
  }
}

describe("KDL Worker RPC", () => {
  it("loads the native WASM module during init when a loader is configured", async () => {
    const calls: unknown[][] = [];
    const native: NativeKdlModule = {
      ccall: (...args) => {
        calls.push(args);
        return 0;
      }
    };
    const runtime = createKdlWorkerRuntime(async () => native);

    const response = await dispatchKdlRpcRequest(runtime, {
      id: 1,
      method: "init",
      payload: [{ wasmBuild: "native-test" }]
    });

    expect(response.ok).toBe(true);
    expect(response.result).toMatchObject({ wasmBuild: "native-test" });
    expect(calls).toEqual([["kdl_init", "number", ["string"], ['{"wasmBuild":"native-test"}']]]);
  });

  it("normalizes native WASM initialization failures", async () => {
    const runtime = createKdlWorkerRuntime(async () => {
      throw new Error("cannot load kdl.js");
    });

    const response = await dispatchKdlRpcRequest(runtime, {
      id: 11,
      method: "init",
      payload: [{}]
    });

    expect(response.ok).toBe(false);
    expect(response.error).toMatchObject({
      code: "KDL_WASM_INIT_FAILED",
      diagnostics: [
        {
          severity: "error",
          code: "KDL_WASM_INIT_FAILED"
        }
      ]
    });
  });

  it("dispatches init and dispose through structured responses", async () => {
    const runtime = createKdlWorkerRuntime();
    const initResponse = await dispatchKdlRpcRequest(runtime, {
      id: 1,
      method: "init",
      payload: [{ wasmBuild: "test", useThreads: true }]
    });

    expect(initResponse.ok).toBe(true);
    expect(initResponse.result).toMatchObject({
      version: "0.1.0",
      wasmBuild: "test",
      supportsThreads: true
    });

    const disposeResponse = await dispatchKdlRpcRequest(runtime, {
      id: 2,
      method: "dispose",
      payload: []
    });

    expect(disposeResponse).toMatchObject({ id: 2, ok: true });
  });

  it("returns a structured error when an implemented method is called before init", async () => {
    const response = await dispatchKdlRpcRequest(createKdlWorkerRuntime(), {
      id: 7,
      method: "fk",
      payload: [1, new Float64Array([0])]
    });

    expect(response.ok).toBe(false);
    expect(response.error).toMatchObject({
      code: "KDL_NOT_INITIALIZED",
      diagnostics: [
        {
          severity: "error",
          code: "KDL_NOT_INITIALIZED"
        }
      ]
    });
  });

  it("uses unique request ids and resolves responses by id", async () => {
    const workers: FakeWorker[] = [];
    const client = new KdlWorkerClient(() => {
      const worker = new FakeWorker();
      workers.push(worker);
      return worker;
    });

    const first = client.call("init", { wasmBuild: "a" });
    const second = client.call("dispose");

    expect(workers).toHaveLength(1);
    expect(workers[0]?.sent.map((request) => request.id)).toEqual([1, 2]);

    workers[0]?.emitResponse({ id: 2, ok: true });
    workers[0]?.emitResponse({
      id: 1,
      ok: true,
      result: {
        version: "0.1.0",
        wasmBuild: "a",
        supportsThreads: false,
        supportsWasmFs: false
      }
    });

    await expect(second).resolves.toBeUndefined();
    await expect(first).resolves.toMatchObject({ wasmBuild: "a" });
  });

  it("rejects pending requests on worker failure and can create a fresh worker", async () => {
    const workers: FakeWorker[] = [];
    const client = new KdlWorkerClient(() => {
      const worker = new FakeWorker();
      workers.push(worker);
      return worker;
    });

    const pending = client.init();
    workers[0]?.emitError("boom");

    await expect(pending).rejects.toMatchObject({
      code: "KDL_WORKER_CRASHED"
    });
    expect(workers[0]?.terminated).toBe(true);

    const restarted = client.init({ wasmBuild: "restart" });
    expect(workers).toHaveLength(2);
    workers[1]?.emitResponse({
      id: 2,
      ok: true,
      result: {
        version: "0.1.0",
        wasmBuild: "restart",
        supportsThreads: false,
        supportsWasmFs: false
      }
    });

    await expect(restarted).resolves.toMatchObject({ wasmBuild: "restart" });
  });
});
