// Import the test entry point first so the resource catalog is installed —
// not strictly required for these helper-level tests, but keeps parity with
// the rest of the test suite and removes a potential foot-gun if a future
// edit introduces a chat.agent({...}) at module scope.
import "../src/v3/test/index.js";

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { apiClientManager } from "@trigger.dev/core/v3";
import {
  __readChatSnapshotProductionPathForTests as readChatSnapshot,
  __writeChatSnapshotProductionPathForTests as writeChatSnapshot,
  type ChatSnapshotV1,
} from "../src/v3/ai.js";

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

/**
 * Build a minimal ChatSnapshotV1 with `count` user messages. Used as the
 * production-path test payload — `messages` is the only field the runtime
 * inspects beyond `version`.
 */
function buildSnapshot(count = 1): ChatSnapshotV1 {
  return {
    version: 1,
    savedAt: 1_000_000,
    messages: Array.from({ length: count }, (_, i) => ({
      id: `m${i}`,
      role: "user" as const,
      parts: [{ type: "text" as const, text: `hello ${i}` }],
    })),
    lastOutEventId: "evt-42",
  };
}

/**
 * Stub `apiClientManager.clientOrThrow()` so the helpers see a fake API
 * client whose `getChatSnapshotUrl` / `createChatSnapshotUploadUrl` resolve
 * with the presigned URLs the test wants. Returns spies for assertion.
 */
function stubApiClient(opts: {
  getChatSnapshotUrl?: (sessionId: string) => Promise<{ presignedUrl: string }>;
  createChatSnapshotUploadUrl?: (sessionId: string) => Promise<{ presignedUrl: string }>;
}) {
  const getChatSnapshotUrl = vi.fn(
    opts.getChatSnapshotUrl ??
      (async (_sessionId: string) => ({ presignedUrl: "https://example.invalid/get" }))
  );
  const createChatSnapshotUploadUrl = vi.fn(
    opts.createChatSnapshotUploadUrl ??
      (async (_sessionId: string) => ({ presignedUrl: "https://example.invalid/put" }))
  );
  const fakeClient = {
    getChatSnapshotUrl,
    createChatSnapshotUploadUrl,
  };
  vi.spyOn(apiClientManager, "clientOrThrow").mockReturnValue(fakeClient as never);
  return { getChatSnapshotUrl, createChatSnapshotUploadUrl };
}

/**
 * Stub global `fetch` so the helpers see whatever Response (or throw) the
 * test wants. Returns a spy keyed on the URL passed.
 */
function stubFetch(impl: (url: string, init?: RequestInit) => Promise<Response> | Response) {
  const spy = vi.fn(impl);
  vi.stubGlobal("fetch", spy);
  return spy;
}

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

