import { ClickHouse } from "@internal/clickhouse";
import { createPostgresContainer, replicationContainerTest } from "@internal/testcontainers";
import { PrismaClient, type TaskRunStatus as TaskRunStatusType } from "@trigger.dev/database";
import { setTimeout } from "node:timers/promises";
import { z } from "zod";
import { TaskRunStatus } from "~/database-types";
import { RunsReplicationService } from "~/services/runsReplicationService.server";
import { createInMemoryTracing } from "./utils/tracing";
import { TestReplicationClickhouseFactory } from "./utils/testReplicationClickhouseFactory";

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

describe("RunsReplicationService (part 8/8) - dual-source dedup", () => {
  replicationContainerTest(
    "collapses the same run from two slots into one ClickHouse row, gen-1 wins across the PG14<->PG17 boundary",
    async ({ clickhouseContainer, redisOptions, postgresContainer, prisma, network }) => {
      // LEGACY / gen-0 source = the fixture's PG14 container.
      const legacyUrl = postgresContainer.getConnectionUri();

      // NEW / gen-1 source = a dedicated PG17 container on the SAME network.
      // createPostgresContainer applies wal_level=logical and pushes the TaskRun schema.
      const { url: newUrl, container: pg17 } = await createPostgresContainer(network, {
        imageTag: "docker.io/postgres:17",
      });

      const clickhouse = new ClickHouse({
        url: clickhouseContainer.getConnectionUrl(),
        name: "runs-replication",
        logLevel: "warn",
      });

      const { tracer } = createInMemoryTracing();

      let runsReplicationService: RunsReplicationService | undefined;

      try {
        // REPLICA IDENTITY FULL on BOTH source DBs before start().
        await prisma.$executeRawUnsafe(`ALTER TABLE public."TaskRun" REPLICA IDENTITY FULL;`);

        const newPrismaForAlter = new PrismaClient({
          datasources: { db: { url: newUrl } },
        });
        try {
          await newPrismaForAlter.$executeRawUnsafe(
            `ALTER TABLE public."TaskRun" REPLICA IDENTITY FULL;`
          );
        } finally {
          await newPrismaForAlter.$disconnect();
        }

        runsReplicationService = new RunsReplicationService({
          clickhouseFactory: new TestReplicationClickhouseFactory(clickhouse),
          serviceName: "runs-replication",
          redisOptions,
          sources: [
            {
              id: "legacy",
              pgConnectionUrl: legacyUrl,
              slotName: "tr_legacy_v1",
              publicationName: "tr_legacy_v1_pub",
              originGeneration: 0,
            },
            {
              id: "new",
              pgConnectionUrl: newUrl,
              slotName: "tr_new_v1",
              publicationName: "tr_new_v1_pub",
              originGeneration: 1,
            },
          ],
          maxFlushConcurrency: 1,
          flushIntervalMs: 100,
          flushBatchSize: 1,
          leaderLockTimeoutMs: 5000,
          leaderLockExtendIntervalMs: 1000,
          ackIntervalSeconds: 5,
          tracer,
          logLevel: "warn",
        });

        await runsReplicationService.start();

        // The ClickHouse ReplacingMergeTree dedup key is the full ORDER BY tuple
        // (organization_id, project_id, environment_id, created_at, run_id) - NOT run_id alone.
        // So the two source rows must share ALL of those columns to collapse into one. We give
        // both DBs identical org/project/env/run ids and an identical createdAt.
        const suffix = `${Date.now()}`;
        const sharedOrgId = `org_dual_${suffix}`;
        const sharedProjectId = `proj_dual_${suffix}`;
        const sharedEnvId = `env_dual_${suffix}`;
        const sharedRunId = `run_dual_${suffix}`;
        const sharedCreatedAt = new Date();

        const newPrisma = new PrismaClient({ datasources: { db: { url: newUrl } } });

        const seedFkRows = async (
          client: PrismaClient,
          tag: string,
          status: TaskRunStatusType,
          friendlyId: string
        ) => {
          await client.organization.create({
            data: { id: sharedOrgId, title: `org-${tag}`, slug: `org-${tag}` },
          });
          await client.project.create({
            data: {
              id: sharedProjectId,
              name: `proj-${tag}`,
              slug: `proj-${tag}`,
              organizationId: sharedOrgId,
              externalRef: `proj-${tag}`,
            },
          });
          await client.runtimeEnvironment.create({
            data: {
              id: sharedEnvId,
              slug: `env-${tag}`,
              type: "DEVELOPMENT",
              projectId: sharedProjectId,
              organizationId: sharedOrgId,
              apiKey: `apikey-${tag}`,
              pkApiKey: `pkapikey-${tag}`,
              shortcode: `shortcode-${tag}`,
            },
          });
          await client.taskRun.create({
            data: {
              id: sharedRunId,
              friendlyId,
              taskIdentifier: "my-task",
              payload: JSON.stringify({ foo: "bar" }),
              traceId: `trace-${tag}`,
              spanId: `span-${tag}`,
              queue: "test",
              status,
              createdAt: sharedCreatedAt,
              runtimeEnvironmentId: sharedEnvId,
              projectId: sharedProjectId,
              organizationId: sharedOrgId,
              environmentType: "DEVELOPMENT",
              engine: "V2",
            },
          });
        };

        // The two source servers have INDEPENDENT WAL counters, so commit order alone does not
        // control raw LSN magnitude. To make legacy's raw LSN deterministically larger we burn WAL
        // on the legacy server with pg_switch_wal() (each call bumps the WAL segment = the high 32
        // bits of the LSN) before inserting the legacy run. With raw LSN as _version the larger-LSN
        // STALE legacy PENDING snapshot would win the dedup - which is the RED this guards against.
        try {
          await seedFkRows(newPrisma, "new", TaskRunStatus.COMPLETED_SUCCESSFULLY, "run_dual_new");

          // Settle so the new DB's WAL entry is produced (and committed) before the legacy one.
          await setTimeout(500);

          for (let i = 0; i < 16; i++) {
            await prisma.$executeRawUnsafe(`SELECT pg_switch_wal();`);
          }

          await seedFkRows(prisma, "legacy", TaskRunStatus.PENDING, "run_dual_legacy");
        } finally {
          await newPrisma.$disconnect();
        }

        const queryRuns = clickhouse.reader.query({
          name: "dual-source",
          query:
            "SELECT run_id, status, count() OVER () AS total FROM trigger_dev.task_runs_v2 FINAL",
          schema: z.object({
            run_id: z.string(),
            status: z.string(),
            total: z.number().int(),
          }),
        });

        // Poll until BOTH streams have flushed and the gen-1 winner has settled in ClickHouse.
        await vi.waitFor(
          async () => {
            const [queryError, result] = await queryRuns({});

            expect(queryError).toBeNull();
            expect(result).toHaveLength(1);
            expect(result?.[0]).toEqual(
              expect.objectContaining({
                run_id: sharedRunId,
                status: "COMPLETED_SUCCESSFULLY",
              })
            );
          },
          { timeout: 30_000, interval: 250 }
        );
      } finally {
        await runsReplicationService?.stop();
        await pg17.stop({ timeout: 0 });
      }
    }
  );

  // Case A - reverse-order independence.
  // Same run in both sources, but we flush the NEW (gen-1) snapshot FIRST, then the LEGACY
  // (gen-0) snapshot. The gen-1 winner must survive regardless of arrival order - the collapse
  // is FINAL-time and ordered by _version (composed origin generation), not by arrival time.
  replicationContainerTest(
    "gen-1 winner survives when the new snapshot flushes before the legacy snapshot (reverse-order independence)",
    async ({ clickhouseContainer, redisOptions, postgresContainer, prisma, network }) => {
      const legacyUrl = postgresContainer.getConnectionUri();

      const { url: newUrl, container: pg17 } = await createPostgresContainer(network, {
        imageTag: "docker.io/postgres:17",
      });

      const clickhouse = new ClickHouse({
        url: clickhouseContainer.getConnectionUrl(),
        name: "runs-replication",
        logLevel: "warn",
      });

      const { tracer } = createInMemoryTracing();

      let runsReplicationService: RunsReplicationService | undefined;

      try {
        await prisma.$executeRawUnsafe(`ALTER TABLE public."TaskRun" REPLICA IDENTITY FULL;`);

        const newPrismaForAlter = new PrismaClient({
          datasources: { db: { url: newUrl } },
        });
        try {
          await newPrismaForAlter.$executeRawUnsafe(
            `ALTER TABLE public."TaskRun" REPLICA IDENTITY FULL;`
          );
        } finally {
          await newPrismaForAlter.$disconnect();
        }

        runsReplicationService = new RunsReplicationService({
          clickhouseFactory: new TestReplicationClickhouseFactory(clickhouse),
          serviceName: "runs-replication",
          redisOptions,
          sources: [
            {
              id: "legacy",
              pgConnectionUrl: legacyUrl,
              slotName: "tr_legacy_a",
              publicationName: "tr_legacy_a_pub",
              originGeneration: 0,
            },
            {
              id: "new",
              pgConnectionUrl: newUrl,
              slotName: "tr_new_a",
              publicationName: "tr_new_a_pub",
              originGeneration: 1,
            },
          ],
          maxFlushConcurrency: 1,
          flushIntervalMs: 100,
          flushBatchSize: 1,
          leaderLockTimeoutMs: 5000,
          leaderLockExtendIntervalMs: 1000,
          ackIntervalSeconds: 5,
          tracer,
          logLevel: "warn",
        });

        await runsReplicationService.start();

        const suffix = `a_${Date.now()}`;
        const sharedOrgId = `org_dual_${suffix}`;
        const sharedProjectId = `proj_dual_${suffix}`;
        const sharedEnvId = `env_dual_${suffix}`;
        const sharedRunId = `run_dual_${suffix}`;
        const sharedCreatedAt = new Date();

        const newPrisma = new PrismaClient({ datasources: { db: { url: newUrl } } });

        const seedFkRows = async (
          client: PrismaClient,
          tag: string,
          status: TaskRunStatusType,
          friendlyId: string
        ) => {
          await client.organization.create({
            data: { id: sharedOrgId, title: `org-${tag}`, slug: `org-${tag}` },
          });
          await client.project.create({
            data: {
              id: sharedProjectId,
              name: `proj-${tag}`,
              slug: `proj-${tag}`,
              organizationId: sharedOrgId,
              externalRef: `proj-${tag}`,
            },
          });
          await client.runtimeEnvironment.create({
            data: {
              id: sharedEnvId,
              slug: `env-${tag}`,
              type: "DEVELOPMENT",
              projectId: sharedProjectId,
              organizationId: sharedOrgId,
              apiKey: `apikey-${tag}`,
              pkApiKey: `pkapikey-${tag}`,
              shortcode: `shortcode-${tag}`,
            },
          });
          await client.taskRun.create({
            data: {
              id: sharedRunId,
              friendlyId,
              taskIdentifier: "my-task",
              payload: JSON.stringify({ foo: "bar" }),
              traceId: `trace-${tag}`,
              spanId: `span-${tag}`,
              queue: "test",
              status,
              createdAt: sharedCreatedAt,
              runtimeEnvironmentId: sharedEnvId,
              projectId: sharedProjectId,
              organizationId: sharedOrgId,
              environmentType: "DEVELOPMENT",
              engine: "V2",
            },
          });
        };

        try {
          // Flush the NEW (gen-1, COMPLETED) snapshot FIRST and let it land in ClickHouse.
          await seedFkRows(newPrisma, "new", TaskRunStatus.COMPLETED_SUCCESSFULLY, "run_dual_new");
          await setTimeout(2000);

          // THEN flush the LEGACY (gen-0, PENDING) snapshot.
          await seedFkRows(prisma, "legacy", TaskRunStatus.PENDING, "run_dual_legacy");
        } finally {
          await newPrisma.$disconnect();
        }

        const queryRuns = clickhouse.reader.query({
          name: "dual-source-reverse",
          query:
            "SELECT run_id, status, count() OVER () AS total FROM trigger_dev.task_runs_v2 FINAL",
          schema: z.object({
            run_id: z.string(),
            status: z.string(),
            total: z.number().int(),
          }),
        });

        // Poll until BOTH streams have flushed and the gen-1 winner has settled in ClickHouse.
        await vi.waitFor(
          async () => {
            const [queryError, result] = await queryRuns({});

            expect(queryError).toBeNull();
            expect(result).toHaveLength(1);
            expect(result?.[0]).toEqual(
              expect.objectContaining({
                run_id: sharedRunId,
                status: "COMPLETED_SUCCESSFULLY",
              })
            );
          },
          { timeout: 30_000, interval: 250 }
        );
      } finally {
        await runsReplicationService?.stop();
        await pg17.stop({ timeout: 0 });
      }
    }
  );

  // Case B - per-source independence / no cross-contamination.
  // Two DIFFERENT runs: run X lives ONLY in the legacy/gen-0 DB, run Y lives ONLY in the
  // new/gen-1 DB. BOTH must appear in ClickHouse exactly once with their own status. This proves
  // (a) BOTH sources became leader and streamed (a single-leader regression would drop one run),
  // and (b) the two streams don't corrupt each other's per-source transaction/LSN state.
  replicationContainerTest(
    "streams two distinct runs from two sources independently without cross-contamination",
    async ({ clickhouseContainer, redisOptions, postgresContainer, prisma, network }) => {
      const legacyUrl = postgresContainer.getConnectionUri();

      const { url: newUrl, container: pg17 } = await createPostgresContainer(network, {
        imageTag: "docker.io/postgres:17",
      });

      const clickhouse = new ClickHouse({
        url: clickhouseContainer.getConnectionUrl(),
        name: "runs-replication",
        logLevel: "warn",
      });

      const { tracer } = createInMemoryTracing();

      let runsReplicationService: RunsReplicationService | undefined;

      try {
        await prisma.$executeRawUnsafe(`ALTER TABLE public."TaskRun" REPLICA IDENTITY FULL;`);

        const newPrismaForAlter = new PrismaClient({
          datasources: { db: { url: newUrl } },
        });
        try {
          await newPrismaForAlter.$executeRawUnsafe(
            `ALTER TABLE public."TaskRun" REPLICA IDENTITY FULL;`
          );
        } finally {
          await newPrismaForAlter.$disconnect();
        }

        runsReplicationService = new RunsReplicationService({
          clickhouseFactory: new TestReplicationClickhouseFactory(clickhouse),
          serviceName: "runs-replication",
          redisOptions,
          sources: [
            {
              id: "legacy",
              pgConnectionUrl: legacyUrl,
              slotName: "tr_legacy_b",
              publicationName: "tr_legacy_b_pub",
              originGeneration: 0,
            },
            {
              id: "new",
              pgConnectionUrl: newUrl,
              slotName: "tr_new_b",
              publicationName: "tr_new_b_pub",
              originGeneration: 1,
            },
          ],
          maxFlushConcurrency: 1,
          flushIntervalMs: 100,
          flushBatchSize: 1,
          leaderLockTimeoutMs: 5000,
          leaderLockExtendIntervalMs: 1000,
          ackIntervalSeconds: 5,
          tracer,
          logLevel: "warn",
        });

        await runsReplicationService.start();

        // Run X (legacy-only) and run Y (new-only) get DISTINCT ids. Each lives in its own DB
        // with its own org/project/env, so there is nothing to collapse - both must survive.
        const suffix = `b_${Date.now()}`;
        const legacyRunId = `run_legacy_only_${suffix}`;
        const newRunId = `run_new_only_${suffix}`;

        const newPrisma = new PrismaClient({ datasources: { db: { url: newUrl } } });

        const seedRun = async (
          client: PrismaClient,
          tag: string,
          runId: string,
          status: TaskRunStatusType
        ) => {
          const orgId = `org_${tag}_${suffix}`;
          const projectId = `proj_${tag}_${suffix}`;
          const envId = `env_${tag}_${suffix}`;

          await client.organization.create({
            data: { id: orgId, title: `org-${tag}`, slug: `org-${tag}-${suffix}` },
          });
          await client.project.create({
            data: {
              id: projectId,
              name: `proj-${tag}`,
              slug: `proj-${tag}-${suffix}`,
              organizationId: orgId,
              externalRef: `proj-${tag}-${suffix}`,
            },
          });
          await client.runtimeEnvironment.create({
            data: {
              id: envId,
              slug: `env-${tag}`,
              type: "DEVELOPMENT",
              projectId: projectId,
              organizationId: orgId,
              apiKey: `apikey-${tag}-${suffix}`,
              pkApiKey: `pkapikey-${tag}-${suffix}`,
              shortcode: `shortcode-${tag}-${suffix}`,
            },
          });
          await client.taskRun.create({
            data: {
              id: runId,
              friendlyId: `friendly-${runId}`,
              taskIdentifier: "my-task",
              payload: JSON.stringify({ foo: "bar" }),
              traceId: `trace-${tag}-${suffix}`,
              spanId: `span-${tag}-${suffix}`,
              queue: "test",
              status,
              createdAt: new Date(),
              runtimeEnvironmentId: envId,
              projectId: projectId,
              organizationId: orgId,
              environmentType: "DEVELOPMENT",
              engine: "V2",
            },
          });
        };

        try {
          // Seed run X ONLY in legacy and run Y ONLY in new.
          await seedRun(prisma, "legacy", legacyRunId, TaskRunStatus.PENDING);
          await seedRun(newPrisma, "new", newRunId, TaskRunStatus.COMPLETED_SUCCESSFULLY);
        } finally {
          await newPrisma.$disconnect();
        }

        const queryRuns = clickhouse.reader.query({
          name: "dual-source-independent",
          query: "SELECT run_id, status FROM trigger_dev.task_runs_v2 FINAL ORDER BY run_id",
          schema: z.object({
            run_id: z.string(),
            status: z.string(),
          }),
        });

        // Poll until BOTH sources have independently flushed their run into ClickHouse.
        const result = await vi.waitFor(
          async () => {
            const [queryError, rows] = await queryRuns({});

            expect(queryError).toBeNull();
            expect(rows).toHaveLength(2);

            return rows;
          },
          { timeout: 30_000, interval: 250 }
        );

        const byRunId = new Map(result?.map((row) => [row.run_id, row.status]));
        expect(byRunId.get(legacyRunId)).toBe("PENDING");
        expect(byRunId.get(newRunId)).toBe("COMPLETED_SUCCESSFULLY");
      } finally {
        await runsReplicationService?.stop();
        await pg17.stop({ timeout: 0 });
      }
    }
  );
});
