// Plan F.3: integration test that round-trips a `ChatSnapshotV1` blob
// through the SDK's snapshot helpers + a real MinIO backing store. Mirrors
// the testcontainer pattern from `objectStore.test.ts`.
//
// What this verifies end-to-end:
//   - SDK's `writeChatSnapshot` calls `apiClient.createUploadPayloadUrl`
//     to mint a presigned PUT, then PUTs JSON to it.
//   - SDK's `readChatSnapshot` calls `apiClient.getPayloadUrl` to mint a
//     presigned GET, then fetches and parses.
//   - The webapp's `generatePresignedUrl` produces URLs MinIO accepts.
//   - The blob round-trips with `version: 1` shape preserved.
//   - 404 (no snapshot for a fresh session) returns `undefined`, not an
//     error.
//
// This is the integration safety net behind the unit tests in
// `packages/trigger-sdk/test/chat-snapshot.test.ts` — those tests mock
// `fetch`; this one drives a real S3-compatible backend.

import { postgresAndMinioTest } from "@internal/testcontainers";
import { apiClientManager } from "@trigger.dev/core/v3";
import {
  __readChatSnapshotProductionPathForTests as readChatSnapshot,
  __writeChatSnapshotProductionPathForTests as writeChatSnapshot,
  type ChatSnapshotV1,
} from "@trigger.dev/sdk/ai";
import type { UIMessage } from "ai";
import { afterEach, describe, expect, vi } from "vitest";
import { env } from "~/env.server";
import { chatSnapshotStoragePathForSession } from "~/services/realtime/chatSnapshot.server";
import { generatePresignedUrl } from "~/v3/objectStore.server";

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

// ── Helpers ────────────────────────────────────────────────────────────

function makeSnapshot(
  opts: { messages?: UIMessage[]; lastOutEventId?: string } = {}
): ChatSnapshotV1 {
  return {
    version: 1,
    savedAt: 1_700_000_000_000,
    messages: opts.messages ?? [
      {
        id: "u-1",
        role: "user",
        parts: [{ type: "text", text: "hello" }],
      },
      {
        id: "a-1",
        role: "assistant",
        parts: [{ type: "text", text: "world" }],
      },
    ],
    lastOutEventId: opts.lastOutEventId ?? "evt-42",
  };
}

/**
 * Stub `apiClientManager.clientOrThrow()` so the SDK helpers see a fake
 * api client. Mirrors the snapshot-url route: derive the canonical
 * `sessions/{id}/snapshot.json` key (with optional default-protocol
 * prefix) and sign it via `generatePresignedUrl` against MinIO.
 */
function stubApiClient(opts: { projectRef: string; envSlug: string }) {
  vi.spyOn(apiClientManager, "clientOrThrow").mockReturnValue({
    async getChatSnapshotUrl(sessionId: string) {
      const key = chatSnapshotStoragePathForSession(sessionId);
      const result = await generatePresignedUrl(opts.projectRef, opts.envSlug, key, "GET");
      if (!result.success) throw new Error(result.error);
      return { presignedUrl: result.url };
    },
    async createChatSnapshotUploadUrl(sessionId: string) {
      const key = chatSnapshotStoragePathForSession(sessionId);
      const result = await generatePresignedUrl(opts.projectRef, opts.envSlug, key, "PUT");
      if (!result.success) throw new Error(result.error);
      return { presignedUrl: result.url };
    },
  } as never);
}

// Suppress noisy warnings from logger.warn during error-path tests.
let warnSpy: ReturnType<typeof vi.spyOn>;

afterEach(() => {
  vi.restoreAllMocks();
  warnSpy?.mockRestore();
});

// ── Tests ──────────────────────────────────────────────────────────────

