import { beforeEach, describe, expect, it, vi } from "vitest";

const mocks = vi.hoisted(() => ({
  bootstrapV3: vi.fn(),
  enqueueRun: vi.fn(),
}));

vi.mock("@/entrypoints/background/record-replay-v3/bootstrap", () => ({
  bootstrapV3: mocks.bootstrapV3,
}));

vi.mock(
  "@/entrypoints/background/record-replay-v3/engine/queue/enqueue-run",
  () => ({
    enqueueRun: mocks.enqueueRun,
  }),
);

import {
  FlowRevisionConflictError,
  buildCompatRunResult,
  enqueueRunAndWait,
  importFlowsToV3,
  saveFlowToV3,
} from "@/entrypoints/background/record-replay-v3/compat";
import { RR_ERROR_CODES } from "@/entrypoints/background/record-replay-v3/domain/errors";
import { normalizeFlowOptionalFields } from "@/entrypoints/background/record-replay-v3/flows/normalize-flow-optional-fields";
import { calculateWorkflowRevision } from "@/entrypoints/background/record-replay-v3/flows/publish";

function asMock(fn: unknown): ReturnType<typeof vi.fn> {
  return fn as ReturnType<typeof vi.fn>;
}

function createRuntime() {
  const flows = new Map<string, any>();
  return {
    storage: {
      flows: {
        get: vi.fn(async (id: string) => flows.get(id) ?? null),
        save: vi.fn(async (flow: any) => {
          flows.set(flow.id, flow);
        }),
        list: vi.fn(async () => Array.from(flows.values())),
      },
      runs: {
        get: vi.fn().mockResolvedValue({
          id: "run-1",
          status: "succeeded",
          tookMs: 4,
          outputs: null,
        }),
      },
      events: {
        list: vi.fn().mockResolvedValue([]),
      },
    },
    events: {},
    scheduler: {},
  } as any;
}

