import { containerTest, heteroPostgresTest } from "@internal/testcontainers";
import { PostgresRunStore } from "@internal/run-store";
import type { Prisma, PrismaClient } from "@trigger.dev/database";
import { generateRunOpsId } from "@trigger.dev/core/v3/isomorphic";
import { beforeEach, describe, expect, vi } from "vitest";

// `resolveSchedule` reads the module-level `prisma` (control-plane handle).
// Point `~/db.server`'s handles at a hoisted mutable holder each test sets to a
// real container client. Not a DB mock: reads still hit a real Postgres
// container — this only redirects which real client the module resolves.
const dbHolder = vi.hoisted(() => ({
  prisma: null as PrismaClient | null,
  $replica: null as PrismaClient | null,
}));

vi.mock("~/db.server", () => ({
  get prisma() {
    return dbHolder.prisma;
  },
  get $replica() {
    return dbHolder.$replica;
  },
}));

// Inert: payloads are inline JSON, never `application/store`, so no presign.
vi.mock("~/v3/objectStore.server", () => ({
  generatePresignedUrl: vi.fn(async () => ({ success: false, error: "not-used" })),
}));

import { ApiRetrieveRunPresenter } from "~/presenters/v3/ApiRetrieveRunPresenter.server";
import type { FoundRun } from "~/presenters/v3/ApiRetrieveRunPresenter.server";
import type { AuthenticatedEnvironment } from "~/services/apiAuth.server";
import { CURRENT_API_VERSION } from "~/api/versions";

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

// The presenter's EXACT read shape — pinned so a refactor that changes
// `findRun`'s `where`/`select` must update this test in lockstep.
const commonRunSelect = {
  id: true,
  friendlyId: true,
  status: true,
  taskIdentifier: true,
  createdAt: true,
  startedAt: true,
  updatedAt: true,
  completedAt: true,
  expiredAt: true,
  delayUntil: true,
  metadata: true,
  metadataType: true,
  ttl: true,
  costInCents: true,
  baseCostInCents: true,
  usageDurationMs: true,
  idempotencyKey: true,
  idempotencyKeyOptions: true,
  isTest: true,
  depth: true,
  scheduleId: true,
  workerQueue: true,
  region: true,
  lockedToVersionId: true,
  resumeParentOnCompletion: true,
  batch: { select: { id: true, friendlyId: true } },
  runTags: true,
} satisfies Prisma.TaskRunSelect;

const findRunSelect = {
  ...commonRunSelect,
  traceId: true,
  payload: true,
  payloadType: true,
  output: true,
  outputType: true,
  error: true,
  attempts: { select: { id: true } },
  attemptNumber: true,
  engine: true,
  taskEventStore: true,
  parentTaskRun: { select: commonRunSelect },
  rootTaskRun: { select: commonRunSelect },
  childRuns: { select: commonRunSelect },
} satisfies Prisma.TaskRunSelect;

// Drive the read exactly as `findRun` does: the RunStore.findRun contract over
// the given store with the presenter's where+select. The scalar `lockedToVersionId`
// folds to a resolved `lockedToVersion` per node, matching the presenter's shape;
// seeded runs carry no locked version, so every node resolves to null.
async function readFoundRunViaStore(
  store: PostgresRunStore,
  friendlyId: string,
  runtimeEnvironmentId: string
): Promise<FoundRun | null> {
  const pgRow = (await store.findRun(
    { friendlyId, runtimeEnvironmentId },
    { select: findRunSelect }
  )) as Record<string, any> | null;
  if (!pgRow) return null;
  const foldVersion = (run: Record<string, any>) => ({ ...run, lockedToVersion: null });
  return {
    ...pgRow,
    lockedToVersion: null,
    parentTaskRun: pgRow.parentTaskRun ? foldVersion(pgRow.parentTaskRun) : null,
    rootTaskRun: pgRow.rootTaskRun ? foldVersion(pgRow.rootTaskRun) : null,
    childRuns: (pgRow.childRuns ?? []).map(foldVersion),
    isBuffered: false,
  } as FoundRun;
}

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: `tr_dev_${suffix}`,
      pkApiKey: `pk_dev_${suffix}`,
      shortcode: `short-${suffix}`,
    },
  });
  return { organization, project, runtimeEnvironment };
}

