import { afterEach, describe, expect, it, vi } from "vitest";
import {
  NO_FILE_CONTEXT,
  StandardResourceCatalog,
} from "../src/v3/resource-catalog/standardResourceCatalog.js";

// Regression tests for COULD_NOT_FIND_EXECUTOR on warm worker processes when
// a task's `task()` / `schemaTask()` call is evaluated during another task's
// execution (e.g. as a side effect of `await import(...)` of a module that
// contains a task definition).
//
// Production throw site:
//   - managed-run-worker.ts:566 (post-wrap)
//   - dev-run-worker.ts:578 (post-wrap)
// Pre-fix symptom: `resourceCatalog.getTask(execution.task.id)` returned
// undefined even after the worker re-imported the task entrypoint.
//
// Pre-fix mechanism: `registerTaskMetadata` silently returned when
// `_currentFileContext` was unset. Any `task()` call firing during a
// running task's run() / lifecycle hooks (directly, or transitively via a
// dynamic import) hit the silent guard. Node's ESM module cache then
// prevented recovery — the worker's setContext + re-import fallback didn't
// re-evaluate the module body, so the `task()` call never fired again.
//
// Post-fix: the runtime workers wrap their `executor.execute(...)` call with
// `setCurrentFileContext(NO_FILE_CONTEXT, NO_FILE_CONTEXT)` so any `task()`
// call firing during execution registers normally with sentinel file
// metadata. The catalog detects the sentinel and emits a one-time warning
// per task id to keep the bundle-shape pattern visible. The indexer never
// sets this sentinel context — its behavior is unchanged.

describe("StandardResourceCatalog — runtime registration via sentinel context", () => {
  afterEach(() => {
    delete (globalThis as { __catalogRegisterTaskMetadata?: unknown })
      .__catalogRegisterTaskMetadata;
    vi.restoreAllMocks();
  });

  it("silently drops registration when no context is set (indexer's invariant)", () => {
    const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
    const catalog = new StandardResourceCatalog();

    catalog.registerTaskMetadata({
      id: "no-context-task",
      fns: { run: async () => "ok" },
    });

    expect(catalog.getTask("no-context-task")).toBeUndefined();
    expect(warn).not.toHaveBeenCalled();
  });

  it(
    "registers normally and warns once when the sentinel context is set " +
      "(simulates the worker's executor wrap)",
    () => {
      const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
      const catalog = new StandardResourceCatalog();

      catalog.setCurrentFileContext(NO_FILE_CONTEXT, NO_FILE_CONTEXT);
      catalog.registerTaskMetadata({
        id: "lazy-task",
        fns: { run: async () => "ok" },
      });
      catalog.clearCurrentFileContext();

      const registered = catalog.getTask("lazy-task");
      expect(registered).toBeDefined();
      expect(registered?.id).toBe("lazy-task");
      expect(registered?.filePath).toBe(NO_FILE_CONTEXT);
      expect(registered?.entryPoint).toBe(NO_FILE_CONTEXT);
      expect(warn).toHaveBeenCalledTimes(1);
      expect(warn.mock.calls[0]?.[0]).toContain("lazy-task");
    }
  );

  it(
    "warm-start path: a task whose top-level definition fires during a " +
      "dynamic import inside the sentinel wrap remains findable; the " +
      "worker's setContext + re-import fallback (managed-run-worker.ts:482) " +
      "is not needed",
    async () => {
      vi.spyOn(console, "warn").mockImplementation(() => {});
      const catalog = new StandardResourceCatalog();

      (globalThis as { __catalogRegisterTaskMetadata?: unknown }).__catalogRegisterTaskMetadata = (
        task: Parameters<StandardResourceCatalog["registerTaskMetadata"]>[0]
      ) => {
        catalog.registerTaskMetadata(task);
      };

      // Simulate the worker wrap: setContext(NO_FILE_CONTEXT) → run user code
      // (which does a dynamic import) → clearContext.
      catalog.setCurrentFileContext(NO_FILE_CONTEXT, NO_FILE_CONTEXT);
      await import("./fixtures/dynamic-task-module.mjs");
      catalog.clearCurrentFileContext();

      const registered = catalog.getTask("lazy-task");
      expect(registered).toBeDefined();
      expect(registered?.filePath).toBe(NO_FILE_CONTEXT);
    }
  );

  it("warns at most once per task id under the sentinel context", () => {
    const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
    const catalog = new StandardResourceCatalog();

    catalog.setCurrentFileContext(NO_FILE_CONTEXT, NO_FILE_CONTEXT);

    const register = (id: string) =>
      catalog.registerTaskMetadata({
        id,
        fns: { run: async () => "ok" },
      });

    register("task-a");
    register("task-a");
    register("task-a");
    expect(warn).toHaveBeenCalledTimes(1);

    register("task-b");
    expect(warn).toHaveBeenCalledTimes(2);

    catalog.clearCurrentFileContext();
  });

  it("control: real file context registers without firing the sentinel warning", async () => {
    const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
    const catalog = new StandardResourceCatalog();

    (globalThis as { __catalogRegisterTaskMetadata?: unknown }).__catalogRegisterTaskMetadata = (
      task: Parameters<StandardResourceCatalog["registerTaskMetadata"]>[0]
    ) => {
      catalog.registerTaskMetadata(task);
    };

    catalog.setCurrentFileContext("/app/dist/lazy-task.entry.mjs", "src/tasks/lazy-task.ts");
    await import("./fixtures/dynamic-task-module.mjs?control");
    catalog.clearCurrentFileContext();

    const task = catalog.getTask("lazy-task");
    expect(task).toBeDefined();
    expect(task?.filePath).toBe("/app/dist/lazy-task.entry.mjs");
    expect(task?.entryPoint).toBe("src/tasks/lazy-task.ts");
    expect(warn).not.toHaveBeenCalled();
  });
});

