import { describe, expect, it } from "vitest";
import type {
  RegexExecutionRequest,
  RegexExecutionResult,
} from "../model/match";
import type { RegexFlavourId } from "../model/flavour";
import { EngineSupervisor } from "./EngineSupervisor";
import {
  ENGINE_WORKER_REGISTRY,
  EngineWorkerRegistry,
} from "./engine-registry";
import type { WorkerLike } from "./WorkerSupervisor";
import {
  WORKER_PROTOCOL_VERSION,
  type EngineWorkerOperation,
  type EngineWorkerResult,
  type WorkerRequest,
  type WorkerResponse,
} from "./worker-protocol";

function request(flavour: RegexFlavourId): RegexExecutionRequest {
  return {
    flavour,
    flavourVersion: "fixture",
    pattern: "a",
    flags: [],
    options: {},
    subject: "a",
    captureMetadata: [],
    scanAll: false,
    maximumMatches: 10,
    maximumCaptureRows: 10,
  };
}

function result(flavour: RegexFlavourId): RegexExecutionResult {
  return {
    accepted: true,
    engine: {
      flavour,
      adapterVersion: "fixture",
      engineName: `${flavour} fixture`,
      engineVersion: "fixture",
      offsetUnit: "utf16",
      capabilities: {
        compilation: true,
        matching: true,
        replacement: false,
        namedCaptures: false,
        captureHistory: false,
        actualTrace: false,
        benchmark: false,
      },
    },
    flags: {
      userFlags: "",
      effectiveFlags: "",
      internallyAddedIndicesFlag: false,
      internallyAddedGlobalFlag: false,
    },
    matches: [],
    diagnostics: [],
    elapsedMs: 0,
    truncated: false,
  };
}

class ResponsiveWorker implements WorkerLike {
  onmessage: ((event: MessageEvent<unknown>) => void) | null = null;
  onerror: ((event: ErrorEvent) => void) | null = null;
  onmessageerror: ((event: MessageEvent<unknown>) => void) | null = null;
  readonly operations: EngineWorkerOperation[] = [];
  terminateCalls = 0;
  private readonly flavour: RegexFlavourId;

  constructor(flavour: RegexFlavourId) {
    this.flavour = flavour;
  }

  postMessage(message: unknown): void {
    const request = message as WorkerRequest<EngineWorkerOperation>;
    this.operations.push(request.payload);
    const payload: EngineWorkerResult =
      request.payload.kind === "load"
        ? {
            kind: "load",
            info: result(this.flavour).engine,
          }
        : request.payload.kind === "execute"
          ? {
              kind: "execute",
              result: result(request.payload.request.flavour),
            }
          : {
              kind: "replace",
              result: {
                execution: result(request.payload.request.flavour),
                output: "",
                outputBytes: 0,
                outputTruncated: false,
                truncated: false,
              },
            };
    const response: WorkerResponse<EngineWorkerResult> = {
      protocolVersion: WORKER_PROTOCOL_VERSION,
      requestId: request.requestId,
      generation: request.generation,
      ok: true,
      payload,
    };
    this.onmessage?.({ data: response } as MessageEvent<unknown>);
  }

  terminate(): void {
    this.terminateCalls += 1;
  }
}

class StallingWorker implements WorkerLike {
  onmessage: ((event: MessageEvent<unknown>) => void) | null = null;
  onerror: ((event: ErrorEvent) => void) | null = null;
  onmessageerror: ((event: MessageEvent<unknown>) => void) | null = null;
  readonly operations: EngineWorkerOperation[] = [];
  terminateCalls = 0;

  postMessage(message: unknown): void {
    const request = message as WorkerRequest<EngineWorkerOperation>;
    this.operations.push(request.payload);
  }

  terminate(): void {
    this.terminateCalls += 1;
  }
}

