// Import the test entry point first so the resource catalog is installed.
import "../src/v3/test/index.js";

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { apiClientManager } from "@trigger.dev/core/v3";
import {
  __findLatestSessionInCursorForTests as findLatestSessionInCursor,
  __replaySessionInTailProductionPathForTests as replaySessionInTail,
} from "../src/v3/ai.js";

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

function userMessage(id: string, text: string) {
  return {
    id,
    role: "user" as const,
    parts: [{ type: "text" as const, text }],
  };
}

function stubReadRecords(chunks: unknown[]) {
  const records = chunks.map((chunk, i) => ({
    data: chunk,
    id: `evt-${i + 1}`,
    seqNum: i + 1,
  }));
  const spy = vi.fn(async () => ({ records }));
  vi.spyOn(apiClientManager, "clientOrThrow").mockReturnValue({
    readSessionStreamRecords: spy,
  } as never);
  return spy;
}

beforeEach(() => {
  vi.restoreAllMocks();
});

afterEach(() => {
  vi.restoreAllMocks();
});

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

describe("replaySessionInTail", () => {
  it("extracts user messages from kind: 'message' records with submit-message trigger", async () => {
    const u1 = userMessage("u-1", "hello");
    const u2 = userMessage("u-2", "again");
    stubReadRecords([
      {
        kind: "message",
        payload: {
          chatId: "c1",
          trigger: "submit-message",
          message: u1,
          metadata: { userId: "a" },
        },
      },
      {
        kind: "message",
        payload: {
          chatId: "c1",
          trigger: "submit-message",
          message: u2,
          metadata: { userId: "b" },
        },
      },
    ]);

    const result = await replaySessionInTail("sess");
    expect(result).toHaveLength(2);
    expect(result[0]!.message.id).toBe("u-1");
    expect(result[0]!.seqNum).toBe(1);
    expect(result[0]!.metadata).toEqual({ userId: "a" });
    expect(result[1]!.message.id).toBe("u-2");
    expect(result[1]!.seqNum).toBe(2);
    expect(result[1]!.metadata).toEqual({ userId: "b" });
  });

  it("ignores non-message variants (stop, handover, handover-skip)", async () => {
    const u1 = userMessage("u-1", "real user");
    stubReadRecords([
      { kind: "stop", message: "user stopped" },
      { kind: "handover-skip" },
      { kind: "handover", partialAssistantMessage: [], isFinal: false },
      { kind: "message", payload: { chatId: "c1", trigger: "submit-message", message: u1 } },
    ]);

    const result = await replaySessionInTail("sess");
    expect(result).toHaveLength(1);
    expect(result[0]!.message.id).toBe("u-1");
  });

  it("ignores message records that aren't submit-message", async () => {
    // regenerate-message / preload / close / action / handover-prepare don't
    // carry a user message — the chain reconstruction must skip them.
    stubReadRecords([
      { kind: "message", payload: { chatId: "c1", trigger: "regenerate-message" } },
      { kind: "message", payload: { chatId: "c1", trigger: "preload" } },
      { kind: "message", payload: { chatId: "c1", trigger: "close" } },
      { kind: "message", payload: { chatId: "c1", trigger: "action", action: { foo: 1 } } },
    ]);

    const result = await replaySessionInTail("sess");
    expect(result).toHaveLength(0);
  });

  it("ignores records whose payload is missing or empty", async () => {
    stubReadRecords([
      { kind: "message" }, // no payload
      { kind: "message", payload: { chatId: "c1", trigger: "submit-message" } }, // no message
      { kind: "message", payload: { chatId: "c1", trigger: "submit-message", message: null } },
      {
        kind: "message",
        payload: { chatId: "c1", trigger: "submit-message", message: "not-an-object" },
      },
    ]);

    const result = await replaySessionInTail("sess");
    expect(result).toHaveLength(0);
  });

  it("skips non-object record data defensively", async () => {
    const u1 = userMessage("u-1", "valid");
    stubReadRecords([
      42,
      null,
      "string-data",
      { kind: "message", payload: { chatId: "c1", trigger: "submit-message", message: u1 } },
    ]);

    const result = await replaySessionInTail("sess");
    expect(result).toHaveLength(1);
    expect(result[0]!.message.id).toBe("u-1");
  });

  it("passes the afterEventId cursor through to readSessionStreamRecords", async () => {
    const spy = stubReadRecords([]);

    await replaySessionInTail("sess", { lastEventId: "evt-42" });

    expect(spy).toHaveBeenCalledWith("sess", "in", { afterEventId: "evt-42" });
  });

  it("returns an empty list when the records endpoint returns no records", async () => {
    stubReadRecords([]);

    const result = await replaySessionInTail("sess");
    expect(result).toEqual([]);
  });
});

// ── Cursor scan (non-blocking records read, not an SSE drain) ──────────

function stubReadRecordsWithHeaders(
  records: Array<{ data?: unknown; headers?: Array<[string, string]> }>
) {
  const shaped = records.map((r, i) => ({
    data: r.data ?? "",
    id: `evt-${i + 1}`,
    seqNum: i + 1,
    headers: r.headers,
  }));
  const spy = vi.fn(async () => ({ records: shaped }));
  vi.spyOn(apiClientManager, "clientOrThrow").mockReturnValue({
    readSessionStreamRecords: spy,
  } as never);
  return spy;
}

describe("findLatestSessionInCursor", () => {
  it("returns the LAST turn-complete's session-in-event-id", async () => {
    const spy = stubReadRecordsWithHeaders([
      { data: { type: "text-delta", delta: "hi" } },
      {
        headers: [
          ["trigger-control", "turn-complete"],
          ["session-in-event-id", "3"],
        ],
      },
      { data: { type: "text-delta", delta: "again" } },
      {
        headers: [
          ["trigger-control", "turn-complete"],
          ["session-in-event-id", "7"],
        ],
      },
    ]);

    const cursor = await findLatestSessionInCursor("sess");
    expect(cursor).toBe(7);
    // Non-blocking records read on `.out`, no SSE subscribe.
    expect(spy).toHaveBeenCalledWith("sess", "out");
  });

  it("ignores other control subtypes and turn-completes without the header", async () => {
    stubReadRecordsWithHeaders([
      {
        headers: [
          ["trigger-control", "upgrade-required"],
          ["session-in-event-id", "99"],
        ],
      },
      { headers: [["trigger-control", "turn-complete"]] },
      {
        headers: [
          ["trigger-control", "turn-complete"],
          ["session-in-event-id", "4"],
        ],
      },
    ]);

    const cursor = await findLatestSessionInCursor("sess");
    expect(cursor).toBe(4);
  });

  it("returns undefined when records carry no headers (older server)", async () => {
    stubReadRecordsWithHeaders([
      { data: { type: "text-delta", delta: "hi" } },
      { data: { type: "finish" } },
    ]);

    const cursor = await findLatestSessionInCursor("sess");
    expect(cursor).toBeUndefined();
  });
});