describe("StandardResourceCatalog — duplicate task id collisions", () => {
  function register(catalog: StandardResourceCatalog, id: string, filePath: string) {
    catalog.setCurrentFileContext(filePath, filePath);
    catalog.registerTaskMetadata({ id, fns: { run: async () => "ok" } });
    catalog.clearCurrentFileContext();
  }

  it("reports no collisions when every task id is unique", () => {
    const catalog = new StandardResourceCatalog();

    register(catalog, "a", "src/a.ts");
    register(catalog, "b", "src/b.ts");

    expect(catalog.listTaskIdCollisions()).toEqual([]);
  });

  it("records a collision with both file paths when an id is reused across files", () => {
    const catalog = new StandardResourceCatalog();

    register(catalog, "dupe", "src/a.ts");
    register(catalog, "dupe", "src/b.ts");

    expect(catalog.listTaskIdCollisions()).toEqual([
      { id: "dupe", filePaths: ["src/a.ts", "src/b.ts"] },
    ]);
  });

  it("collects every distinct file path when an id is defined three or more times", () => {
    const catalog = new StandardResourceCatalog();

    register(catalog, "dupe", "src/a.ts");
    register(catalog, "dupe", "src/b.ts");
    register(catalog, "dupe", "src/c.ts");

    expect(catalog.listTaskIdCollisions()).toEqual([
      { id: "dupe", filePaths: ["src/a.ts", "src/b.ts", "src/c.ts"] },
    ]);
  });

  it("records two definitions in the same file (e.g. two exports sharing an id)", () => {
    const catalog = new StandardResourceCatalog();

    register(catalog, "dupe", "src/dupe.ts");
    register(catalog, "dupe", "src/dupe.ts");

    expect(catalog.listTaskIdCollisions()).toEqual([
      { id: "dupe", filePaths: ["src/dupe.ts", "src/dupe.ts"] },
    ]);
  });
});