// Build the AuthenticatedEnvironment shape `call()` reads (id, organizationId,
// slug, project.externalRef). Only those fields are touched on the happy path.
function authEnv(
  organization: { id: string },
  project: { id: string; externalRef: string },
  runtimeEnvironment: { id: string; slug: string }
): AuthenticatedEnvironment {
  return {
    id: runtimeEnvironment.id,
    slug: runtimeEnvironment.slug,
    organizationId: organization.id,
    organization: { id: organization.id },
    project: { id: project.id, externalRef: project.externalRef },
  } as unknown as AuthenticatedEnvironment;
}

interface SeedRunOpts {
  id: string;
  friendlyId: string;
  runtimeEnvironmentId: string;
  projectId: string;
  organizationId: string;
  scheduleId?: string;
  runTags?: string[];
  parentTaskRunId?: string;
  rootTaskRunId?: string;
  metadata?: string;
}

async function seedRun(prisma: PrismaClient, opts: SeedRunOpts) {
  return prisma.taskRun.create({
    data: {
      id: opts.id,
      friendlyId: opts.friendlyId,
      taskIdentifier: "my-task",
      payload: JSON.stringify({ hello: "world" }),
      payloadType: "application/json",
      traceId: `trace_${opts.id}`,
      spanId: `span_${opts.id}`,
      queue: "task/my-task",
      runtimeEnvironmentId: opts.runtimeEnvironmentId,
      projectId: opts.projectId,
      organizationId: opts.organizationId,
      environmentType: "DEVELOPMENT",
      engine: "V2",
      runTags: opts.runTags ?? [],
      scheduleId: opts.scheduleId,
      parentTaskRunId: opts.parentTaskRunId,
      rootTaskRunId: opts.rootTaskRunId,
      metadata: opts.metadata,
      metadataType: opts.metadata ? "application/json" : undefined,
    },
  });
}

// A TaskRunAttempt requires the BackgroundWorker -> BackgroundWorkerTask ->
// TaskQueue FK chain; seed the minimum of each for one attempt.
async function seedAttempt(
  prisma: PrismaClient,
  opts: { runId: string; runtimeEnvironmentId: string; projectId: string; suffix: string }
) {
  const worker = await prisma.backgroundWorker.create({
    data: {
      friendlyId: `worker_${opts.runId}`,
      contentHash: `hash_${opts.suffix}`,
      version: "20260627.1",
      metadata: {},
      projectId: opts.projectId,
      runtimeEnvironmentId: opts.runtimeEnvironmentId,
    },
  });
  const task = await prisma.backgroundWorkerTask.create({
    data: {
      friendlyId: `task_${opts.runId}`,
      slug: "my-task",
      filePath: "src/trigger/my-task.ts",
      workerId: worker.id,
      projectId: opts.projectId,
      runtimeEnvironmentId: opts.runtimeEnvironmentId,
    },
  });
  const queue = await prisma.taskQueue.create({
    data: {
      friendlyId: `queue_${opts.runId}`,
      name: "task/my-task",
      projectId: opts.projectId,
      runtimeEnvironmentId: opts.runtimeEnvironmentId,
    },
  });
  return prisma.taskRunAttempt.create({
    data: {
      friendlyId: `attempt_${opts.runId}`,
      number: 1,
      taskRunId: opts.runId,
      runtimeEnvironmentId: opts.runtimeEnvironmentId,
      backgroundWorkerId: worker.id,
      backgroundWorkerTaskId: task.id,
      queueId: queue.id,
      status: "EXECUTING",
    },
  });
}

