import { heteroPostgresTest } from "@internal/testcontainers";
import type { PrismaClient } from "@trigger.dev/database";
import { generateRunOpsId } from "@trigger.dev/core/v3/isomorphic";
import { describe, expect, vi } from "vitest";

// Stub so the runStore singleton doesn't eagerly connect at import.
vi.mock("~/db.server", () => ({
  prisma: {},
  $replica: {},
  runOpsNewPrisma: {},
  runOpsLegacyPrisma: {},
}));
// Keep split off so resolveIdempotencyDedupClient returns this.prisma (the hetero fixture client).
vi.mock("~/v3/runOpsMigration/splitMode.server", () => ({ isSplitEnabled: async () => false }));

import { IdempotencyKeyConcern } from "~/runEngine/concerns/idempotencyKeys.server";
import type { TriggerTaskRequest } from "~/runEngine/types";

vi.setConfig({ testTimeout: 60_000 });

function makeConcern(client: PrismaClient) {
  return new IdempotencyKeyConcern(client as never, {} as never, {} as never);
}

function makeRequest(opts: {
  environmentId: string;
  organizationId: string;
  projectId: string;
  taskId: string;
  idempotencyKey: string;
}): TriggerTaskRequest {
  return {
    taskId: opts.taskId,
    environment: {
      id: opts.environmentId,
      organizationId: opts.organizationId,
      projectId: opts.projectId,
      organization: { featureFlags: {} },
    },
    options: {},
    body: { options: { idempotencyKey: opts.idempotencyKey } },
  } as unknown as TriggerTaskRequest;
}

async function seedOrgProjectEnv(prisma: PrismaClient, suffix: string) {
  const organization = await prisma.organization.create({
    data: { title: `test-${suffix}`, slug: `test-${suffix}` },
  });
  const project = await prisma.project.create({
    data: {
      name: `test-${suffix}`,
      slug: `test-${suffix}`,
      organizationId: organization.id,
      externalRef: `test-${suffix}`,
    },
  });
  const runtimeEnvironment = await prisma.runtimeEnvironment.create({
    data: {
      slug: `test-${suffix}`,
      type: "DEVELOPMENT",
      projectId: project.id,
      organizationId: organization.id,
      apiKey: `test-${suffix}`,
      pkApiKey: `test-${suffix}`,
      shortcode: `test-${suffix}`,
    },
  });
  return { organization, project, runtimeEnvironment };
}

async function seedRun(
  prisma: PrismaClient,
  args: {
    runtimeEnvironmentId: string;
    projectId: string;
    organizationId: string;
    taskIdentifier: string;
    idempotencyKey: string;
    status?: "PENDING" | "EXECUTING" | "COMPLETED_SUCCESSFULLY" | "COMPLETED_WITH_ERRORS";
  }
) {
  const runId = generateRunOpsId();
  return prisma.taskRun.create({
    data: {
      id: runId,
      friendlyId: `run_${runId}`,
      taskIdentifier: args.taskIdentifier,
      idempotencyKey: args.idempotencyKey,
      idempotencyKeyExpiresAt: null,
      status: args.status ?? "EXECUTING",
      payload: JSON.stringify({ foo: "bar" }),
      payloadType: "application/json",
      traceId: "1234",
      spanId: "1234",
      queue: "test",
      runtimeEnvironmentId: args.runtimeEnvironmentId,
      projectId: args.projectId,
      organizationId: args.organizationId,
      environmentType: "DEVELOPMENT",
      engine: "V2",
    },
  });
}

describe("IdempotencyKeyConcern · residency-routed dedup (cross-DB)", () => {
  heteroPostgresTest(
    "a would-be-new run resolves its key against the new (PG17) DB, not the legacy (PG14) DB",
    async ({ prisma14, prisma17 }) => {
      // Same env shape on both DBs.
      const legacy = await seedOrgProjectEnv(prisma14, "resid-legacy");
      const next = await seedOrgProjectEnv(prisma17, "resid-new");

      const key = "idem-resid-1";

      const newRun = await seedRun(prisma17, {
        runtimeEnvironmentId: next.runtimeEnvironment.id,
        projectId: next.project.id,
        organizationId: next.organization.id,
        taskIdentifier: "my-task",
        idempotencyKey: key,
        status: "EXECUTING",
      });

      const concernOnNew = makeConcern(prisma17);
      const hit = await concernOnNew.handleTriggerRequest(
        makeRequest({
          environmentId: next.runtimeEnvironment.id,
          organizationId: next.organization.id,
          projectId: next.project.id,
          taskId: "my-task",
          idempotencyKey: key,
        }),
        undefined
      );
      expect(hit.isCached).toBe(true);
      if (hit.isCached === true) {
        expect(hit.run.id).toBe(newRun.id);
      }

      // The legacy DB holds no row for this key — a legacy-pinned probe would miss it.
      const legacyMatches = await prisma14.taskRun.count({
        where: {
          runtimeEnvironmentId: legacy.runtimeEnvironment.id,
          taskIdentifier: "my-task",
          idempotencyKey: key,
        },
      });
      expect(legacyMatches).toBe(0);
    }
  );

  heteroPostgresTest(
    "a would-be-legacy run still resolves its key against the legacy (PG14) DB",
    async ({ prisma14 }) => {
      const legacy = await seedOrgProjectEnv(prisma14, "resid-legacy-only");
      const key = "idem-resid-legacy";

      const legacyRun = await seedRun(prisma14, {
        runtimeEnvironmentId: legacy.runtimeEnvironment.id,
        projectId: legacy.project.id,
        organizationId: legacy.organization.id,
        taskIdentifier: "my-task",
        idempotencyKey: key,
        status: "EXECUTING",
      });

      const concernOnLegacy = makeConcern(prisma14);
      const hit = await concernOnLegacy.handleTriggerRequest(
        makeRequest({
          environmentId: legacy.runtimeEnvironment.id,
          organizationId: legacy.organization.id,
          projectId: legacy.project.id,
          taskId: "my-task",
          idempotencyKey: key,
        }),
        undefined
      );
      expect(hit.isCached).toBe(true);
      if (hit.isCached === true) {
        expect(hit.run.id).toBe(legacyRun.id);
      }
    }
  );
});