describe("chat snapshot helpers", () => {
  // Suppress the runtime's `logger.warn` calls — they pollute output but
  // don't change test outcomes. Restored in afterEach.
  let warnSpy: ReturnType<typeof vi.spyOn>;

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

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

  describe("readChatSnapshot", () => {
    it("returns the snapshot on a successful GET", async () => {
      const { getChatSnapshotUrl } = stubApiClient({});
      const snapshot = buildSnapshot(2);
      stubFetch(
        async () =>
          new Response(JSON.stringify(snapshot), {
            status: 200,
            headers: { "content-type": "application/json" },
          })
      );

      const result = await readChatSnapshot("session-1");
      expect(getChatSnapshotUrl).toHaveBeenCalledWith("session-1");
      expect(result).toMatchObject({
        version: 1,
        messages: snapshot.messages,
        lastOutEventId: "evt-42",
      });
    });

    it("returns undefined on 404 (fresh session, no snapshot yet)", async () => {
      stubApiClient({});
      stubFetch(async () => new Response("Not Found", { status: 404 }));

      const result = await readChatSnapshot("missing-session");
      expect(result).toBeUndefined();
    });

    it("returns undefined on non-404 non-OK (e.g. 500)", async () => {
      stubApiClient({});
      stubFetch(async () => new Response("Internal Error", { status: 500 }));

      const result = await readChatSnapshot("flaky-session");
      expect(result).toBeUndefined();
    });

    it("returns undefined when the response body is malformed JSON", async () => {
      stubApiClient({});
      stubFetch(
        async () =>
          new Response("not-json-{[", {
            status: 200,
            headers: { "content-type": "application/json" },
          })
      );

      const result = await readChatSnapshot("malformed-session");
      expect(result).toBeUndefined();
    });

    it("returns undefined on version mismatch (forward-compat)", async () => {
      stubApiClient({});
      // Future format the current runtime can't decode — runtime ignores it.
      const futureSnapshot = {
        version: 99,
        savedAt: Date.now(),
        messages: [],
      };
      stubFetch(
        async () =>
          new Response(JSON.stringify(futureSnapshot), {
            status: 200,
            headers: { "content-type": "application/json" },
          })
      );

      const result = await readChatSnapshot("v99-session");
      expect(result).toBeUndefined();
    });

    it("returns undefined when `messages` field is missing or wrong type", async () => {
      stubApiClient({});
      stubFetch(
        async () =>
          new Response(JSON.stringify({ version: 1, savedAt: 1, messages: "not-an-array" }), {
            status: 200,
          })
      );

      const result = await readChatSnapshot("bad-shape-session");
      expect(result).toBeUndefined();
    });

    it("returns undefined when fetch throws (network error)", async () => {
      stubApiClient({});
      stubFetch(async () => {
        throw new Error("ECONNREFUSED");
      });

      const result = await readChatSnapshot("offline-session");
      expect(result).toBeUndefined();
    });

    it("returns undefined when presign call fails", async () => {
      stubApiClient({
        getChatSnapshotUrl: async () => {
          throw new Error("presign denied");
        },
      });
      // No fetch should fire — presign failed.
      const fetchSpy = stubFetch(async () => new Response("nope", { status: 500 }));

      const result = await readChatSnapshot("denied-session");
      expect(result).toBeUndefined();
      expect(fetchSpy).not.toHaveBeenCalled();
    });

    it("returns undefined when the response is not an object", async () => {
      stubApiClient({});
      stubFetch(async () => new Response(JSON.stringify("just-a-string"), { status: 200 }));

      const result = await readChatSnapshot("string-response");
      expect(result).toBeUndefined();
    });
  });

  describe("writeChatSnapshot", () => {
    it("PUTs the snapshot JSON to the presigned URL", async () => {
      const { createChatSnapshotUploadUrl } = stubApiClient({});
      const fetchSpy = stubFetch(async () => new Response(null, { status: 200 }));

      const snapshot = buildSnapshot(3);
      await writeChatSnapshot("session-2", snapshot);

      expect(createChatSnapshotUploadUrl).toHaveBeenCalledWith("session-2");
      expect(fetchSpy).toHaveBeenCalledOnce();
      const [url, init] = fetchSpy.mock.calls[0]!;
      expect(url).toBe("https://example.invalid/put");
      expect((init as RequestInit).method).toBe("PUT");
      expect((init as RequestInit).headers).toMatchObject({
        "content-type": "application/json",
      });
      // Body is the JSON-stringified snapshot — round-trip to confirm.
      const sentBody = JSON.parse((init as RequestInit).body as string);
      expect(sentBody).toEqual(snapshot);
    });

    it("returns without throwing on a non-OK PUT response (warns)", async () => {
      stubApiClient({});
      stubFetch(async () => new Response("forbidden", { status: 403 }));

      await expect(
        writeChatSnapshot("forbidden-session", buildSnapshot())
      ).resolves.toBeUndefined();
    });

    it("returns without throwing on a fetch network error (warns)", async () => {
      stubApiClient({});
      stubFetch(async () => {
        throw new Error("ETIMEDOUT");
      });

      await expect(writeChatSnapshot("timeout-session", buildSnapshot())).resolves.toBeUndefined();
    });

    it("returns without throwing when presign fails (warns)", async () => {
      stubApiClient({
        createChatSnapshotUploadUrl: async () => {
          throw new Error("presign denied");
        },
      });
      const fetchSpy = stubFetch(async () => new Response(null, { status: 200 }));

      await expect(writeChatSnapshot("denied-session", buildSnapshot())).resolves.toBeUndefined();
      // Presign failed → no PUT attempted.
      expect(fetchSpy).not.toHaveBeenCalled();
    });

    it("addresses reads and writes by the same sessionId", async () => {
      // Round-trip check: both presign methods receive the same sessionId.
      // The canonical key (`sessions/{id}/snapshot.json`) lives server-side
      // now, so the SDK has no key string to compare — sessionId equality
      // is the SDK-visible invariant.
      const { getChatSnapshotUrl } = stubApiClient({
        getChatSnapshotUrl: async () => ({ presignedUrl: "https://example.invalid/get" }),
      });
      stubFetch(async () => new Response(null, { status: 404 }));

      await readChatSnapshot("round-trip-session");
      const [readArg] = getChatSnapshotUrl.mock.calls[0]!;

      const { createChatSnapshotUploadUrl } = stubApiClient({
        createChatSnapshotUploadUrl: async () => ({ presignedUrl: "https://example.invalid/put" }),
      });
      stubFetch(async () => new Response(null, { status: 200 }));
      await writeChatSnapshot("round-trip-session", buildSnapshot());
      const [writeArg] = createChatSnapshotUploadUrl.mock.calls[0]!;

      expect(readArg).toBe(writeArg);
      expect(readArg).toBe("round-trip-session");
    });
  });
});