// Seed a run plus a parent, a root, a child and one attempt — the tree that must
// round-trip. `seedTestRun.ts` only seeds a single root run, so the tree + attempt
// rows are created inline here.
async function seedRunWithTree(
  prisma: PrismaClient,
  base: {
    runtimeEnvironmentId: string;
    projectId: string;
    organizationId: string;
    suffix: string;
  }
) {
  const parentId = generateRunOpsId();
  const rootId = generateRunOpsId();
  const runId = generateRunOpsId();
  const childId = generateRunOpsId();

  await seedRun(prisma, {
    id: rootId,
    friendlyId: `run_${rootId}`,
    runtimeEnvironmentId: base.runtimeEnvironmentId,
    projectId: base.projectId,
    organizationId: base.organizationId,
  });
  await seedRun(prisma, {
    id: parentId,
    friendlyId: `run_${parentId}`,
    runtimeEnvironmentId: base.runtimeEnvironmentId,
    projectId: base.projectId,
    organizationId: base.organizationId,
    rootTaskRunId: rootId,
  });

  const run = await seedRun(prisma, {
    id: runId,
    friendlyId: `run_${runId}`,
    runtimeEnvironmentId: base.runtimeEnvironmentId,
    projectId: base.projectId,
    organizationId: base.organizationId,
    parentTaskRunId: parentId,
    rootTaskRunId: rootId,
    runTags: ["alpha", "beta"],
    metadata: JSON.stringify({ k: base.suffix }),
  });

  await seedRun(prisma, {
    id: childId,
    friendlyId: `run_${childId}`,
    runtimeEnvironmentId: base.runtimeEnvironmentId,
    projectId: base.projectId,
    organizationId: base.organizationId,
    parentTaskRunId: runId,
    rootTaskRunId: rootId,
  });

  const attempt = await seedAttempt(prisma, {
    runId,
    runtimeEnvironmentId: base.runtimeEnvironmentId,
    projectId: base.projectId,
    suffix: base.suffix,
  });

  return {
    run,
    runFriendlyId: `run_${runId}`,
    parentFriendlyId: `run_${parentId}`,
    rootFriendlyId: `run_${rootId}`,
    childFriendlyId: `run_${childId}`,
    attemptId: attempt.id,
  };
}

beforeEach(() => {
  dbHolder.prisma = null;
  dbHolder.$replica = null;
});