describe("engine worker selection", () => {
  it("registers every shipped execution worker with a startup budget", () => {
    expect(
      ENGINE_WORKER_REGISTRY.registrations.map(
        (registration) => registration.flavour,
      ),
    ).toEqual([
      "ecmascript",
      "pcre2",
      "php",
      "perl",
      "python",
      "ruby",
      "java",
      "cpp",
      "go",
      "dotnet",
      "rust",
      "scala",
    ]);
    expect(ENGINE_WORKER_REGISTRY.require("pcre2").label).toBe("PCRE2 engine");
    expect(ENGINE_WORKER_REGISTRY.require("python").startupTimeoutMs).toBe(
      30_000,
    );
    expect(ENGINE_WORKER_REGISTRY.require("php").startupTimeoutMs).toBe(30_000);
    expect(ENGINE_WORKER_REGISTRY.require("perl").startupTimeoutMs).toBe(
      30_000,
    );
    expect(ENGINE_WORKER_REGISTRY.require("dotnet").startupTimeoutMs).toBe(
      60_000,
    );
  });

  it("selects and reuses a dedicated supervisor per registered flavour", async () => {
    const workers = new Map<RegexFlavourId, ResponsiveWorker[]>();
    const registration = (flavour: RegexFlavourId) => ({
      flavour,
      label: `${flavour} fixture`,
      startupTimeoutMs: 100,
      createWorker: () => {
        const worker = new ResponsiveWorker(flavour);
        workers.set(flavour, [...(workers.get(flavour) ?? []), worker]);
        return worker;
      },
    });
    const supervisor = new EngineSupervisor(
      new EngineWorkerRegistry([
        registration("ecmascript"),
        registration("pcre2"),
      ]),
    );

    await expect(
      supervisor.execute(request("ecmascript"), 100),
    ).resolves.toEqual(
      expect.objectContaining({
        engine: expect.objectContaining({ flavour: "ecmascript" }),
      }),
    );
    await expect(supervisor.execute(request("pcre2"), 100)).resolves.toEqual(
      expect.objectContaining({
        engine: expect.objectContaining({ flavour: "pcre2" }),
      }),
    );
    await supervisor.execute(request("ecmascript"), 100);

    expect(workers.get("ecmascript")).toHaveLength(1);
    expect(workers.get("pcre2")).toHaveLength(1);
    expect(
      workers
        .get("ecmascript")?.[0]
        ?.operations.map((operation) => operation.kind),
    ).toEqual(["load", "execute", "execute"]);
    expect(
      workers.get("pcre2")?.[0]?.operations.map((operation) => operation.kind),
    ).toEqual(["load", "execute"]);

    supervisor.dispose();
    expect(workers.get("ecmascript")?.[0]?.terminateCalls).toBe(1);
    expect(workers.get("pcre2")?.[0]?.terminateCalls).toBe(1);
  });

  it("evicts the least-recently-used idle runtime at the retention limit", async () => {
    const workers = new Map<RegexFlavourId, ResponsiveWorker[]>();
    const registration = (flavour: RegexFlavourId) => ({
      flavour,
      label: `${flavour} fixture`,
      startupTimeoutMs: 100,
      createWorker: () => {
        const worker = new ResponsiveWorker(flavour);
        workers.set(flavour, [...(workers.get(flavour) ?? []), worker]);
        return worker;
      },
    });
    const supervisor = new EngineSupervisor(
      new EngineWorkerRegistry([
        registration("ecmascript"),
        registration("pcre2"),
        registration("python"),
      ]),
      2,
    );

    await supervisor.execute(request("ecmascript"), 100);
    await supervisor.execute(request("pcre2"), 100);
    await supervisor.execute(request("ecmascript"), 100);
    await supervisor.execute(request("python"), 100);

    expect(workers.get("ecmascript")?.[0]?.terminateCalls).toBe(0);
    expect(workers.get("pcre2")?.[0]?.terminateCalls).toBe(1);
    expect(workers.get("python")).toHaveLength(1);

    await supervisor.execute(request("pcre2"), 100);
    expect(workers.get("pcre2")).toHaveLength(2);
    expect(workers.get("ecmascript")?.[0]?.terminateCalls).toBe(1);

    supervisor.dispose();
  });

  it("fails closed before constructing a worker for an unavailable flavour", async () => {
    const supervisor = new EngineSupervisor(new EngineWorkerRegistry([]));

    await expect(supervisor.execute(request("pcre2"), 100)).rejects.toThrow(
      /No execution worker is registered/u,
    );
    supervisor.dispose();
  });

  it("lets the newest request supersede a shared ready-engine continuation", async () => {
    const worker = new ResponsiveWorker("python");
    const supervisor = new EngineSupervisor(
      new EngineWorkerRegistry([
        {
          flavour: "python",
          label: "Python fixture",
          startupTimeoutMs: 100,
          createWorker: () => worker,
        },
      ]),
    );
    await supervisor.load("python");

    const older = supervisor.execute(request("python"), 100);
    const newer = supervisor.execute(request("python"), 100);

    await expect(older).rejects.toMatchObject({ kind: "cancelled" });
    await expect(newer).resolves.toEqual(
      expect.objectContaining({
        engine: expect.objectContaining({ flavour: "python" }),
      }),
    );
    expect(worker.operations.map((operation) => operation.kind)).toEqual([
      "load",
      "execute",
    ]);
    supervisor.dispose();
  });

  it("restarts a superseded cold runtime under the startup budget before execution", async () => {
    const coldWorker = new StallingWorker();
    const readyWorker = new ResponsiveWorker("python");
    let factoryCalls = 0;
    const supervisor = new EngineSupervisor(
      new EngineWorkerRegistry([
        {
          flavour: "python",
          label: "Python fixture",
          startupTimeoutMs: 100,
          createWorker: () => {
            factoryCalls += 1;
            return factoryCalls === 1 ? coldWorker : readyWorker;
          },
        },
      ]),
    );

    const older = supervisor.execute(request("python"), 1);
    const newer = supervisor.execute(request("python"), 1);

    await expect(older).rejects.toMatchObject({ kind: "cancelled" });
    await expect(newer).resolves.toEqual(
      expect.objectContaining({
        engine: expect.objectContaining({ flavour: "python" }),
      }),
    );
    expect(coldWorker.operations.map((operation) => operation.kind)).toEqual([
      "load",
    ]);
    expect(coldWorker.terminateCalls).toBe(1);
    expect(readyWorker.operations.map((operation) => operation.kind)).toEqual([
      "load",
      "execute",
    ]);
    supervisor.dispose();
  });
});