describe("record-replay-v3 compat", () => {
  beforeEach(() => {
    vi.clearAllMocks();
    mocks.bootstrapV3.mockResolvedValue(createRuntime());
    mocks.enqueueRun.mockResolvedValue({ runId: "run-1" });
    if (typeof (chrome.tabs as any).query !== "function") {
      (chrome.tabs as any).query = vi.fn();
    }
    if (typeof (chrome.tabs as any).get !== "function") {
      (chrome.tabs as any).get = vi.fn();
    }
    if (typeof (chrome.tabs as any).create !== "function") {
      (chrome.tabs as any).create = vi.fn();
    }
    if (typeof (chrome.tabs as any).update !== "function") {
      (chrome.tabs as any).update = vi.fn();
    }
    if (typeof (chrome.tabs as any).reload !== "function") {
      (chrome.tabs as any).reload = vi.fn();
    }
    asMock(chrome.tabs.query).mockReset();
    asMock(chrome.tabs.get).mockReset();
    asMock(chrome.tabs.create).mockReset();
    asMock(chrome.tabs.update).mockReset();
    asMock(chrome.tabs.reload).mockReset();
  });

  it("binds runs to an existing web tab by default instead of creating a blank ephemeral tab", async () => {
    const extensionTab = {
      id: 10,
      url: "chrome-extension://abc123/popup.html",
      active: true,
      status: "complete",
      windowId: 1,
    };
    const webTab = {
      id: 22,
      url: "https://example.com/dashboard",
      active: false,
      status: "complete",
      windowId: 1,
    };

    asMock(chrome.tabs.query).mockImplementation(async (query) => {
      if (query?.active) {
        return [extensionTab];
      }
      return [extensionTab, webTab];
    });
    asMock(chrome.tabs.update).mockResolvedValue({ ...webTab, active: true });

    await enqueueRunAndWait({
      flowId: "flow-1" as any,
    });

    expect(chrome.tabs.update).toHaveBeenCalledWith(22, { active: true });
    expect(chrome.tabs.create).not.toHaveBeenCalled();
    expect(mocks.enqueueRun).toHaveBeenCalledWith(
      expect.anything(),
      expect.objectContaining({
        flowId: "flow-1",
        tabId: 22,
      }),
    );
  });

  it("preserves current quality oracle and audit enum values during normalization", () => {
    const normalized = normalizeFlowOptionalFields(
      {
        meta: {
          quality: {
            verification: {
              oracle: "externalReadback",
              oracleStrength: "strong",
            },
          },
          audit: {
            events: [
              {
                id: "audit-quality-skip",
                kind: "quality_run_skipped",
                actor: "runtime",
                ts: "2026-01-01T00:00:00.000Z",
                flowId: "flow-1",
                runId: "run-1",
                reason: "workflow_revision_changed",
              },
            ],
          },
        },
      },
      "Flow with quality metadata",
      new Set(["node-1"]),
    );

    expect(normalized.meta?.quality?.verification?.oracle).toBe("externalReadback");
    expect(normalized.meta?.audit?.events?.[0]).toMatchObject({
      kind: "quality_run_skipped",
      actor: "runtime",
      runId: "run-1",
    });
  });

  it("creates a new tab for explicit new-tab execution before enqueueing the run", async () => {
    const activeTab = {
      id: 15,
      url: "https://example.com/current",
      active: true,
      status: "complete",
      windowId: 1,
    };
    const createdTab = {
      id: 99,
      url: "https://example.com/checkout",
      active: true,
      status: "complete",
      windowId: 1,
    };

    asMock(chrome.tabs.query).mockImplementation(async () => [activeTab]);
    asMock(chrome.tabs.create).mockResolvedValue(createdTab);
    asMock(chrome.tabs.get).mockImplementation(async (tabId: number) => {
      if (tabId === 99) {
        return createdTab;
      }
      if (tabId === 15) {
        return activeTab;
      }
      throw new Error(`Unknown tab ${tabId}`);
    });

    await enqueueRunAndWait({
      flowId: "flow-2" as any,
      tabTarget: "new",
      startUrl: "https://example.com/checkout",
    });

    expect(chrome.tabs.create).toHaveBeenCalledWith({
      active: true,
      url: "https://example.com/checkout",
    });
    expect(mocks.enqueueRun).toHaveBeenCalledWith(
      expect.anything(),
      expect.objectContaining({
        flowId: "flow-2",
        tabId: 99,
      }),
    );
  });

  it("falls back to an about:blank tab instead of binding runs to a non-web current tab", async () => {
    const extensionTab = {
      id: 10,
      url: "chrome-extension://abc123/popup.html",
      active: true,
      status: "complete",
      windowId: 1,
    };
    const createdTab = {
      id: 55,
      url: "about:blank",
      active: true,
      status: "complete",
      windowId: 1,
    };

    asMock(chrome.tabs.query).mockImplementation(async () => [extensionTab]);
    asMock(chrome.tabs.create).mockResolvedValue(createdTab);
    asMock(chrome.tabs.get)
      .mockResolvedValueOnce(createdTab)
      .mockResolvedValueOnce(createdTab);

    await enqueueRunAndWait({
      flowId: "flow-current-fallback" as any,
      tabTarget: "current",
    });

    expect(chrome.tabs.create).toHaveBeenCalledWith({
      active: true,
      url: "about:blank",
    });
    expect(mocks.enqueueRun).toHaveBeenCalledWith(
      expect.anything(),
      expect.objectContaining({
        flowId: "flow-current-fallback",
        tabId: 55,
      }),
    );
  });

  it("uses about:blank for new-tab runs when the active tab is not a web page", async () => {
    const extensionTab = {
      id: 12,
      url: "chrome-extension://abc123/popup.html",
      active: true,
      status: "complete",
      windowId: 1,
    };
    const createdTab = {
      id: 77,
      url: "about:blank",
      active: true,
      status: "complete",
      windowId: 1,
    };

    asMock(chrome.tabs.query).mockImplementation(async () => [extensionTab]);
    asMock(chrome.tabs.create).mockResolvedValue(createdTab);
    asMock(chrome.tabs.get).mockImplementation(async (tabId: number) => {
      if (tabId === 77) {
        return createdTab;
      }
      if (tabId === 12) {
        return extensionTab;
      }
      throw new Error(`Unknown tab ${tabId}`);
    });

    await enqueueRunAndWait({
      flowId: "flow-new-fallback" as any,
      tabTarget: "new",
    });

    expect(chrome.tabs.create).toHaveBeenCalledWith({
      active: true,
      url: "about:blank",
    });
    expect(mocks.enqueueRun).toHaveBeenCalledWith(
      expect.anything(),
      expect.objectContaining({
        flowId: "flow-new-fallback",
        tabId: 77,
      }),
    );
  });

  it("waits for startUrl navigation to actually begin before enqueueing a run on an explicit tab", async () => {
    const tabBeforeNavigation = {
      id: 31,
      url: "https://example.com/current",
      active: true,
      status: "complete",
      windowId: 1,
    };
    const tabWhileNavigating = {
      id: 31,
      url: "https://example.com/current",
      pendingUrl: "https://example.com/target",
      active: true,
      status: "loading",
      windowId: 1,
    };
    const tabAfterNavigation = {
      id: 31,
      url: "https://example.com/target",
      active: true,
      status: "complete",
      windowId: 1,
    };

    asMock(chrome.tabs.get)
      .mockResolvedValueOnce(tabBeforeNavigation)
      .mockResolvedValueOnce(tabBeforeNavigation)
      .mockResolvedValueOnce(tabBeforeNavigation)
      .mockResolvedValueOnce(tabWhileNavigating)
      .mockResolvedValueOnce(tabAfterNavigation);
    asMock(chrome.tabs.update).mockResolvedValue(tabAfterNavigation);

    await enqueueRunAndWait({
      flowId: "flow-3" as any,
      tabId: 31,
      startUrl: "https://example.com/target",
    });

    expect(chrome.tabs.update).toHaveBeenCalledWith(31, {
      url: "https://example.com/target",
    });
    expect(chrome.tabs.get).toHaveBeenCalledTimes(5);
    expect(mocks.enqueueRun).toHaveBeenCalledWith(
      expect.anything(),
      expect.objectContaining({
        flowId: "flow-3",
        tabId: 31,
      }),
    );
  });

  it("waits for a refresh to leave the pre-reload complete state before enqueueing", async () => {
    const tabBeforeReload = {
      id: 41,
      url: "https://example.com/dashboard",
      active: true,
      status: "complete",
      windowId: 1,
    };
    const tabReloading = {
      id: 41,
      url: "https://example.com/dashboard",
      active: true,
      status: "loading",
      windowId: 1,
    };

    asMock(chrome.tabs.get)
      .mockResolvedValueOnce(tabBeforeReload)
      .mockResolvedValueOnce(tabBeforeReload)
      .mockResolvedValueOnce(tabBeforeReload)
      .mockResolvedValueOnce(tabReloading)
      .mockResolvedValueOnce(tabBeforeReload);

    await enqueueRunAndWait({
      flowId: "flow-4" as any,
      tabId: 41,
      refresh: true,
    });

    expect(chrome.tabs.reload).toHaveBeenCalledWith(41);
    expect(chrome.tabs.get).toHaveBeenCalledTimes(5);
    expect(mocks.enqueueRun).toHaveBeenCalledWith(
      expect.anything(),
      expect.objectContaining({
        flowId: "flow-4",
        tabId: 41,
      }),
    );
  });

  it("falls back to an about:blank tab for explicit non-web tab targets without navigation", async () => {
    const extensionTab = {
      id: 51,
      url: "chrome-extension://abc123/popup.html",
      active: true,
      status: "complete",
      windowId: 1,
    };
    const createdTab = {
      id: 88,
      url: "about:blank",
      active: true,
      status: "complete",
      windowId: 1,
    };

    asMock(chrome.tabs.get)
      .mockResolvedValueOnce(extensionTab)
      .mockResolvedValueOnce(createdTab)
      .mockResolvedValueOnce(createdTab);
    asMock(chrome.tabs.create).mockResolvedValue(createdTab);

    await enqueueRunAndWait({
      flowId: "flow-explicit-fallback" as any,
      tabId: 51,
    });

    expect(chrome.tabs.create).toHaveBeenCalledWith({
      active: true,
      url: "about:blank",
    });
    expect(chrome.tabs.reload).not.toHaveBeenCalled();
    expect(mocks.enqueueRun).toHaveBeenCalledWith(
      expect.anything(),
      expect.objectContaining({
        flowId: "flow-explicit-fallback",
        tabId: 88,
      }),
    );
  });

  it("rejects explicit non-public tabs for public flow runs", async () => {
    const fileTab = {
      id: 61,
      url: "data:text/html,<h1>secret</h1>",
      active: true,
      status: "complete",
      windowId: 1,
    };

    asMock(chrome.tabs.get).mockResolvedValue(fileTab);

    await expect(
      enqueueRunAndWait({
        flowId: "flow-file-tab" as any,
        tabId: 61,
        execution: { disallowLocalFilePages: true },
      }),
    ).rejects.toThrow(
      "Public flow runs only support HTTP(S) tabs. Switch to an HTTP(S) page or provide an HTTP(S) startUrl.",
    );

    expect(chrome.tabs.create).not.toHaveBeenCalled();
    expect(mocks.enqueueRun).not.toHaveBeenCalled();
  });

  it("rejects current-target public runs when no HTTP(S) tab is available", async () => {
    const extensionTab = {
      id: 73,
      url: "chrome-extension://abc123/popup.html",
      active: true,
      status: "complete",
      windowId: 1,
    };

    asMock(chrome.tabs.query).mockImplementation(async () => [extensionTab]);

    await expect(
      enqueueRunAndWait({
        flowId: "flow-current-public-error" as any,
        execution: { disallowLocalFilePages: true },
      }),
    ).rejects.toThrow(
      "Public flow runs only support HTTP(S) tabs. Switch to an HTTP(S) page or provide an HTTP(S) startUrl.",
    );

    expect(chrome.tabs.create).not.toHaveBeenCalled();
    expect(mocks.enqueueRun).not.toHaveBeenCalled();
  });

  it("standardizes failed V3 run results with error taxonomy and debug args", () => {
    const result = buildCompatRunResult(
      {
        schemaVersion: 3,
        id: "run-failed" as any,
        flowId: "flow-failed" as any,
        status: "failed",
        createdAt: 1,
        updatedAt: 12,
        startedAt: 2,
        finishedAt: 12,
        tookMs: 10,
        tabId: 7,
        currentNodeId: "fill-email" as any,
        attempt: 1,
        maxAttempts: 1,
        error: {
          code: RR_ERROR_CODES.TARGET_NOT_FOUND,
          message: "Could not locate target",
        },
        nextSeq: 4,
      },
      [
        {
          type: "node.started",
          runId: "run-failed" as any,
          nodeId: "fill-email" as any,
          attempt: 1,
          ts: 3,
          seq: 0,
        },
        {
          type: "node.failed",
          runId: "run-failed" as any,
          nodeId: "fill-email" as any,
          attempt: 1,
          error: {
            code: RR_ERROR_CODES.TARGET_NOT_FOUND,
            message: "Could not locate target",
          },
          decision: "stop",
          ts: 10,
          seq: 1,
        },
        {
          type: "run.failed",
          runId: "run-failed" as any,
          nodeId: "fill-email" as any,
          error: {
            code: RR_ERROR_CODES.TARGET_NOT_FOUND,
            message: "Could not locate target",
          },
          ts: 12,
          seq: 2,
        },
      ],
    );

    expect(result).toMatchObject({
      runId: "run-failed",
      flowId: "flow-failed",
      success: false,
      status: "failed",
      currentNodeId: "fill-email",
      failedNodeId: "fill-email",
      errorCode: RR_ERROR_CODES.TARGET_NOT_FOUND,
      error: {
        code: RR_ERROR_CODES.TARGET_NOT_FOUND,
        category: "runtime",
        retryable: true,
        message: "Could not locate target",
        nodeId: "fill-email",
      },
      debug: {
        debugTool: "workflow_debug_view",
        debugArgs: {
          runId: "run-failed",
          flowId: "flow-failed",
          nodeId: "fill-email",
          includeArtifacts: true,
        },
      },
      eventSummary: {
        totalEvents: 3,
        nodeEvents: 2,
        artifactEvents: 0,
        lastSeq: 2,
      },
    });
  });

  it("uses about:blank for public new-tab runs when the active tab is a local file page", async () => {
    const fileTab = {
      id: 71,
      url: "file:///tmp/secret.txt",
      active: true,
      status: "complete",
      windowId: 1,
    };
    const createdTab = {
      id: 72,
      url: "about:blank",
      active: true,
      status: "complete",
      windowId: 1,
    };

    asMock(chrome.tabs.query).mockImplementation(async () => [fileTab]);
    asMock(chrome.tabs.create).mockResolvedValue(createdTab);
    asMock(chrome.tabs.get).mockImplementation(async (tabId: number) => {
      if (tabId === 71) return fileTab;
      if (tabId === 72) return createdTab;
      throw new Error(`Unknown tab ${tabId}`);
    });

    await enqueueRunAndWait({
      flowId: "flow-new-file-fallback" as any,
      tabTarget: "new",
      execution: { disallowLocalFilePages: true },
    });

    expect(chrome.tabs.create).toHaveBeenCalledWith({
      active: true,
      url: "about:blank",
    });
    expect(mocks.enqueueRun).toHaveBeenCalledWith(
      expect.anything(),
      expect.objectContaining({
        flowId: "flow-new-file-fallback",
        tabId: 72,
      }),
    );
  });

  it("saveFlowToV3 converts steps-only compatibility flows before persisting", async () => {
    const runtime = createRuntime();
    mocks.bootstrapV3.mockResolvedValue(runtime);

    const saved = await saveFlowToV3({
      id: "legacy-flow",
      name: "Legacy Flow",
      version: 1,
      steps: [
        { id: "step-1", type: "navigate", url: "https://example.com" },
        { id: "step-2", type: "click", target: { selector: "#submit" } },
      ],
    });

    expect(saved.entryNodeId).toBe("step-1");
    expect(saved.nodes.map((node) => node.id)).toEqual(["step-1", "step-2"]);
    expect(saved.edges).toEqual([
      expect.objectContaining({
        from: "step-1",
        to: "step-2",
      }),
    ]);
    expect(runtime.storage.flows.save).toHaveBeenCalledWith(
      expect.objectContaining({
        id: "legacy-flow",
        entryNodeId: "step-1",
      }),
    );
  });

  it("saveFlowToV3 rejects writes when the expected revision is stale", async () => {
    const runtime = createRuntime();
    mocks.bootstrapV3.mockResolvedValue(runtime);
    const baseline = await saveFlowToV3({
      schemaVersion: 3,
      id: "revision-guard",
      name: "Revision Guard",
      createdAt: new Date(0).toISOString(),
      updatedAt: new Date(0).toISOString(),
      entryNodeId: "node-1",
      nodes: [{ id: "node-1", kind: "navigate", config: { url: "https://example.com/a" } }],
      edges: [],
    });
    const expectedRevision = calculateWorkflowRevision(baseline);
    await saveFlowToV3({
      ...baseline,
      nodes: [{ id: "node-1", kind: "navigate", config: { url: "https://example.com/b" } }],
    });

    await expect(
      saveFlowToV3(
        {
          ...baseline,
          name: "Stale Save",
        },
        { expectedRevision },
      ),
    ).rejects.toMatchObject({
      code: "STALE_WORKFLOW_REVISION",
      flowId: "revision-guard",
      expectedRevision,
    });
    await expect(
      saveFlowToV3(
        {
          ...baseline,
          name: "Stale Save",
        },
        { expectedRevision },
      ),
    ).rejects.toBeInstanceOf(FlowRevisionConflictError);
  });

  it("importFlowsToV3 accepts legacy steps-only JSON payloads", async () => {
    const runtime = createRuntime();
    mocks.bootstrapV3.mockResolvedValue(runtime);

    const imported = await importFlowsToV3(
      JSON.stringify({
        flows: [
          {
            id: "legacy-import",
            name: "Imported Legacy Flow",
            version: 1,
            steps: [
              {
                id: "step-a",
                type: "navigate",
                url: "https://example.com/import",
              },
            ],
          },
        ],
      }),
    );

    expect(imported).toHaveLength(1);
    expect(imported[0]?.entryNodeId).toBe("step-a");
    expect(runtime.storage.flows.save).toHaveBeenCalledTimes(1);
  });

  it("importFlowsToV3 falls back unknown legacy step types to script nodes", async () => {
    const runtime = createRuntime();
    mocks.bootstrapV3.mockResolvedValue(runtime);

    const imported = await importFlowsToV3(
      JSON.stringify({
        flows: [
          {
            id: "legacy-unknown-type",
            name: "Imported Unknown Type",
            version: 1,
            steps: [
              { id: "step-a", type: "unknown_type_xyz", code: "return 42;" },
            ],
          },
        ],
      }),
    );

    expect(imported).toHaveLength(1);
    expect(imported[0]?.nodes).toEqual([
      expect.objectContaining({
        id: "step-a",
        kind: "script",
      }),
    ]);
    expect(runtime.storage.flows.save).toHaveBeenCalledWith(
      expect.objectContaining({
        id: "legacy-unknown-type",
        nodes: [expect.objectContaining({ id: "step-a", kind: "script" })],
      }),
    );
  });

  it("saveFlowToV3 rejects duplicate node ids produced by legacy steps payloads", async () => {
    const runtime = createRuntime();
    mocks.bootstrapV3.mockResolvedValue(runtime);

    await expect(
      saveFlowToV3({
        id: "legacy-dup",
        name: "Legacy Duplicate Flow",
        version: 1,
        steps: [
          { id: "dup-step", type: "navigate", url: "https://example.com" },
          { id: "dup-step", type: "click", target: { selector: "#submit" } },
        ],
      }),
    ).rejects.toThrow('Duplicate node ID: "dup-step"');

    expect(runtime.storage.flows.save).not.toHaveBeenCalled();
  });

  it("saveFlowToV3 rejects node kinds that are explicitly unsupported by the V3 runtime", async () => {
    const runtime = createRuntime();
    mocks.bootstrapV3.mockResolvedValue(runtime);

    await expect(
      saveFlowToV3({
        schemaVersion: 3,
        id: "unsupported-kind",
        name: "Unsupported Kind",
        createdAt: new Date(0).toISOString(),
        updatedAt: new Date(0).toISOString(),
        entryNodeId: "node-1",
        nodes: [{ id: "node-1", kind: "foreach", config: {} }],
        edges: [],
      }),
    ).rejects.toThrow(
      'flow.nodes[0].kind "foreach" is not supported by the current V3 runtime',
    );

    expect(runtime.storage.flows.save).not.toHaveBeenCalled();
  });

  it("saveFlowToV3 rejects raw V3 flows whose entry path starts with a non-executable trigger node", async () => {
    const runtime = createRuntime();
    mocks.bootstrapV3.mockResolvedValue(runtime);

    await expect(
      saveFlowToV3({
        schemaVersion: 3,
        id: "trigger-entry",
        name: "Trigger Entry",
        createdAt: new Date(0).toISOString(),
        updatedAt: new Date(0).toISOString(),
        entryNodeId: "trigger-1",
        nodes: [
          { id: "trigger-1", kind: "trigger", config: {} },
          {
            id: "node-1",
            kind: "navigate",
            config: { url: "https://example.com" },
          },
        ],
        edges: [{ id: "edge-1", from: "trigger-1", to: "node-1" }],
      }),
    ).rejects.toThrow(
      'Flow entry path reaches non-executable node "trigger-1" with kind "trigger"',
    );

    expect(runtime.storage.flows.save).not.toHaveBeenCalled();
  });

  it("saveFlowToV3 still allows trigger nodes that sit outside the executable entry path", async () => {
    const runtime = createRuntime();
    mocks.bootstrapV3.mockResolvedValue(runtime);

    const saved = await saveFlowToV3({
      id: "trigger-prefix",
      name: "Trigger Prefix",
      version: 1,
      nodes: [
        { id: "trigger-1", type: "trigger", config: {} },
        {
          id: "nav-1",
          type: "navigate",
          config: { url: "https://example.com" },
        },
      ],
      edges: [{ id: "edge-1", from: "trigger-1", to: "nav-1" }],
    });

    expect(saved.entryNodeId).toBe("nav-1");
    expect(runtime.storage.flows.save).toHaveBeenCalledWith(
      expect.objectContaining({
        id: "trigger-prefix",
        entryNodeId: "nav-1",
      }),
    );
  });

  it("saveFlowToV3 normalizes published slugs and rejects collisions like the RPC path", async () => {
    const runtime = createRuntime();
    mocks.bootstrapV3.mockResolvedValue(runtime);

    const first = await saveFlowToV3({
      schemaVersion: 3,
      id: "published-a",
      name: "Published A",
      createdAt: new Date(0).toISOString(),
      updatedAt: new Date(0).toISOString(),
      entryNodeId: "node-1",
      nodes: [
        {
          id: "node-1",
          kind: "navigate",
          config: { url: "https://example.com/a" },
        },
      ],
      edges: [],
      meta: {
        tool: {
          published: true,
          slug: "Shared Slug",
        },
      },
    });

    expect(first.meta?.tool?.slug).toBe("shared-slug");

    await expect(
      saveFlowToV3({
        schemaVersion: 3,
        id: "published-b",
        name: "Published B",
        createdAt: new Date(0).toISOString(),
        updatedAt: new Date(0).toISOString(),
        entryNodeId: "node-1",
        nodes: [
          {
            id: "node-1",
            kind: "navigate",
            config: { url: "https://example.com/b" },
          },
        ],
        edges: [],
        meta: {
          tool: {
            published: true,
            slug: "shared-slug",
          },
        },
      }),
    ).rejects.toThrow(
      'Published workflow slug "shared-slug" is already used by flow "published-a"',
    );
  });

  it("saveFlowToV3 preserves an existing custom slug when republishing without an explicit override", async () => {
    const runtime = createRuntime();
    mocks.bootstrapV3.mockResolvedValue(runtime);

    await saveFlowToV3({
      schemaVersion: 3,
      id: "published-custom",
      name: "Original Published Flow",
      createdAt: new Date(0).toISOString(),
      updatedAt: new Date(0).toISOString(),
      entryNodeId: "node-1",
      nodes: [
        {
          id: "node-1",
          kind: "navigate",
          config: { url: "https://example.com/original" },
        },
      ],
      edges: [],
      meta: {
        tool: {
          published: true,
          slug: "custom-shared-slug",
        },
      },
    });

    const republished = await saveFlowToV3({
      schemaVersion: 3,
      id: "published-custom",
      name: "Renamed Published Flow",
      createdAt: new Date(0).toISOString(),
      updatedAt: new Date(0).toISOString(),
      entryNodeId: "node-1",
      nodes: [
        {
          id: "node-1",
          kind: "navigate",
          config: { url: "https://example.com/renamed" },
        },
      ],
      edges: [],
      meta: {
        tool: {
          published: true,
        },
      },
    });

    expect(republished.name).toBe("Renamed Published Flow");
    expect(republished.meta?.tool?.slug).toBe("custom-shared-slug");
  });

  it("saveFlowToV3 validates raw V3 variable definitions like the RPC path", async () => {
    const runtime = createRuntime();
    mocks.bootstrapV3.mockResolvedValue(runtime);

    await expect(
      saveFlowToV3({
        schemaVersion: 3,
        id: "invalid-variable",
        name: "Invalid Variable",
        createdAt: new Date(0).toISOString(),
        updatedAt: new Date(0).toISOString(),
        entryNodeId: "node-1",
        nodes: [
          {
            id: "node-1",
            kind: "navigate",
            config: { url: "https://example.com" },
          },
        ],
        edges: [],
        variables: [{ description: "missing name" }],
      }),
    ).rejects.toThrow("flow.variables[0].name is required");

    expect(runtime.storage.flows.save).not.toHaveBeenCalled();
  });

  it("saveFlowToV3 rejects non-boolean published flags in raw V3 metadata", async () => {
    const runtime = createRuntime();
    mocks.bootstrapV3.mockResolvedValue(runtime);

    await expect(
      saveFlowToV3({
        schemaVersion: 3,
        id: "invalid-published",
        name: "Invalid Published",
        createdAt: new Date(0).toISOString(),
        updatedAt: new Date(0).toISOString(),
        entryNodeId: "node-1",
        nodes: [
          {
            id: "node-1",
            kind: "navigate",
            config: { url: "https://example.com" },
          },
        ],
        edges: [],
        meta: {
          tool: {
            published: "yes",
          },
        },
      }),
    ).rejects.toThrow("flow.meta.tool.published must be a boolean");

    expect(runtime.storage.flows.save).not.toHaveBeenCalled();
  });

  it("saveFlowToV3 rejects non-object policies in raw V3 payloads", async () => {
    const runtime = createRuntime();
    mocks.bootstrapV3.mockResolvedValue(runtime);

    await expect(
      saveFlowToV3({
        schemaVersion: 3,
        id: "invalid-policy",
        name: "Invalid Policy",
        createdAt: new Date(0).toISOString(),
        updatedAt: new Date(0).toISOString(),
        entryNodeId: "node-1",
        nodes: [
          {
            id: "node-1",
            kind: "navigate",
            config: { url: "https://example.com" },
          },
        ],
        edges: [],
        policy: "strict",
      }),
    ).rejects.toThrow("flow.policy must be an object");

    expect(runtime.storage.flows.save).not.toHaveBeenCalled();
  });
});