describe("ApiRetrieveRunPresenter.findRun store-routed read (single-DB invariant)", () => {
  containerTest(
    "returns run + attempts + tree from the store read; resolveSchedule reads control-plane prisma",
    async ({ prisma }) => {
      // Single-DB shape: one PostgresRunStore over the one prisma/replica pair,
      // exactly as the production `runStore.server.ts` singleton constructs it.
      dbHolder.prisma = prisma;
      dbHolder.$replica = prisma;
      const store = new PostgresRunStore({ prisma, readOnlyPrisma: prisma });

      const { project, organization, runtimeEnvironment } = await seedOrgProjectEnv(
        prisma,
        "single"
      );

      // Control-plane schedule on the SAME single client.
      const scheduleId = generateRunOpsId();
      await prisma.taskSchedule.create({
        data: {
          id: scheduleId,
          friendlyId: `sched_${scheduleId}`,
          externalId: "my-external-schedule",
          taskIdentifier: "my-task",
          generatorExpression: "0 * * * *",
          generatorDescription: "Every hour",
          projectId: project.id,
        },
      });

      const tree = await seedRunWithTree(prisma, {
        runtimeEnvironmentId: runtimeEnvironment.id,
        projectId: project.id,
        organizationId: organization.id,
        suffix: "single",
      });

      await prisma.taskRun.update({
        where: { id: tree.run.id },
        data: { scheduleId },
      });

      const env = authEnv(organization, project, runtimeEnvironment);
      const found = await readFoundRunViaStore(store, tree.runFriendlyId, env.id);

      expect(found).not.toBeNull();
      expect(found!.isBuffered).toBe(false);
      expect(found!.friendlyId).toBe(tree.runFriendlyId);
      expect(found!.parentTaskRun?.friendlyId).toBe(tree.parentFriendlyId);
      expect(found!.rootTaskRun?.friendlyId).toBe(tree.rootFriendlyId);
      expect(found!.childRuns.map((c) => c.friendlyId)).toEqual([tree.childFriendlyId]);
      expect(found!.attempts.map((a) => a.id)).toEqual([tree.attemptId]);
      expect([...found!.runTags].sort()).toEqual(["alpha", "beta"]);

      // Drive the full presenter `call()` — exercises the real control-plane
      // `resolveSchedule` against the module `prisma` (the container client).
      const out = await new ApiRetrieveRunPresenter(CURRENT_API_VERSION).call(found!, env);

      expect(out.schedule).toBeDefined();
      expect(out.schedule?.externalId).toBe("my-external-schedule");
      expect(out.schedule?.generator.expression).toBe("0 * * * *");
      expect(out.relatedRuns.parent?.id).toBe(tree.parentFriendlyId);
      expect(out.relatedRuns.root?.id).toBe(tree.rootFriendlyId);
      expect(out.relatedRuns.children.map((c) => c.id)).toEqual([tree.childFriendlyId]);
      expect(out.attemptCount).toBe(found!.attemptNumber ?? 0);
    }
  );

  containerTest(
    "resolveSchedule re-reads TaskSchedule off the control-plane prisma on every call (no caching)",
    async ({ prisma }) => {
      // Single-DB: this proves resolveSchedule re-reads `prisma.taskSchedule`
      // on each call() and reflects a delete (no stale cache). The structural
      // "schedule comes from the control-plane client, not the run-ops store"
      // separation is proven by the cross-DB test below (distinct schedule
      // client + an onLegacy=null check); in single-DB both views are the
      // same physical row, so this case cannot discriminate the two paths.
      dbHolder.prisma = prisma;
      dbHolder.$replica = prisma;
      const store = new PostgresRunStore({ prisma, readOnlyPrisma: prisma });

      const { project, organization, runtimeEnvironment } = await seedOrgProjectEnv(
        prisma,
        "cp-inv"
      );

      const scheduleId = generateRunOpsId();
      await prisma.taskSchedule.create({
        data: {
          id: scheduleId,
          friendlyId: `sched_${scheduleId}`,
          taskIdentifier: "my-task",
          generatorExpression: "*/5 * * * *",
          projectId: project.id,
        },
      });

      const runId = generateRunOpsId();
      await seedRun(prisma, {
        id: runId,
        friendlyId: `run_${runId}`,
        runtimeEnvironmentId: runtimeEnvironment.id,
        projectId: project.id,
        organizationId: organization.id,
        scheduleId,
      });

      const env = authEnv(organization, project, runtimeEnvironment);
      const found = await readFoundRunViaStore(store, `run_${runId}`, env.id);
      expect(found).not.toBeNull();
      expect(found!.scheduleId).toBe(scheduleId);

      const presenter = new ApiRetrieveRunPresenter(CURRENT_API_VERSION);
      const out = await presenter.call(found!, env);
      expect(out.schedule?.id).toBe(`sched_${scheduleId}`);

      // Delete the control-plane TaskSchedule row; the run-ops row is
      // untouched. resolveSchedule must now return undefined — proving the
      // schedule comes from `prisma.taskSchedule`, NOT from the run-ops store.
      await prisma.taskSchedule.delete({ where: { id: scheduleId } });
      const out2 = await presenter.call(found!, env);
      expect(out2.schedule).toBeUndefined();
    }
  );
});

