import { redisTest } from "@internal/testcontainers";
import { setTimeout as sleep } from "node:timers/promises";
import { CURRENT_API_VERSION } from "~/api/versions";
import { EnvChangeRouter } from "~/services/realtime/envChangeRouter.server";
import {
  NativeRealtimeClient,
  type RealtimeListEnvironment,
} from "~/services/realtime/nativeRealtimeClient.server";
import {
  InMemoryReplayCursorStore,
  RedisReplayCursorStore,
  type ReplayCursorStore,
} from "~/services/realtime/replayCursorStore.server";
import { describe, expect, it, vi } from "vitest";

describe("InMemoryReplayCursorStore", () => {
  it("round-trips and expires", async () => {
    const store = new InMemoryReplayCursorStore(50, 10);
    store.set("env_1:h1", 123_456);
    expect(await store.get("env_1:h1")).toBe(123_456);
    expect(await store.get("env_1:other")).toBeUndefined();
    await sleep(60);
    expect(await store.get("env_1:h1")).toBeUndefined();
  });
});

describe("RedisReplayCursorStore", () => {
  redisTest("round-trips, misses, and expires via PX", async ({ redisOptions }) => {
    const store = new RedisReplayCursorStore({
      redis: { ...redisOptions, tlsDisabled: true },
      ttlMs: 150,
    });
    try {
      const now = Date.now();
      store.set("env_1:h1", now);
      await vi.waitFor(async () => expect(await store.get("env_1:h1")).toBe(now));
      expect(await store.get("env_1:missing")).toBeUndefined();
      await sleep(200);
      expect(await store.get("env_1:h1")).toBeUndefined();
    } finally {
      await store.quit();
    }
  });

  redisTest(
    "a second store instance reads the first's cursor (fleet sharing)",
    async ({ redisOptions }) => {
      const a = new RedisReplayCursorStore({
        redis: { ...redisOptions, tlsDisabled: true },
        ttlMs: 60_000,
      });
      const b = new RedisReplayCursorStore({
        redis: { ...redisOptions, tlsDisabled: true },
        ttlMs: 60_000,
      });
      try {
        a.set("env_1:h2", 42_000);
        await vi.waitFor(async () => expect(await b.get("env_1:h2")).toBe(42_000));
      } finally {
        await Promise.all([a.quit(), b.quit()]);
      }
    }
  );

  it("degrades to undefined within the read deadline when Redis is unreachable", async () => {
    const results: Array<[string, boolean]> = [];
    const store = new RedisReplayCursorStore({
      redis: { host: "127.0.0.1", port: 1, tlsDisabled: true } as any,
      ttlMs: 1_000,
      getTimeoutMs: 100,
      onResult: (op, ok) => results.push([op, ok]),
    });
    try {
      expect(await store.get("env_1:h3")).toBeUndefined();
      expect(results).toContainEqual(["get", false]);
    } finally {
      await store.quit().catch(() => {});
    }
  });
});

describe("NativeRealtimeClient replay-cursor threading", () => {
  const ENV: RealtimeListEnvironment = {
    id: "env_1",
    organizationId: "org_1",
    projectId: "proj_1",
  };
  const FLOOR_MS = Date.UTC(2026, 5, 7, 12, 0, 0);

  it("passes the stored cursor to register and stamps the store after responding", async () => {
    const cursorMs = Date.now() - 500;
    const gets: string[] = [];
    const sets: Array<[string, number]> = [];
    const store: ReplayCursorStore = {
      get: async (key) => {
        gets.push(key);
        return cursorMs;
      },
      set: (key, ms) => {
        sets.push([key, ms]);
      },
    };

    const router = new EnvChangeRouter({
      source: { subscribeToEnv: () => () => {} },
      hydrator: { hydrateByIds: async () => [] },
      replayWindowMs: 0,
      unsubscribeLingerMs: 0,
    });
    const registerSpy = vi.spyOn(router, "register");

    const client = new NativeRealtimeClient({
      runReader: { getRunById: async () => null, hydrateByIds: async () => [] } as any,
      runListResolver: { resolveMatchingRunIds: async () => [] } as any,
      router,
      limiter: { incrementAndCheck: async () => true, decrement: async () => {} } as any,
      cachedLimitProvider: { getCachedLimit: async () => 100 },
      maximumCreatedAtFilterAgeMs: 100 * 365 * 24 * 60 * 60 * 1000,
      runSetResolveCacheTtlMs: 0,
      livePollTimeoutMs: 30,
      replayCursorStore: store,
    });

    const res = await client.streamRuns(
      `http://localhost:3030/realtime/v1/runs?offset=${FLOOR_MS}_1&live=true&handle=runs_${FLOOR_MS}_7`,
      ENV,
      { tags: ["t"] },
      CURRENT_API_VERSION,
      undefined,
      "1.0.0"
    );

    expect(res.status).toBe(200);
    expect(gets).toEqual([`env_1:runs_${FLOOR_MS}_7`]);
    expect(registerSpy).toHaveBeenCalledWith(
      "env_1",
      expect.objectContaining({ kind: "tag" }),
      expect.anything(),
      { replaySinceMs: cursorMs }
    );
    // The backstop's up-to-date response stamps the cursor for the next poll.
    expect(sets.length).toBe(1);
    expect(sets[0][0]).toBe(`env_1:runs_${FLOOR_MS}_7`);
    expect(sets[0][1]).toBeGreaterThanOrEqual(cursorMs);
  });
});