describe("chat snapshot integration (MinIO + SDK helpers)", () => {
  postgresAndMinioTest("round-trips a snapshot through real MinIO", async ({ minioConfig }) => {
    env.OBJECT_STORE_BASE_URL = minioConfig.baseUrl;
    env.OBJECT_STORE_ACCESS_KEY_ID = minioConfig.accessKeyId;
    env.OBJECT_STORE_SECRET_ACCESS_KEY = minioConfig.secretAccessKey;
    env.OBJECT_STORE_REGION = minioConfig.region;
    env.OBJECT_STORE_DEFAULT_PROTOCOL = undefined;

    stubApiClient({ projectRef: "proj_snap_rt", envSlug: "dev" });

    const sessionId = "sess_round_trip_1";
    const snapshot = makeSnapshot();

    // Write through the SDK helper — should land in MinIO at
    // `packets/proj_snap_rt/dev/sessions/sess_round_trip_1/snapshot.json`.
    await writeChatSnapshot(sessionId, snapshot);

    // Read back through the SDK helper — should reconstruct the original.
    const result = await readChatSnapshot(sessionId);

    expect(result).toEqual(snapshot);
  });

  postgresAndMinioTest(
    "returns undefined for a fresh session with no snapshot",
    async ({ minioConfig }) => {
      env.OBJECT_STORE_BASE_URL = minioConfig.baseUrl;
      env.OBJECT_STORE_ACCESS_KEY_ID = minioConfig.accessKeyId;
      env.OBJECT_STORE_SECRET_ACCESS_KEY = minioConfig.secretAccessKey;
      env.OBJECT_STORE_REGION = minioConfig.region;
      env.OBJECT_STORE_DEFAULT_PROTOCOL = undefined;

      stubApiClient({ projectRef: "proj_snap_404", envSlug: "dev" });

      warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});

      // Session never had a snapshot written — read returns undefined.
      const result = await readChatSnapshot("sess_never_existed");
      expect(result).toBeUndefined();
    }
  );

  postgresAndMinioTest(
    "overwrites a prior snapshot in place (single-writer)",
    async ({ minioConfig }) => {
      // The runtime guarantees one attempt alive at a time, and
      // `writeChatSnapshot` runs awaited after `onTurnComplete`. Verify
      // that a second write to the same key replaces the first cleanly —
      // the read-after-write reflects the latest blob.
      env.OBJECT_STORE_BASE_URL = minioConfig.baseUrl;
      env.OBJECT_STORE_ACCESS_KEY_ID = minioConfig.accessKeyId;
      env.OBJECT_STORE_SECRET_ACCESS_KEY = minioConfig.secretAccessKey;
      env.OBJECT_STORE_REGION = minioConfig.region;
      env.OBJECT_STORE_DEFAULT_PROTOCOL = undefined;

      stubApiClient({ projectRef: "proj_snap_overwrite", envSlug: "dev" });

      const sessionId = "sess_overwrite";

      const turn1 = makeSnapshot({
        messages: [{ id: "u-1", role: "user", parts: [{ type: "text", text: "first" }] }],
        lastOutEventId: "evt-turn1",
      });
      const turn2 = makeSnapshot({
        messages: [
          { id: "u-1", role: "user", parts: [{ type: "text", text: "first" }] },
          { id: "a-1", role: "assistant", parts: [{ type: "text", text: "reply-1" }] },
          { id: "u-2", role: "user", parts: [{ type: "text", text: "second" }] },
          { id: "a-2", role: "assistant", parts: [{ type: "text", text: "reply-2" }] },
        ],
        lastOutEventId: "evt-turn2",
      });

      await writeChatSnapshot(sessionId, turn1);
      await writeChatSnapshot(sessionId, turn2);

      const result = await readChatSnapshot(sessionId);
      expect(result).toEqual(turn2);
      expect(result?.messages).toHaveLength(4);
      expect(result?.lastOutEventId).toBe("evt-turn2");
    }
  );

  postgresAndMinioTest(
    "isolates snapshots by sessionId (no cross-talk)",
    async ({ minioConfig }) => {
      env.OBJECT_STORE_BASE_URL = minioConfig.baseUrl;
      env.OBJECT_STORE_ACCESS_KEY_ID = minioConfig.accessKeyId;
      env.OBJECT_STORE_SECRET_ACCESS_KEY = minioConfig.secretAccessKey;
      env.OBJECT_STORE_REGION = minioConfig.region;
      env.OBJECT_STORE_DEFAULT_PROTOCOL = undefined;

      stubApiClient({ projectRef: "proj_snap_iso", envSlug: "dev" });

      const sessA = "sess_iso_A";
      const sessB = "sess_iso_B";
      const snapA = makeSnapshot({ lastOutEventId: "evt-A" });
      const snapB = makeSnapshot({ lastOutEventId: "evt-B" });

      await writeChatSnapshot(sessA, snapA);
      await writeChatSnapshot(sessB, snapB);

      const readA = await readChatSnapshot(sessA);
      const readB = await readChatSnapshot(sessB);

      expect(readA?.lastOutEventId).toBe("evt-A");
      expect(readB?.lastOutEventId).toBe("evt-B");
      // Distinct objects — modifying one shouldn't affect the other.
      expect(readA?.lastOutEventId).not.toBe(readB?.lastOutEventId);
    }
  );

  postgresAndMinioTest(
    "handles snapshots with large message lists (~50 messages)",
    async ({ minioConfig }) => {
      // Stress test: a 50-turn chat snapshot. Plan F.4 mentions the
      // pre-change baseline grew past 512 KiB around turn 10-30 with tool
      // use; the post-slim wire keeps wire payloads small but the snapshot
      // itself can still get large. Verify the helpers handle a realistic
      // payload size.
      env.OBJECT_STORE_BASE_URL = minioConfig.baseUrl;
      env.OBJECT_STORE_ACCESS_KEY_ID = minioConfig.accessKeyId;
      env.OBJECT_STORE_SECRET_ACCESS_KEY = minioConfig.secretAccessKey;
      env.OBJECT_STORE_REGION = minioConfig.region;
      env.OBJECT_STORE_DEFAULT_PROTOCOL = undefined;

      stubApiClient({ projectRef: "proj_snap_big", envSlug: "dev" });

      const messages: UIMessage[] = [];
      for (let i = 0; i < 50; i++) {
        messages.push({
          id: `u-${i}`,
          role: "user",
          parts: [{ type: "text", text: `user message ${i}: ${"x".repeat(200)}` }],
        });
        messages.push({
          id: `a-${i}`,
          role: "assistant",
          parts: [{ type: "text", text: `assistant reply ${i}: ${"y".repeat(500)}` }],
        });
      }
      const snapshot = makeSnapshot({ messages, lastOutEventId: "evt-50" });

      await writeChatSnapshot("sess_big_chat", snapshot);
      const result = await readChatSnapshot("sess_big_chat");

      expect(result).toBeDefined();
      expect(result!.messages).toHaveLength(100);
      expect(result!.lastOutEventId).toBe("evt-50");
      // Spot-check ordering integrity — the messages array round-tripped
      // in the same order.
      expect(result!.messages[0]!.id).toBe("u-0");
      expect(result!.messages[99]!.id).toBe("a-49");
    }
  );
});