describe("ApiRetrieveRunPresenter.findRun cross-version read (PG14 + PG17)", () => {
  heteroPostgresTest(
    "single retrieve returns run + attempts + tree byte-identically from NEW (PG17) and LEGACY (PG14) stores",
    async ({ prisma17, prisma14 }) => {
      const newStore = new PostgresRunStore({ prisma: prisma17, readOnlyPrisma: prisma17 });
      const legacyStore = new PostgresRunStore({ prisma: prisma14, readOnlyPrisma: prisma14 });

      // NEW residency subgraph on PG17.
      const newEnv = await seedOrgProjectEnv(prisma17, "new");
      const newTree = await seedRunWithTree(prisma17, {
        runtimeEnvironmentId: newEnv.runtimeEnvironment.id,
        projectId: newEnv.project.id,
        organizationId: newEnv.organization.id,
        suffix: "new",
      });

      // Distinct LEGACY residency subgraph on PG14.
      const legacyEnv = await seedOrgProjectEnv(prisma14, "legacy");
      const legacyTree = await seedRunWithTree(prisma14, {
        runtimeEnvironmentId: legacyEnv.runtimeEnvironment.id,
        projectId: legacyEnv.project.id,
        organizationId: legacyEnv.organization.id,
        suffix: "legacy",
      });

      // Read the NEW run from the PG17 store.
      const foundNew = await readFoundRunViaStore(
        newStore,
        newTree.runFriendlyId,
        newEnv.runtimeEnvironment.id
      );
      expect(foundNew).not.toBeNull();
      expect(foundNew!.friendlyId).toBe(newTree.runFriendlyId);
      expect(foundNew!.parentTaskRun?.friendlyId).toBe(newTree.parentFriendlyId);
      expect(foundNew!.rootTaskRun?.friendlyId).toBe(newTree.rootFriendlyId);
      expect(foundNew!.childRuns.map((c) => c.friendlyId)).toEqual([newTree.childFriendlyId]);
      expect(foundNew!.attempts.map((a) => a.id)).toEqual([newTree.attemptId]);
      expect([...foundNew!.runTags].sort()).toEqual(["alpha", "beta"]);
      expect(foundNew!.metadata).toBe(JSON.stringify({ k: "new" }));

      // Sanity: the two stores are genuinely distinct DBs — the NEW run's key
      // is absent from PG14 (it was only ever written to PG17).
      const leaked = await readFoundRunViaStore(
        legacyStore,
        newTree.runFriendlyId,
        newEnv.runtimeEnvironment.id
      );
      expect(leaked).toBeNull();

      // Read the LEGACY run from the PG14 store — byte-identical tree.
      const foundLegacy = await readFoundRunViaStore(
        legacyStore,
        legacyTree.runFriendlyId,
        legacyEnv.runtimeEnvironment.id
      );
      expect(foundLegacy).not.toBeNull();
      expect(foundLegacy!.friendlyId).toBe(legacyTree.runFriendlyId);
      expect(foundLegacy!.parentTaskRun?.friendlyId).toBe(legacyTree.parentFriendlyId);
      expect(foundLegacy!.rootTaskRun?.friendlyId).toBe(legacyTree.rootFriendlyId);
      expect(foundLegacy!.childRuns.map((c) => c.friendlyId)).toEqual([legacyTree.childFriendlyId]);
      expect(foundLegacy!.attempts.map((a) => a.id)).toEqual([legacyTree.attemptId]);
      expect(foundLegacy!.metadata).toBe(JSON.stringify({ k: "legacy" }));
    }
  );

  heteroPostgresTest(
    "schedule resolved cross-DB: run hydrated from run-ops store (PG14), schedule from a distinct control-plane client (PG17)",
    async ({ prisma17, prisma14 }) => {
      // Run-ops residency = LEGACY (PG14). Control-plane (TaskSchedule) lives
      // on a DISTINCT client — PG17 — and is the handle the module `prisma`
      // resolves to in this test. This proves the run-ops-row -> control-plane
      // -schedule cross-seam read: the run comes from one DB, the schedule
      // from another.
      const legacyStore = new PostgresRunStore({ prisma: prisma14, readOnlyPrisma: prisma14 });

      // Module control-plane handle -> PG17.
      dbHolder.prisma = prisma17;
      dbHolder.$replica = prisma17;

      const legacyEnv = await seedOrgProjectEnv(prisma14, "x-run");
      // The control-plane project the schedule hangs off lives on PG17.
      const cpEnv = await seedOrgProjectEnv(prisma17, "x-cp");

      const scheduleId = generateRunOpsId();
      await prisma17.taskSchedule.create({
        data: {
          id: scheduleId,
          friendlyId: `sched_${scheduleId}`,
          externalId: "cross-db-schedule",
          taskIdentifier: "my-task",
          generatorExpression: "0 0 * * *",
          projectId: cpEnv.project.id,
        },
      });

      const runId = generateRunOpsId();
      await seedRun(prisma14, {
        id: runId,
        friendlyId: `run_${runId}`,
        runtimeEnvironmentId: legacyEnv.runtimeEnvironment.id,
        projectId: legacyEnv.project.id,
        organizationId: legacyEnv.organization.id,
        scheduleId,
      });

      const env = authEnv(legacyEnv.organization, legacyEnv.project, legacyEnv.runtimeEnvironment);
      const found = await readFoundRunViaStore(
        legacyStore,
        `run_${runId}`,
        legacyEnv.runtimeEnvironment.id
      );
      expect(found).not.toBeNull();
      expect(found!.scheduleId).toBe(scheduleId);

      // The schedule row does NOT exist on the run-ops (PG14) client.
      const onLegacy = await prisma14.taskSchedule.findFirst({ where: { id: scheduleId } });
      expect(onLegacy).toBeNull();

      const out = await new ApiRetrieveRunPresenter(CURRENT_API_VERSION).call(found!, env);
      expect(out.schedule).toBeDefined();
      expect(out.schedule?.id).toBe(`sched_${scheduleId}`);
      expect(out.schedule?.externalId).toBe("cross-db-schedule");
      expect(out.schedule?.generator.expression).toBe("0 0 * * *");
    }
  );

  heteroPostgresTest(
    "correct past-retention / not-found response: both stores miss => findRun returns null",
    async ({ prisma17, prisma14 }) => {
      const newStore = new PostgresRunStore({ prisma: prisma17, readOnlyPrisma: prisma17 });
      const legacyStore = new PostgresRunStore({ prisma: prisma14, readOnlyPrisma: prisma14 });

      const newEnv = await seedOrgProjectEnv(prisma17, "miss-new");
      const legacyEnv = await seedOrgProjectEnv(prisma14, "miss-legacy");

      // A run that exists on NEITHER store (terminated + past-retention,
      // observed at this layer as a miss on both underlying stores).
      const goneFriendlyId = `run_${generateRunOpsId()}`;

      const fromNew = await readFoundRunViaStore(
        newStore,
        goneFriendlyId,
        newEnv.runtimeEnvironment.id
      );
      const fromLegacy = await readFoundRunViaStore(
        legacyStore,
        goneFriendlyId,
        legacyEnv.runtimeEnvironment.id
      );

      expect(fromNew).toBeNull();
      expect(fromLegacy).toBeNull();
    }
  );

  heteroPostgresTest(
    "single-DB passthrough: one PostgresRunStore over one client hydrates run + tree (self-host collapse)",
    async ({ prisma17 }) => {
      // The single-DB collapse: one plain PostgresRunStore over one client.
      // No legacy probe / known-migrated machinery at this layer.
      const store = new PostgresRunStore({ prisma: prisma17, readOnlyPrisma: prisma17 });

      const onlyEnv = await seedOrgProjectEnv(prisma17, "passthrough");
      const tree = await seedRunWithTree(prisma17, {
        runtimeEnvironmentId: onlyEnv.runtimeEnvironment.id,
        projectId: onlyEnv.project.id,
        organizationId: onlyEnv.organization.id,
        suffix: "passthrough",
      });

      const found = await readFoundRunViaStore(
        store,
        tree.runFriendlyId,
        onlyEnv.runtimeEnvironment.id
      );
      expect(found).not.toBeNull();
      expect(found!.isBuffered).toBe(false);
      expect(found!.parentTaskRun?.friendlyId).toBe(tree.parentFriendlyId);
      expect(found!.rootTaskRun?.friendlyId).toBe(tree.rootFriendlyId);
      expect(found!.childRuns.map((c) => c.friendlyId)).toEqual([tree.childFriendlyId]);
      expect(found!.attempts.map((a) => a.id)).toEqual([tree.attemptId]);
    }
  );
});
