import {
  ChatErrorCode,
  CLAUDE_CLIENT_FILTER,
  CLAUDE_CLIENT_ID,
  CLAUDE_CODE_CLIENT_ID,
} from "@archestra/shared";
import { beforeEach, describe, expect, test } from "@/test";
import type { InsertInteraction } from "@/types";
import { SelectInteractionSchema } from "@/types";
import AgentModel from "./agent";
import AgentTeamModel from "./agent-team";
import ConversationModel from "./conversation";
import ConversationChatErrorModel from "./conversation-chat-error";
import EnvironmentModel from "./environment";
import InteractionModel from "./interaction";
import InteractionDeltaManager from "./interaction-delta-manager";
import LimitModel from "./limit";
import TeamModel from "./team";

describe("InteractionModel", () => {
  let profileId: string;

  beforeEach(async ({ makeAgent }) => {
    // Create test profile
    const agent = await makeAgent();
    profileId = agent.id;
  });

  describe("create", () => {
    test("can create an interaction", async () => {
      const interaction = await InteractionModel.create({
        profileId,
        request: {
          model: "gpt-4",
          messages: [{ role: "user", content: "Hello" }],
        },
        response: {
          id: "test-response",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [
            {
              index: 0,
              message: {
                role: "assistant",
                content: "Hi there",
                refusal: null,
              },
              finish_reason: "stop",
              logprobs: null,
            },
          ],
        },
        type: "openai:chatCompletions",
      });

      expect(interaction).toBeDefined();
      expect(interaction.id).toBeDefined();
      expect(interaction.profileId).toBe(profileId);
      expect(interaction.request).toBeDefined();
      expect(interaction.response).toBeDefined();
    });

    test("snapshots the agent's environment and keeps it stable across reassignment", async ({
      makeAgent,
      makeOrganization,
    }) => {
      const org = await makeOrganization();
      const envA = await EnvironmentModel.create({
        organizationId: org.id,
        name: "production",
      });
      const envB = await EnvironmentModel.create({
        organizationId: org.id,
        name: "staging",
      });
      const agent = await makeAgent({
        organizationId: org.id,
        environmentId: envA.id,
      });

      const baseInteraction = {
        request: {
          model: "gpt-4o",
          messages: [{ role: "user" as const, content: "Hello" }],
        },
        response: {
          id: "r",
          object: "chat.completion",
          created: 1,
          model: "gpt-4o",
          choices: [
            {
              index: 0,
              message: {
                role: "assistant" as const,
                content: "Hi",
                refusal: null,
              },
              finish_reason: "stop" as const,
              logprobs: null,
            },
          ],
        },
        type: "openai:chatCompletions" as const,
      };

      const first = await InteractionModel.create({
        profileId: agent.id,
        ...baseInteraction,
      });
      expect(first.environmentId).toBe(envA.id);

      // Reassigning the agent must not retroactively change the snapshot.
      await AgentModel.update(agent.id, { environmentId: envB.id });

      const second = await InteractionModel.create({
        profileId: agent.id,
        ...baseInteraction,
      });
      expect(second.environmentId).toBe(envB.id);

      const refetchedFirst = await InteractionModel.findById(first.id);
      expect(refetchedFirst?.environmentId).toBe(envA.id);
    });

    test("returns chat errors for chat conversation sessions", async ({
      makeUser,
      makeOrganization,
      makeAgent,
    }) => {
      const user = await makeUser();
      const org = await makeOrganization();
      const agent = await makeAgent({ organizationId: org.id });
      const conversation = await ConversationModel.create({
        userId: user.id,
        organizationId: org.id,
        agentId: agent.id,
      });
      await ConversationChatErrorModel.create({
        conversationId: conversation.id,
        error: {
          code: ChatErrorCode.ServerError,
          message: "Provider failed.",
          isRetryable: true,
        },
      });
      const interaction = await InteractionModel.create({
        profileId: agent.id,
        sessionId: conversation.id,
        request: {
          model: "gpt-4",
          messages: [{ role: "user", content: "Hello" }],
        },
        response: {
          id: "test-response",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [
            {
              index: 0,
              message: {
                role: "assistant",
                content: "Hi there",
                refusal: null,
              },
              finish_reason: "stop",
              logprobs: null,
            },
          ],
        },
        type: "openai:chatCompletions",
      });

      const found = await InteractionModel.findById(interaction.id);

      expect(found?.chatErrors?.map((chatError) => chatError.error)).toEqual([
        {
          code: ChatErrorCode.ServerError,
          message: "Provider failed.",
          isRetryable: true,
        },
      ]);
    });

    test("can create and serialize an Azure interaction", async () => {
      const interaction = await InteractionModel.create({
        profileId,
        request: {
          model: "gpt-4o",
          messages: [{ role: "user", content: "Hello Azure" }],
        },
        response: {
          id: "azure-response",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4o",
          choices: [
            {
              index: 0,
              message: {
                role: "assistant",
                content: "Hi from Azure",
                refusal: null,
              },
              finish_reason: "stop",
              logprobs: null,
            },
          ],
          usage: {
            prompt_tokens: 10,
            completion_tokens: 5,
            total_tokens: 15,
          },
        },
        type: "azure:chatCompletions",
      });

      const parsed = SelectInteractionSchema.safeParse(interaction);
      expect(parsed.success).toBe(true);
    });
  });

  describe("create - null byte sanitization", () => {
    test("strips null bytes from JSONB fields to avoid PostgreSQL rejection", async () => {
      // PostgreSQL JSONB rejects \u0000 (null byte) Unicode escape sequences.
      // These can appear in LLM responses, e.g. Gemini's thoughtSignature fields.
      const interaction = await InteractionModel.create({
        profileId,
        request: {
          model: "gpt-4",
          messages: [
            { role: "user", content: "Hello \u0000 world \u0000 test" },
          ],
        },
        response: {
          id: "test-nullbytes",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [
            {
              index: 0,
              message: {
                role: "assistant",
                content: "Response \u0000 with \u0000 null bytes",
                refusal: null,
              },
              finish_reason: "stop",
              logprobs: null,
            },
          ],
        },
        type: "openai:chatCompletions",
      });

      expect(interaction).toBeDefined();
      expect(interaction.id).toBeDefined();

      // Verify the null bytes were stripped by reading back from DB
      const found = await InteractionModel.findById(interaction.id);
      expect(found).not.toBeNull();

      const response = found?.response as {
        choices?: Array<{ message?: { content?: string } }>;
      };
      expect(response?.choices?.[0]?.message?.content).toBe(
        "Response  with  null bytes",
      );

      const request = found?.request as {
        messages?: Array<{ content?: string }>;
      };
      expect(request?.messages?.[0]?.content).toBe("Hello  world  test");
    });

    test("throws FK violation when profileId does not exist", async () => {
      await expect(
        InteractionModel.create({
          profileId: "00000000-0000-0000-0000-000000000000",
          request: {
            model: "gpt-4",
            messages: [{ role: "user", content: "Hello" }],
          },
          response: {
            id: "test-response",
            object: "chat.completion",
            created: Date.now(),
            model: "gpt-4",
            choices: [
              {
                index: 0,
                message: {
                  role: "assistant",
                  content: "Hi",
                  refusal: null,
                },
                finish_reason: "stop",
                logprobs: null,
              },
            ],
          },
          type: "openai:chatCompletions",
        }),
      ).rejects.toThrow();
    });

    test("handles data without null bytes unchanged", async () => {
      const interaction = await InteractionModel.create({
        profileId,
        request: {
          model: "gpt-4",
          messages: [
            { role: "user", content: "Normal text without null bytes" },
          ],
        },
        response: {
          id: "test-response",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [
            {
              index: 0,
              message: {
                role: "assistant",
                content: "Normal response",
                refusal: null,
              },
              finish_reason: "stop",
              logprobs: null,
            },
          ],
        },
        type: "openai:chatCompletions",
      });

      const found = await InteractionModel.findById(interaction.id);
      const request = found?.request as {
        messages?: Array<{ content?: string }>;
      };
      expect(request?.messages?.[0]?.content).toBe(
        "Normal text without null bytes",
      );
    });
  });

  describe("findById", () => {
    test("returns interaction by id", async () => {
      const created = await InteractionModel.create({
        profileId,
        request: {
          model: "gpt-4",
          messages: [{ role: "user", content: "Test message" }],
        },
        response: {
          id: "test-response",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [
            {
              index: 0,
              message: {
                role: "assistant",
                content: "Test response",
                refusal: null,
              },
              finish_reason: "stop",
              logprobs: null,
            },
          ],
        },
        type: "openai:chatCompletions",
      });

      const found = await InteractionModel.findById(created.id);
      expect(found).toBeDefined();
      expect(found?.id).toBe(created.id);
    });

    test("returns null for non-existent id", async () => {
      const found = await InteractionModel.findById(
        "00000000-0000-0000-0000-000000000000",
      );
      expect(found).toBeNull();
    });
  });

  describe("getAllInteractionsForProfile", () => {
    test("returns all interactions for a specific agent", async () => {
      // Create another agent
      const otherAgent = await AgentModel.create({
        name: "Other Agent",
        teams: [],
        scope: "org",
      });

      // Create interactions for both agents
      await InteractionModel.create({
        profileId,
        request: {
          model: "gpt-4",
          messages: [{ role: "user", content: "Agent 1 message" }],
        },
        response: {
          id: "response-1",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [
            {
              index: 0,
              message: {
                role: "assistant",
                content: "Agent 1 response",
                refusal: null,
              },
              finish_reason: "stop",
              logprobs: null,
            },
          ],
        },
        type: "openai:chatCompletions",
      });

      await InteractionModel.create({
        profileId: otherAgent.id,
        request: {
          model: "gpt-4",
          messages: [{ role: "user", content: "Agent 2 message" }],
        },
        response: {
          id: "response-2",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [
            {
              index: 0,
              message: {
                role: "assistant",
                content: "Agent 2 response",
                refusal: null,
              },
              finish_reason: "stop",
              logprobs: null,
            },
          ],
        },
        type: "openai:chatCompletions",
      });

      const agentInteractions =
        await InteractionModel.getAllInteractionsForProfile(profileId);
      expect(agentInteractions).toHaveLength(1);
      expect(agentInteractions[0].profileId).toBe(profileId);
    });
  });

  describe("Access Control", () => {
    test("admin can see all interactions", async ({ makeAdmin }) => {
      const admin = await makeAdmin();

      const agent1 = await AgentModel.create({
        name: "Agent 1",
        teams: [],
        scope: "org",
      });
      const agent2 = await AgentModel.create({
        name: "Agent 2",
        teams: [],
        scope: "org",
      });

      await InteractionModel.create({
        profileId: agent1.id,
        request: { model: "gpt-4", messages: [] },
        response: {
          id: "r1",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      await InteractionModel.create({
        profileId: agent2.id,
        request: { model: "gpt-4", messages: [] },
        response: {
          id: "r2",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      const interactions = await InteractionModel.findAllPaginated(
        { limit: 100, offset: 0 },
        undefined,
        admin.id,
        true,
      );
      expect(interactions.data).toHaveLength(2);
    });

    test("member only sees interactions for accessible profiles", async ({
      makeUser,
      makeAdmin,
      makeOrganization,
      makeTeam,
    }) => {
      const user1 = await makeUser();
      const user2 = await makeUser();
      const admin = await makeAdmin();
      const org = await makeOrganization();

      // Create teams and add users
      const team1 = await makeTeam(org.id, admin.id, { name: "Team 1" });
      await TeamModel.addMember(team1.id, user1.id);

      const team2 = await makeTeam(org.id, admin.id, { name: "Team 2" });
      await TeamModel.addMember(team2.id, user2.id);

      // Create agents with team assignments
      const agent1 = await AgentModel.create({
        name: "Agent 1",
        teams: [team1.id],
        scope: "team",
      });
      const agent2 = await AgentModel.create({
        name: "Agent 2",
        teams: [team2.id],
        scope: "team",
      });

      await InteractionModel.create({
        profileId: agent1.id,
        request: { model: "gpt-4", messages: [] },
        response: {
          id: "r1",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      await InteractionModel.create({
        profileId: agent2.id,
        request: { model: "gpt-4", messages: [] },
        response: {
          id: "r2",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      const interactions = await InteractionModel.findAllPaginated(
        { limit: 100, offset: 0 },
        undefined,
        user1.id,
        false,
      );
      expect(interactions.data).toHaveLength(1);
      expect(interactions.data[0].profileId).toBe(agent1.id);
    });

    test("member with no access sees only org-wide agent interactions", async ({
      makeUser,
    }) => {
      const user = await makeUser();

      // Teamless agent is org-wide, visible to all members
      const agent1 = await AgentModel.create({
        name: "Agent 1",
        teams: [],
        scope: "org",
      });

      await InteractionModel.create({
        profileId: agent1.id,
        request: { model: "gpt-4", messages: [] },
        response: {
          id: "r1",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      const interactions = await InteractionModel.findAllPaginated(
        { limit: 100, offset: 0 },
        undefined,
        user.id,
        false,
      );
      // Org-wide agents are visible to all members
      expect(interactions.data).toHaveLength(1);
      expect(interactions.data[0].profileId).toBe(agent1.id);
    });

    test("findById returns interaction for admin", async ({ makeAdmin }) => {
      const admin = await makeAdmin();

      const agent = await AgentModel.create({
        name: "Test Agent",
        teams: [],
        scope: "org",
      });

      const interaction = await InteractionModel.create({
        profileId: agent.id,
        request: { model: "gpt-4", messages: [] },
        response: {
          id: "r1",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      const found = await InteractionModel.findById(
        interaction.id,
        admin.id,
        true,
      );
      expect(found).not.toBeNull();
      expect(found?.id).toBe(interaction.id);
    });

    test("findById returns interaction for user with profile access", async ({
      makeUser,
      makeAdmin,
      makeOrganization,
      makeTeam,
    }) => {
      const user = await makeUser();
      const admin = await makeAdmin();
      const org = await makeOrganization();

      // Create team and add user
      const team = await makeTeam(org.id, admin.id);
      await TeamModel.addMember(team.id, user.id);

      const agent = await AgentModel.create({
        name: "Test Agent",
        teams: [team.id],
        scope: "team",
      });

      const interaction = await InteractionModel.create({
        profileId: agent.id,
        request: { model: "gpt-4", messages: [] },
        response: {
          id: "r1",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      const found = await InteractionModel.findById(
        interaction.id,
        user.id,
        false,
      );
      expect(found).not.toBeNull();
      expect(found?.id).toBe(interaction.id);
    });

    test("findById returns interaction for org-wide agent", async ({
      makeUser,
    }) => {
      const user = await makeUser();

      // Teamless agent is org-wide
      const agent = await AgentModel.create({
        name: "Test Agent",
        teams: [],
        scope: "org",
      });

      const interaction = await InteractionModel.create({
        profileId: agent.id,
        request: { model: "gpt-4", messages: [] },
        response: {
          id: "r1",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      const found = await InteractionModel.findById(
        interaction.id,
        user.id,
        false,
      );
      // Org-wide agents are accessible to all members
      expect(found).not.toBeNull();
      expect(found?.id).toBe(interaction.id);
    });
  });

  describe("findAllPaginated filters", () => {
    test("filters by profileId", async ({ makeAdmin }) => {
      const admin = await makeAdmin();

      const agent1 = await AgentModel.create({
        name: "Agent 1",
        teams: [],
        scope: "org",
      });
      const agent2 = await AgentModel.create({
        name: "Agent 2",
        teams: [],
        scope: "org",
      });

      await InteractionModel.create({
        profileId: agent1.id,
        request: { model: "gpt-4", messages: [] },
        response: {
          id: "r1",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      await InteractionModel.create({
        profileId: agent2.id,
        request: { model: "gpt-4", messages: [] },
        response: {
          id: "r2",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      const interactions = await InteractionModel.findAllPaginated(
        { limit: 100, offset: 0 },
        undefined,
        admin.id,
        true,
        { profileId: agent1.id },
      );

      expect(interactions.data).toHaveLength(1);
      expect(interactions.data[0].profileId).toBe(agent1.id);
    });

    test("filters by externalAgentId", async ({ makeAdmin }) => {
      const admin = await makeAdmin();

      const agent = await AgentModel.create({
        name: "Agent",
        teams: [],
        scope: "org",
      });

      await InteractionModel.create({
        profileId: agent.id,
        externalAgentId: "my-app-prod",
        request: { model: "gpt-4", messages: [] },
        response: {
          id: "r1",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      await InteractionModel.create({
        profileId: agent.id,
        externalAgentId: "my-app-staging",
        request: { model: "gpt-4", messages: [] },
        response: {
          id: "r2",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      await InteractionModel.create({
        profileId: agent.id,
        // No externalAgentId
        request: { model: "gpt-4", messages: [] },
        response: {
          id: "r3",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      const interactions = await InteractionModel.findAllPaginated(
        { limit: 100, offset: 0 },
        undefined,
        admin.id,
        true,
        { externalAgentId: "my-app-prod" },
      );

      expect(interactions.data).toHaveLength(1);
      expect(interactions.data[0].externalAgentId).toBe("my-app-prod");
    });

    test("filters by both profileId and externalAgentId", async ({
      makeAdmin,
    }) => {
      const admin = await makeAdmin();

      const agent1 = await AgentModel.create({
        name: "Agent 1",
        teams: [],
        scope: "org",
      });
      const agent2 = await AgentModel.create({
        name: "Agent 2",
        teams: [],
        scope: "org",
      });

      // Agent 1 with external ID
      await InteractionModel.create({
        profileId: agent1.id,
        externalAgentId: "my-app",
        request: { model: "gpt-4", messages: [] },
        response: {
          id: "r1",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      // Agent 1 without external ID
      await InteractionModel.create({
        profileId: agent1.id,
        request: { model: "gpt-4", messages: [] },
        response: {
          id: "r2",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      // Agent 2 with same external ID
      await InteractionModel.create({
        profileId: agent2.id,
        externalAgentId: "my-app",
        request: { model: "gpt-4", messages: [] },
        response: {
          id: "r3",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      const interactions = await InteractionModel.findAllPaginated(
        { limit: 100, offset: 0 },
        undefined,
        admin.id,
        true,
        { profileId: agent1.id, externalAgentId: "my-app" },
      );

      expect(interactions.data).toHaveLength(1);
      expect(interactions.data[0].profileId).toBe(agent1.id);
      expect(interactions.data[0].externalAgentId).toBe("my-app");
    });

    test("filters respect access control for non-admin users", async ({
      makeUser,
      makeAdmin,
      makeOrganization,
      makeTeam,
    }) => {
      const user = await makeUser();
      const admin = await makeAdmin();
      const org = await makeOrganization();

      const team = await makeTeam(org.id, admin.id);
      await TeamModel.addMember(team.id, user.id);

      const accessibleAgent = await AgentModel.create({
        name: "Accessible Agent",
        teams: [team.id],
        scope: "team",
      });
      // Org-wide agent (no teams) is also accessible
      const orgWideAgent = await AgentModel.create({
        name: "Org-Wide Agent",
        teams: [],
        scope: "org",
      });

      // Interaction for team-scoped agent
      await InteractionModel.create({
        profileId: accessibleAgent.id,
        externalAgentId: "my-app",
        request: { model: "gpt-4", messages: [] },
        response: {
          id: "r1",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      // Interaction for org-wide agent with same external ID
      await InteractionModel.create({
        profileId: orgWideAgent.id,
        externalAgentId: "my-app",
        request: { model: "gpt-4", messages: [] },
        response: {
          id: "r2",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      // User sees both: team-scoped + org-wide agent interactions
      const interactions = await InteractionModel.findAllPaginated(
        { limit: 100, offset: 0 },
        undefined,
        user.id,
        false,
        { externalAgentId: "my-app" },
      );

      expect(interactions.data).toHaveLength(2);
    });

    test("filters by userId", async ({ makeAdmin, makeUser }) => {
      const admin = await makeAdmin();
      const user1 = await makeUser();
      const user2 = await makeUser();

      const agent = await AgentModel.create({
        name: "Agent",
        teams: [],
        scope: "org",
      });

      // Interaction with user1
      await InteractionModel.create({
        profileId: agent.id,
        userId: user1.id,
        request: { model: "gpt-4", messages: [] },
        response: {
          id: "r1",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      // Interaction with user2
      await InteractionModel.create({
        profileId: agent.id,
        userId: user2.id,
        request: { model: "gpt-4", messages: [] },
        response: {
          id: "r2",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      // Interaction without userId
      await InteractionModel.create({
        profileId: agent.id,
        request: { model: "gpt-4", messages: [] },
        response: {
          id: "r3",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      const interactions = await InteractionModel.findAllPaginated(
        { limit: 100, offset: 0 },
        undefined,
        admin.id,
        true,
        { userId: user1.id },
      );

      expect(interactions.data).toHaveLength(1);
      expect(interactions.data[0].userId).toBe(user1.id);
    });
  });

  describe("date range filtering", () => {
    test("filters by startDate", async ({ makeAdmin }) => {
      const admin = await makeAdmin();
      const agent = await AgentModel.create({
        name: "Agent",
        teams: [],
        scope: "org",
      });

      // Create interactions with different timestamps
      const now = new Date();
      const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
      const twoDaysAgo = new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000);

      // Interaction from 2 days ago
      await InteractionModel.create({
        profileId: agent.id,
        request: { model: "gpt-4", messages: [] },
        response: {
          id: "r1",
          object: "chat.completion",
          created: twoDaysAgo.getTime(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      // Interaction from yesterday
      await InteractionModel.create({
        profileId: agent.id,
        request: { model: "gpt-4", messages: [] },
        response: {
          id: "r2",
          object: "chat.completion",
          created: yesterday.getTime(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      // Filter for interactions from yesterday onwards
      const interactions = await InteractionModel.findAllPaginated(
        { limit: 100, offset: 0 },
        undefined,
        admin.id,
        true,
        { startDate: yesterday },
      );

      // Should include yesterday's interaction and possibly the one just created
      expect(interactions.data.length).toBeGreaterThanOrEqual(1);
    });

    test("filters by endDate", async ({ makeAdmin }) => {
      const admin = await makeAdmin();
      const agent = await AgentModel.create({
        name: "Agent",
        teams: [],
        scope: "org",
      });

      // Create interactions
      await InteractionModel.create({
        profileId: agent.id,
        request: { model: "gpt-4", messages: [] },
        response: {
          id: "r1",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      // Filter for interactions before a past date (should exclude all current interactions)
      const pastDate = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
      const interactions = await InteractionModel.findAllPaginated(
        { limit: 100, offset: 0 },
        undefined,
        admin.id,
        true,
        { endDate: pastDate },
      );

      // Should not include the just-created interaction
      expect(
        interactions.data.every(
          (i) => new Date(i.createdAt).getTime() <= pastDate.getTime(),
        ),
      ).toBe(true);
    });

    test("filters by date range (startDate and endDate)", async ({
      makeAdmin,
    }) => {
      const admin = await makeAdmin();
      const agent = await AgentModel.create({
        name: "Agent",
        teams: [],
        scope: "org",
      });

      // Create an interaction
      await InteractionModel.create({
        profileId: agent.id,
        request: { model: "gpt-4", messages: [] },
        response: {
          id: "r1",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      // Filter for interactions in a date range that includes now
      const startDate = new Date(Date.now() - 24 * 60 * 60 * 1000); // yesterday
      const endDate = new Date(Date.now() + 24 * 60 * 60 * 1000); // tomorrow

      const interactions = await InteractionModel.findAllPaginated(
        { limit: 100, offset: 0 },
        undefined,
        admin.id,
        true,
        { startDate, endDate },
      );

      expect(interactions.data.length).toBeGreaterThanOrEqual(1);
      expect(
        interactions.data.every((i) => {
          const createdAt = new Date(i.createdAt).getTime();
          return (
            createdAt >= startDate.getTime() && createdAt <= endDate.getTime()
          );
        }),
      ).toBe(true);
    });

    test("date filter works with other filters", async ({ makeAdmin }) => {
      const admin = await makeAdmin();
      const agent1 = await AgentModel.create({
        name: "Agent 1",
        teams: [],
        scope: "org",
      });
      const agent2 = await AgentModel.create({
        name: "Agent 2",
        teams: [],
        scope: "org",
      });

      // Create interactions for both agents
      await InteractionModel.create({
        profileId: agent1.id,
        request: { model: "gpt-4", messages: [] },
        response: {
          id: "r1",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      await InteractionModel.create({
        profileId: agent2.id,
        request: { model: "gpt-4", messages: [] },
        response: {
          id: "r2",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      // Filter by profileId and date range
      const startDate = new Date(Date.now() - 24 * 60 * 60 * 1000);
      const endDate = new Date(Date.now() + 24 * 60 * 60 * 1000);

      const interactions = await InteractionModel.findAllPaginated(
        { limit: 100, offset: 0 },
        undefined,
        admin.id,
        true,
        { profileId: agent1.id, startDate, endDate },
      );

      expect(interactions.data).toHaveLength(1);
      expect(interactions.data[0].profileId).toBe(agent1.id);
    });
  });

  describe("getSessions date filtering", () => {
    test("filters sessions by date range", async ({ makeAdmin }) => {
      const admin = await makeAdmin();
      const agent = await AgentModel.create({
        name: "Agent",
        teams: [],
        scope: "org",
      });

      // Create interaction
      await InteractionModel.create({
        profileId: agent.id,
        sessionId: "test-session",
        request: { model: "gpt-4", messages: [] },
        response: {
          id: "r1",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      // Filter for sessions in a date range that includes now
      const startDate = new Date(Date.now() - 24 * 60 * 60 * 1000);
      const endDate = new Date(Date.now() + 24 * 60 * 60 * 1000);

      const sessions = await InteractionModel.getSessions(
        { limit: 100, offset: 0 },
        admin.id,
        true,
        { startDate, endDate },
      );

      expect(sessions.data.length).toBeGreaterThanOrEqual(1);
    });
  });

  describe("getSessions auth attribution", () => {
    test("aggregates auth methods and authenticated app names", async ({
      makeAdmin,
    }) => {
      const admin = await makeAdmin();
      const agent = await AgentModel.create({
        name: "Agent",
        teams: [],
        scope: "org",
      });

      await InteractionModel.create({
        profileId: agent.id,
        sessionId: "auth-attribution-session",
        authMethod: "oauth_client_credentials",
        authenticatedAppId: "app-1",
        authenticatedAppName: "Backend Service",
        request: {
          model: "gpt-4",
          messages: [{ role: "user", content: "First request" }],
        },
        response: {
          id: "r1",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });
      await InteractionModel.create({
        profileId: agent.id,
        sessionId: "auth-attribution-session",
        authMethod: "oauth_user",
        authenticatedAppId: "app-2",
        authenticatedAppName: "User OAuth App, Inc.",
        request: {
          model: "gpt-4",
          messages: [{ role: "user", content: "Second request" }],
        },
        response: {
          id: "r2",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      const sessions = await InteractionModel.getSessions(
        { limit: 100, offset: 0 },
        admin.id,
        true,
        { sessionId: "auth-attribution-session" },
      );

      expect(sessions.data).toHaveLength(1);
      expect(sessions.data[0].authMethods).toEqual(
        expect.arrayContaining(["oauth_client_credentials", "oauth_user"]),
      );
      expect(sessions.data[0].authenticatedAppNames).toEqual(
        expect.arrayContaining(["Backend Service", "User OAuth App, Inc."]),
      );
    });
  });

  describe("getSessions source filtering", () => {
    test("filters sessions by source", async ({ makeAdmin }) => {
      const admin = await makeAdmin();
      const agent = await AgentModel.create({
        name: "Agent",
        teams: [],
        scope: "org",
      });

      // Create interactions with different sources
      await InteractionModel.create({
        profileId: agent.id,
        sessionId: "api-session",
        source: "api",
        request: { model: "gpt-4", messages: [] },
        response: {
          id: "r1",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      await InteractionModel.create({
        profileId: agent.id,
        sessionId: "chat-session",
        source: "chat",
        request: { model: "gpt-4", messages: [] },
        response: {
          id: "r2",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      await InteractionModel.create({
        profileId: null,
        source: "knowledge:embedding",
        request: {
          model: "text-embedding-3-small",
          input: ["test"],
          dimensions: 1536,
        },
        response: {
          object: "list",
          data: [{ object: "embedding", embedding: [], index: 0 }],
          model: "text-embedding-3-small",
          usage: { prompt_tokens: 10, total_tokens: 10 },
        },
        type: "openai:embeddings",
        model: "text-embedding-3-small",
        inputTokens: 10,
        outputTokens: 0,
      });

      await InteractionModel.create({
        profileId: null,
        source: "knowledge:reranker",
        request: {
          model: "gpt-4o",
          messages: [{ role: "user", content: "Rerank these passages" }],
        },
        response: {
          id: "r3",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4o",
          choices: [],
        },
        type: "openai:chatCompletions",
        model: "gpt-4o",
        inputTokens: 50,
        outputTokens: 20,
      });

      // Filter by "api" source
      const apiSessions = await InteractionModel.getSessions(
        { limit: 100, offset: 0 },
        admin.id,
        true,
        { source: "api" },
      );
      expect(apiSessions.data).toHaveLength(1);
      expect(apiSessions.data[0].source).toBe("api");

      // Filter by "knowledge:embedding" source
      const embeddingSessions = await InteractionModel.getSessions(
        { limit: 100, offset: 0 },
        admin.id,
        true,
        { source: "knowledge:embedding" },
      );
      expect(embeddingSessions.data).toHaveLength(1);
      expect(embeddingSessions.data[0].source).toBe("knowledge:embedding");

      // Filter by "knowledge:reranker" source
      const rerankerSessions = await InteractionModel.getSessions(
        { limit: 100, offset: 0 },
        admin.id,
        true,
        { source: "knowledge:reranker" },
      );
      expect(rerankerSessions.data).toHaveLength(1);
      expect(rerankerSessions.data[0].source).toBe("knowledge:reranker");

      // No filter returns all
      const allSessions = await InteractionModel.getSessions(
        { limit: 100, offset: 0 },
        admin.id,
        true,
      );
      expect(allSessions.data).toHaveLength(4);
    });

    test("filters sessions by client (external_agent_id)", async ({
      makeAdmin,
    }) => {
      const admin = await makeAdmin();
      const agent = await AgentModel.create({
        name: "Agent",
        teams: [],
        scope: "org",
      });

      const make = (sessionId: string, externalAgentId: string | null) =>
        InteractionModel.create({
          profileId: agent.id,
          sessionId,
          source: "api",
          externalAgentId,
          request: { model: "gpt-4", messages: [] },
          response: {
            id: sessionId,
            object: "chat.completion",
            created: Date.now(),
            model: "gpt-4",
            choices: [],
          },
          type: "openai:chatCompletions",
        });

      // Two Claude clients (auto-discovered generic id and header-set Code id),
      // a customer agent, and a plain session with no client.
      await make("auto-claude-session", CLAUDE_CLIENT_ID);
      await make("claude-code-session", CLAUDE_CODE_CLIENT_ID);
      await make("customer-session", "my-custom-agent");
      await make("plain-session", null);

      // Filter to Claude — expands to every Claude client id.
      const claude = await InteractionModel.getSessions(
        { limit: 100, offset: 0 },
        admin.id,
        true,
        { client: CLAUDE_CLIENT_FILTER },
      );
      expect(claude.data).toHaveLength(2);
      expect(claude.data.flatMap((s) => s.externalAgentIds).sort()).toEqual(
        [CLAUDE_CLIENT_ID, CLAUDE_CODE_CLIENT_ID].sort(),
      );

      // No filter returns all four
      const all = await InteractionModel.getSessions(
        { limit: 100, offset: 0 },
        admin.id,
        true,
      );
      expect(all.data).toHaveLength(4);
    });

    test("marks mixed-source chat sessions without promoting compaction to the session source", async ({
      makeAdmin,
    }) => {
      const admin = await makeAdmin();
      const agent = await AgentModel.create({
        name: "Agent",
        teams: [],
        scope: "org",
      });

      await InteractionModel.create({
        profileId: agent.id,
        sessionId: "mixed-chat-session",
        source: "chat",
        request: { model: "gpt-4", messages: [] },
        response: {
          id: "r1",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      await InteractionModel.create({
        profileId: agent.id,
        sessionId: "mixed-chat-session",
        source: "chat:compaction",
        request: { model: "gpt-4", messages: [] },
        response: {
          id: "r2",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      const sessions = await InteractionModel.getSessions(
        { limit: 100, offset: 0 },
        admin.id,
        true,
      );

      expect(sessions.data).toHaveLength(1);
      expect(sessions.data[0].source).toBeNull();
      expect(sessions.data[0].sources).toEqual(
        expect.arrayContaining(["chat", "chat:compaction"]),
      );
    });

    test("returns empty when filtering by source with no matches", async ({
      makeAdmin,
    }) => {
      const admin = await makeAdmin();
      const agent = await AgentModel.create({
        name: "Agent",
        teams: [],
        scope: "org",
      });

      await InteractionModel.create({
        profileId: agent.id,
        sessionId: "api-session",
        source: "api",
        request: { model: "gpt-4", messages: [] },
        response: {
          id: "r1",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      const sessions = await InteractionModel.getSessions(
        { limit: 100, offset: 0 },
        admin.id,
        true,
        { source: "knowledge:embedding" },
      );
      expect(sessions.data).toHaveLength(0);
    });

    test("can create embedding interaction with null profileId", async () => {
      const interaction = await InteractionModel.create({
        profileId: null,
        source: "knowledge:embedding",
        type: "openai:embeddings",
        request: {
          model: "text-embedding-3-small",
          input: ["hello world"],
          dimensions: 1536,
        },
        response: {
          object: "list",
          data: [{ object: "embedding", embedding: [], index: 0 }],
          model: "text-embedding-3-small",
          usage: { prompt_tokens: 5, total_tokens: 5 },
        },
        model: "text-embedding-3-small",
        inputTokens: 5,
        outputTokens: 0,
      });

      expect(interaction).toBeDefined();
      expect(interaction.id).toBeDefined();
      expect(interaction.profileId).toBeNull();
      expect(interaction.source).toBe("knowledge:embedding");
      expect(interaction.type).toBe("openai:embeddings");
    });
  });

  describe("getSessions lastInteraction and claudeCodeTitle", () => {
    test("returns lastInteractionRequest and lastInteractionType for session with single interaction", async ({
      makeAdmin,
    }) => {
      const admin = await makeAdmin();
      const agent = await AgentModel.create({
        name: "Agent",
        teams: [],
        scope: "org",
      });

      await InteractionModel.create({
        profileId: agent.id,
        sessionId: "single-interaction-session",
        request: {
          model: "gpt-4",
          messages: [
            {
              role: "user",
              content:
                "This is a meaningful message with more than 20 characters",
            },
          ],
        },
        response: {
          id: "r1",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [
            {
              index: 0,
              message: {
                role: "assistant",
                content: "Response",
                refusal: null,
              },
              finish_reason: "stop",
              logprobs: null,
            },
          ],
        },
        type: "openai:chatCompletions",
      });

      const sessions = await InteractionModel.getSessions(
        { limit: 100, offset: 0 },
        admin.id,
        true,
        { sessionId: "single-interaction-session" },
      );

      expect(sessions.data).toHaveLength(1);
      expect(sessions.data[0].lastInteractionRequest).not.toBeNull();
      expect(sessions.data[0].lastInteractionType).toBe(
        "openai:chatCompletions",
      );
    });

    test("skips prompt suggestion generator requests when finding lastInteractionRequest", async ({
      makeAdmin,
    }) => {
      const admin = await makeAdmin();
      const agent = await AgentModel.create({
        name: "Agent",
        teams: [],
        scope: "org",
      });

      // First: a real user request (should be the lastInteractionRequest)
      await InteractionModel.create({
        profileId: agent.id,
        sessionId: "session-with-prompt-suggestion",
        request: {
          model: "gpt-4",
          messages: [
            {
              role: "user",
              content:
                "This is a real user question that should be shown as last request",
            },
          ],
        },
        response: {
          id: "r1",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      // Second: a prompt suggestion request (should be skipped)
      await InteractionModel.create({
        profileId: agent.id,
        sessionId: "session-with-prompt-suggestion",
        request: {
          model: "gpt-4",
          messages: [
            {
              role: "user",
              content:
                "You are a prompt suggestion generator. Generate suggestions.",
            },
          ],
        },
        response: {
          id: "r2",
          object: "chat.completion",
          created: Date.now() + 1000,
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      const sessions = await InteractionModel.getSessions(
        { limit: 100, offset: 0 },
        admin.id,
        true,
        { sessionId: "session-with-prompt-suggestion" },
      );

      expect(sessions.data).toHaveLength(1);
      const lastRequest = sessions.data[0].lastInteractionRequest as {
        messages: Array<{ content: string }>;
      };
      expect(lastRequest.messages[0].content).toContain("real user question");
    });

    test("skips title generation requests when finding lastInteractionRequest", async ({
      makeAdmin,
    }) => {
      const admin = await makeAdmin();
      const agent = await AgentModel.create({
        name: "Agent",
        teams: [],
        scope: "org",
      });

      // First: a real user request
      await InteractionModel.create({
        profileId: agent.id,
        sessionId: "session-with-title-gen",
        request: {
          model: "gpt-4",
          messages: [
            {
              role: "user",
              content:
                "This is a real question about programming that should appear",
            },
          ],
        },
        response: {
          id: "r1",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      // Second: a title generation request (should be skipped for lastInteractionRequest)
      await InteractionModel.create({
        profileId: agent.id,
        sessionId: "session-with-title-gen",
        request: {
          model: "gpt-4",
          messages: [
            {
              role: "user",
              content: "Please write a 5-10 word title for this conversation.",
            },
          ],
        },
        response: {
          id: "r2",
          object: "chat.completion",
          created: Date.now() + 1000,
          model: "gpt-4",
          choices: [
            {
              index: 0,
              message: {
                role: "assistant",
                content: "My Generated Title Here",
                refusal: null,
              },
              finish_reason: "stop",
              logprobs: null,
            },
          ],
        },
        type: "openai:chatCompletions",
      });

      const sessions = await InteractionModel.getSessions(
        { limit: 100, offset: 0 },
        admin.id,
        true,
        { sessionId: "session-with-title-gen" },
      );

      expect(sessions.data).toHaveLength(1);
      const lastRequest = sessions.data[0].lastInteractionRequest as {
        messages: Array<{ content: string }>;
      };
      expect(lastRequest.messages[0].content).toContain("real question");
    });

    test("extracts claudeCodeTitle from title generation response", async ({
      makeAdmin,
    }) => {
      const admin = await makeAdmin();
      const agent = await AgentModel.create({
        name: "Agent",
        teams: [],
        scope: "org",
      });

      // Real request
      await InteractionModel.create({
        profileId: agent.id,
        sessionId: "session-with-claude-title",
        request: {
          model: "claude-3-5-sonnet",
          messages: [
            {
              role: "user",
              content: "Help me write a Python script for data analysis",
            },
          ],
        },
        response: {
          id: "r1",
          object: "chat.completion",
          created: Date.now(),
          model: "claude-3-5-sonnet",
          choices: [],
        },
        type: "anthropic:messages",
      });

      // Title generation request with response containing the title
      await InteractionModel.create({
        profileId: agent.id,
        sessionId: "session-with-claude-title",
        request: {
          model: "claude-3-5-sonnet",
          messages: [
            {
              role: "user",
              content:
                "Please write a 5-10 word title summarizing this conversation.",
            },
          ],
        },
        response: {
          id: "msg_title",
          content: [{ type: "text", text: "Python Data Analysis Script Help" }],
          model: "claude-3-5-sonnet",
          role: "assistant",
          stop_reason: "end_turn",
          stop_sequence: null,
          type: "message",
          usage: { input_tokens: 100, output_tokens: 10 },
        },
        type: "anthropic:messages",
      });

      const sessions = await InteractionModel.getSessions(
        { limit: 100, offset: 0 },
        admin.id,
        true,
        { sessionId: "session-with-claude-title" },
      );

      expect(sessions.data).toHaveLength(1);
      expect(sessions.data[0].claudeCodeTitle).toBe(
        "Python Data Analysis Script Help",
      );
    });

    test("handles title generation request with malformed response (no text)", async ({
      makeAdmin,
    }) => {
      const admin = await makeAdmin();
      const agent = await AgentModel.create({
        name: "Agent",
        teams: [],
        scope: "org",
      });

      // Real request (should be returned as lastInteractionRequest)
      await InteractionModel.create({
        profileId: agent.id,
        sessionId: "session-malformed-title",
        request: {
          model: "claude-3-5-sonnet",
          messages: [
            {
              role: "user",
              content: "Help me write a Python script for data analysis",
            },
          ],
        },
        response: {
          id: "r1",
          object: "chat.completion",
          created: Date.now(),
          model: "claude-3-5-sonnet",
          choices: [],
        },
        type: "anthropic:messages",
      });

      // Title generation request with malformed response (missing text)
      await InteractionModel.create({
        profileId: agent.id,
        sessionId: "session-malformed-title",
        request: {
          model: "claude-3-5-sonnet",
          messages: [
            {
              role: "user",
              content:
                "Please write a 5-10 word title summarizing this conversation.",
            },
          ],
        },
        response: {
          id: "msg_title",
          content: [], // Empty content array - no text
          model: "claude-3-5-sonnet",
          role: "assistant",
          stop_reason: "end_turn",
          stop_sequence: null,
          type: "message",
          usage: { input_tokens: 100, output_tokens: 0 },
        },
        type: "anthropic:messages",
      });

      const sessions = await InteractionModel.getSessions(
        { limit: 100, offset: 0 },
        admin.id,
        true,
        { sessionId: "session-malformed-title" },
      );

      expect(sessions.data).toHaveLength(1);
      // Should have the main interaction but null for title
      expect(sessions.data[0].lastInteractionRequest).not.toBeNull();
      expect(sessions.data[0].claudeCodeTitle).toBeNull();
    });

    test("returns lastInteractionRequest even for short messages", async ({
      makeAdmin,
    }) => {
      const admin = await makeAdmin();
      const agent = await AgentModel.create({
        name: "Agent",
        teams: [],
        scope: "org",
      });

      // Short message - should still be returned
      await InteractionModel.create({
        profileId: agent.id,
        sessionId: "short-session",
        request: {
          model: "gpt-4",
          messages: [{ role: "user", content: "hi" }],
        },
        response: {
          id: "r1",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      const sessions = await InteractionModel.getSessions(
        { limit: 100, offset: 0 },
        admin.id,
        true,
        { sessionId: "short-session" },
      );

      expect(sessions.data).toHaveLength(1);
      expect(sessions.data[0].lastInteractionRequest).not.toBeNull();
    });

    test("returns lastInteractionRequest for Gemini format (contents[].parts[].text)", async ({
      makeAdmin,
    }) => {
      const admin = await makeAdmin();
      const agent = await AgentModel.create({
        name: "Agent",
        teams: [],
        scope: "org",
      });

      // Gemini format uses contents[] with parts[] instead of messages[]
      await InteractionModel.create({
        profileId: agent.id,
        sessionId: "gemini-session",
        request: {
          contents: [
            {
              role: "user",
              parts: [{ text: "123" }],
            },
          ],
          systemInstruction: {
            parts: [{ text: "You are a helpful AI assistant." }],
          },
          generationConfig: {},
        },
        response: {
          candidates: [
            {
              content: { role: "model", parts: [{ text: "Hello!" }] },
              finishReason: "STOP",
              index: 0,
            },
          ],
          modelVersion: "gemini-2.5-pro",
        },
        type: "gemini:generateContent",
      });

      const sessions = await InteractionModel.getSessions(
        { limit: 100, offset: 0 },
        admin.id,
        true,
        { sessionId: "gemini-session" },
      );

      expect(sessions.data).toHaveLength(1);
      expect(sessions.data[0].lastInteractionRequest).not.toBeNull();
      expect(sessions.data[0].lastInteractionType).toBe(
        "gemini:generateContent",
      );
    });

    test("returns lastInteractionRequest for Gemini with image-only content (no text)", async ({
      makeAdmin,
    }) => {
      const admin = await makeAdmin();
      const agent = await AgentModel.create({
        name: "Agent",
        teams: [],
        scope: "org",
      });

      // Gemini request with only image data (no text parts)
      await InteractionModel.create({
        profileId: agent.id,
        sessionId: "gemini-image-session",
        request: {
          contents: [
            {
              role: "user",
              parts: [
                {
                  inlineData: {
                    mimeType: "image/png",
                    data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
                  },
                },
              ],
            },
          ],
        },
        response: {
          candidates: [
            {
              content: {
                role: "model",
                parts: [{ text: "I see an image" }],
              },
              finishReason: "STOP",
              index: 0,
            },
          ],
          modelVersion: "gemini-2.5-pro",
        },
        type: "gemini:generateContent",
      });

      const sessions = await InteractionModel.getSessions(
        { limit: 100, offset: 0 },
        admin.id,
        true,
        { sessionId: "gemini-image-session" },
      );

      expect(sessions.data).toHaveLength(1);
      expect(sessions.data[0].lastInteractionRequest).not.toBeNull();
      expect(sessions.data[0].lastInteractionType).toBe(
        "gemini:generateContent",
      );
    });

    test("returns lastInteractionRequest for Gemini with function response (tool result)", async ({
      makeAdmin,
    }) => {
      const admin = await makeAdmin();
      const agent = await AgentModel.create({
        name: "Agent",
        teams: [],
        scope: "org",
      });

      // Gemini request with function response (common in agentic workflows)
      await InteractionModel.create({
        profileId: agent.id,
        sessionId: "gemini-function-session",
        request: {
          contents: [
            {
              role: "user",
              parts: [{ text: "Search for weather" }],
            },
            {
              role: "model",
              parts: [
                {
                  functionCall: {
                    name: "get_weather",
                    args: { location: "New York" },
                  },
                },
              ],
            },
            {
              role: "user",
              parts: [
                {
                  functionResponse: {
                    name: "get_weather",
                    response: { temperature: 72, condition: "sunny" },
                  },
                },
              ],
            },
          ],
        },
        response: {
          candidates: [
            {
              content: {
                role: "model",
                parts: [{ text: "The weather in New York is sunny at 72°F" }],
              },
              finishReason: "STOP",
              index: 0,
            },
          ],
          modelVersion: "gemini-2.5-pro",
        },
        type: "gemini:generateContent",
      });

      const sessions = await InteractionModel.getSessions(
        { limit: 100, offset: 0 },
        admin.id,
        true,
        { sessionId: "gemini-function-session" },
      );

      expect(sessions.data).toHaveLength(1);
      expect(sessions.data[0].lastInteractionRequest).not.toBeNull();
      expect(sessions.data[0].lastInteractionType).toBe(
        "gemini:generateContent",
      );
    });

    test("handles single interactions without sessionId (null session)", async ({
      makeAdmin,
    }) => {
      const admin = await makeAdmin();
      const agent = await AgentModel.create({
        name: "Agent",
        teams: [],
        scope: "org",
      });

      const interaction = await InteractionModel.create({
        profileId: agent.id,
        // No sessionId - this is a single interaction
        request: {
          model: "gpt-4",
          messages: [
            {
              role: "user",
              content: "This is a standalone interaction without a session ID",
            },
          ],
        },
        response: {
          id: "r1",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      const sessions = await InteractionModel.getSessions(
        { limit: 100, offset: 0 },
        admin.id,
        true,
      );

      // Find our session (identified by interactionId since sessionId is null)
      const ourSession = sessions.data.find(
        (s) => s.interactionId === interaction.id,
      );
      expect(ourSession).toBeDefined();
      expect(ourSession?.sessionId).toBeNull();
      expect(ourSession?.interactionId).toBe(interaction.id);
      expect(ourSession?.lastInteractionRequest).not.toBeNull();
    });
  });

  describe("existsByExecutionId", () => {
    test("returns false when no interaction has the execution id", async () => {
      const exists = await InteractionModel.existsByExecutionId(
        "non-existent-exec-id",
      );
      expect(exists).toBe(false);
    });

    test("returns true when an interaction has the execution id", async () => {
      await InteractionModel.create({
        profileId,
        executionId: "test-exec-123",
        request: {
          model: "gpt-4",
          messages: [{ role: "user", content: "Hello" }],
        },
        response: {
          id: "r1",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      const exists =
        await InteractionModel.existsByExecutionId("test-exec-123");
      expect(exists).toBe(true);
    });

    test("returns true when multiple interactions share the execution id", async () => {
      await InteractionModel.create({
        profileId,
        executionId: "shared-exec-id",
        request: {
          model: "gpt-4",
          messages: [{ role: "user", content: "First" }],
        },
        response: {
          id: "r1",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      await InteractionModel.create({
        profileId,
        executionId: "shared-exec-id",
        request: {
          model: "gpt-4",
          messages: [{ role: "user", content: "Second" }],
        },
        response: {
          id: "r2",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      const exists =
        await InteractionModel.existsByExecutionId("shared-exec-id");
      expect(exists).toBe(true);
    });
  });

  describe("getUniqueUserIds", () => {
    test("returns unique user IDs with names", async ({
      makeAdmin,
      makeUser,
    }) => {
      const admin = await makeAdmin();
      const user1 = await makeUser({ name: "User One" });
      const user2 = await makeUser({ name: "User Two" });

      const agent = await AgentModel.create({
        name: "Agent",
        teams: [],
        scope: "org",
      });

      // Create interactions for both users
      await InteractionModel.create({
        profileId: agent.id,
        userId: user1.id,
        request: { model: "gpt-4", messages: [] },
        response: {
          id: "r1",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      await InteractionModel.create({
        profileId: agent.id,
        userId: user2.id,
        request: { model: "gpt-4", messages: [] },
        response: {
          id: "r2",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      // Create another interaction for user1 (should not duplicate in result)
      await InteractionModel.create({
        profileId: agent.id,
        userId: user1.id,
        request: { model: "gpt-4", messages: [] },
        response: {
          id: "r3",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      const userIds = await InteractionModel.getUniqueUserIds(admin.id, true);

      expect(userIds).toHaveLength(2);
      // Results should be sorted by name
      expect(userIds.map((u) => u.name)).toContain("User One");
      expect(userIds.map((u) => u.name)).toContain("User Two");
      expect(userIds.every((u) => u.id && u.name)).toBe(true);
    });

    test("excludes interactions without userId", async ({
      makeAdmin,
      makeUser,
    }) => {
      const admin = await makeAdmin();
      const user = await makeUser({ name: "Test User" });

      const agent = await AgentModel.create({
        name: "Agent",
        teams: [],
        scope: "org",
      });

      // Interaction with userId
      await InteractionModel.create({
        profileId: agent.id,
        userId: user.id,
        request: { model: "gpt-4", messages: [] },
        response: {
          id: "r1",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      // Interaction without userId
      await InteractionModel.create({
        profileId: agent.id,
        request: { model: "gpt-4", messages: [] },
        response: {
          id: "r2",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      const userIds = await InteractionModel.getUniqueUserIds(admin.id, true);

      expect(userIds).toHaveLength(1);
      expect(userIds[0].name).toBe("Test User");
    });

    test("respects access control for non-admin users", async ({
      makeUser,
      makeAdmin,
      makeOrganization,
      makeTeam,
    }) => {
      const user = await makeUser({ name: "Regular User" });
      const otherUser = await makeUser({ name: "Other User" });
      const admin = await makeAdmin();
      const org = await makeOrganization();

      const team = await makeTeam(org.id, admin.id);
      await TeamModel.addMember(team.id, user.id);

      const accessibleAgent = await AgentModel.create({
        name: "Accessible Agent",
        teams: [team.id],
        scope: "team",
      });
      const orgWideAgent = await AgentModel.create({
        name: "Org-Wide Agent",
        teams: [],
        scope: "org",
      });

      // Interaction for team-scoped agent with otherUser
      await InteractionModel.create({
        profileId: accessibleAgent.id,
        userId: otherUser.id,
        request: { model: "gpt-4", messages: [] },
        response: {
          id: "r1",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      // Interaction for org-wide agent with admin
      await InteractionModel.create({
        profileId: orgWideAgent.id,
        userId: admin.id,
        request: { model: "gpt-4", messages: [] },
        response: {
          id: "r2",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      // User sees userIds from both team-scoped and org-wide agent interactions
      const userIds = await InteractionModel.getUniqueUserIds(user.id, false);

      expect(userIds).toHaveLength(2);
    });
  });

  describe("preserves interactions when profile is deleted", () => {
    test("interaction is preserved with null profileId when profile is deleted", async ({
      makeAdmin,
    }) => {
      const admin = await makeAdmin();
      const agent = await AgentModel.create({
        name: "Agent To Delete",
        teams: [],
        scope: "org",
      });

      // Create an interaction for the agent
      const interaction = await InteractionModel.create({
        profileId: agent.id,
        request: {
          model: "gpt-4",
          messages: [{ role: "user", content: "Hello" }],
        },
        response: {
          id: "r1",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [
            {
              index: 0,
              message: {
                role: "assistant",
                content: "Hi there",
                refusal: null,
              },
              finish_reason: "stop",
              logprobs: null,
            },
          ],
        },
        type: "openai:chatCompletions",
      });

      // Delete the agent
      await AgentModel.delete(agent.id);

      // Admin should still be able to see the interaction with null profileId
      const found = await InteractionModel.findById(
        interaction.id,
        admin.id,
        true,
      );

      expect(found).toBeDefined();
      expect(found?.id).toBe(interaction.id);
      expect(found?.profileId).toBeNull();
    });

    test("non-admin cannot see interaction with deleted profile (null profileId)", async ({
      makeUser,
      makeAdmin,
      makeOrganization,
      makeTeam,
    }) => {
      const user = await makeUser();
      const admin = await makeAdmin();
      const org = await makeOrganization();

      // Give user access to the team
      const team = await makeTeam(org.id, admin.id);
      await TeamModel.addMember(team.id, user.id);

      const agent = await AgentModel.create({
        name: "Agent To Delete",
        teams: [team.id],
        scope: "team",
      });

      // Create an interaction for the agent
      const interaction = await InteractionModel.create({
        profileId: agent.id,
        request: {
          model: "gpt-4",
          messages: [{ role: "user", content: "Hello" }],
        },
        response: {
          id: "r1",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      // User can see interaction before agent deletion
      const beforeDelete = await InteractionModel.findById(
        interaction.id,
        user.id,
        false,
      );
      expect(beforeDelete).not.toBeNull();

      // Delete the agent
      await AgentModel.delete(agent.id);

      // Non-admin should NOT see the interaction with null profileId
      const afterDelete = await InteractionModel.findById(
        interaction.id,
        user.id,
        false,
      );
      expect(afterDelete).toBeNull();

      // But admin should still see it
      const adminView = await InteractionModel.findById(
        interaction.id,
        admin.id,
        true,
      );
      expect(adminView).not.toBeNull();
      expect(adminView?.profileId).toBeNull();
    });

    test("getSessions includes sessions with deleted profiles for admin", async ({
      makeAdmin,
    }) => {
      const admin = await makeAdmin();
      const agent = await AgentModel.create({
        name: "Agent To Delete",
        teams: [],
        scope: "org",
      });

      // Create an interaction with session
      await InteractionModel.create({
        profileId: agent.id,
        sessionId: "session-to-preserve",
        request: {
          model: "gpt-4",
          messages: [{ role: "user", content: "Session message" }],
        },
        response: {
          id: "r1",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      // Delete the agent
      await AgentModel.delete(agent.id);

      // Admin should see the session with null profileId
      const sessions = await InteractionModel.getSessions(
        { limit: 100, offset: 0 },
        admin.id,
        true,
        { sessionId: "session-to-preserve" },
      );

      expect(sessions.data).toHaveLength(1);
      expect(sessions.data[0].sessionId).toBe("session-to-preserve");
      expect(sessions.data[0].profileId).toBeNull();
    });

    test("findAllPaginated includes interactions with deleted profiles for admin", async ({
      makeAdmin,
    }) => {
      const admin = await makeAdmin();
      const agentToDelete = await AgentModel.create({
        name: "Agent To Delete",
        teams: [],
        scope: "org",
      });
      const agentToKeep = await AgentModel.create({
        name: "Agent To Keep",
        teams: [],
        scope: "org",
      });

      // Create interactions for both agents
      await InteractionModel.create({
        profileId: agentToDelete.id,
        request: { model: "gpt-4", messages: [] },
        response: {
          id: "r1",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      await InteractionModel.create({
        profileId: agentToKeep.id,
        request: { model: "gpt-4", messages: [] },
        response: {
          id: "r2",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      // Delete one agent
      await AgentModel.delete(agentToDelete.id);

      // Admin should see both interactions
      const interactions = await InteractionModel.findAllPaginated(
        { limit: 100, offset: 0 },
        undefined,
        admin.id,
        true,
      );

      expect(interactions.data).toHaveLength(2);

      const deletedProfileInteraction = interactions.data.find(
        (i) => i.profileId === null,
      );
      const existingProfileInteraction = interactions.data.find(
        (i) => i.profileId === agentToKeep.id,
      );

      expect(deletedProfileInteraction).toBeDefined();
      expect(existingProfileInteraction).toBeDefined();
    });

    test("findAllPaginated resolves external agent labels after profile deletion", async ({
      makeAdmin,
    }) => {
      const admin = await makeAdmin();
      const agent = await AgentModel.create({
        name: "Deleted External Agent",
        teams: [],
        scope: "org",
      });

      await InteractionModel.create({
        profileId: agent.id,
        externalAgentId: agent.id,
        request: { model: "gpt-4", messages: [] },
        response: {
          id: "r1",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      await AgentModel.delete(agent.id);

      const interactions = await InteractionModel.findAllPaginated(
        { limit: 100, offset: 0 },
        undefined,
        admin.id,
        true,
        { externalAgentId: agent.id },
      );

      expect(interactions.data).toHaveLength(1);
      const [interaction] =
        interactions.data as ((typeof interactions.data)[number] & {
          externalAgentIdLabel: string | null;
        })[];
      expect(interaction.profileId).toBeNull();
      expect(interaction.externalAgentIdLabel).toBe(agent.name);
    });
  });

  describe("updateUsageAfterInteraction", () => {
    test("updates user limit usage when interaction has userId", async ({
      makeAgent,
      makeUser,
    }) => {
      const agent = await makeAgent();
      const user = await makeUser();

      const userLimit = await LimitModel.create({
        entityType: "user",
        entityId: user.id,
        limitType: "token_cost",
        limitValue: 1000000,
        model: ["gpt-4o"],
      });

      await InteractionModel.create({
        profileId: agent.id,
        userId: user.id,
        model: "gpt-4o",
        inputTokens: 100,
        outputTokens: 200,
        request: { model: "gpt-4o", messages: [] },
        response: {
          id: "r1",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4o",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      // Tick to the task queue effectively draining the microtask queue
      // TODO: if calls to InteractionModel.updateUsageAfterInteraction change might want to change the test as well
      await new Promise((resolve) => setTimeout(resolve, 100));

      const usage = await LimitModel.getModelUsageBreakdown(userLimit.id);
      expect(usage).toHaveLength(1);
      expect(usage[0].model).toBe("gpt-4o");
      expect(usage[0].tokensIn).toBe(100);
      expect(usage[0].tokensOut).toBe(200);
    });

    test("updates virtual_key limit usage when interaction has virtualKeyId", async ({
      makeAgent,
      makeOrganization,
      makeVirtualApiKey,
    }) => {
      const agent = await makeAgent();
      const org = await makeOrganization();
      const virtualKey = await makeVirtualApiKey(org.id);

      const vkLimit = await LimitModel.create({
        entityType: "virtual_key",
        entityId: virtualKey.id,
        limitType: "token_cost",
        limitValue: 5000000,
        model: ["gpt-4o"],
      });

      await InteractionModel.create({
        profileId: agent.id,
        virtualKeyId: virtualKey.id,
        model: "gpt-4o",
        inputTokens: 50,
        outputTokens: 100,
        request: { model: "gpt-4o", messages: [] },
        response: {
          id: "r1",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4o",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      // Tick to the task queue effectively draining the microtask queue
      // TODO: if calls to InteractionModel.updateUsageAfterInteraction change might want to change the test as well
      await new Promise((resolve) => setTimeout(resolve, 100));

      const usage = await LimitModel.getModelUsageBreakdown(vkLimit.id);
      expect(usage).toHaveLength(1);
      expect(usage[0].model).toBe("gpt-4o");
      expect(usage[0].tokensIn).toBe(50);
      expect(usage[0].tokensOut).toBe(100);
    });

    test("updates both user AND virtual_key limits when both present", async ({
      makeAgent,
      makeUser,
      makeOrganization,
      makeVirtualApiKey,
    }) => {
      const agent = await makeAgent();
      const user = await makeUser();
      const org = await makeOrganization();
      const virtualKey = await makeVirtualApiKey(org.id);

      const userLimit = await LimitModel.create({
        entityType: "user",
        entityId: user.id,
        limitType: "token_cost",
        limitValue: 1000000,
        model: ["gpt-4o"],
      });

      const vkLimit = await LimitModel.create({
        entityType: "virtual_key",
        entityId: virtualKey.id,
        limitType: "token_cost",
        limitValue: 5000000,
        model: ["gpt-4o"],
      });

      await InteractionModel.create({
        profileId: agent.id,
        userId: user.id,
        virtualKeyId: virtualKey.id,
        model: "gpt-4o",
        inputTokens: 75,
        outputTokens: 150,
        request: { model: "gpt-4o", messages: [] },
        response: {
          id: "r1",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4o",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      // Tick to the task queue effectively draining the microtask queue
      // TODO: if calls to InteractionModel.updateUsageAfterInteraction change might want to change the test as well
      await new Promise((resolve) => setTimeout(resolve, 100));

      const userUsage = await LimitModel.getModelUsageBreakdown(userLimit.id);
      expect(userUsage).toHaveLength(1);
      expect(userUsage[0].tokensIn).toBe(75);
      expect(userUsage[0].tokensOut).toBe(150);

      const vkUsage = await LimitModel.getModelUsageBreakdown(vkLimit.id);
      expect(vkUsage).toHaveLength(1);
      expect(vkUsage[0].tokensIn).toBe(75);
      expect(vkUsage[0].tokensOut).toBe(150);
    });

    test("still updates agent/team/org limits as before (regression)", async ({
      makeAgent,
      makeOrganization,
      makeAdmin,
      makeTeam,
    }) => {
      const org = await makeOrganization();
      const admin = await makeAdmin();
      const team = await makeTeam(org.id, admin.id);
      const agent = await makeAgent({ teams: [team.id], scope: "team" });

      const agentLimit = await LimitModel.create({
        entityType: "agent",
        entityId: agent.id,
        limitType: "token_cost",
        limitValue: 1000000,
        model: ["gpt-4o"],
      });

      const teamLimit = await LimitModel.create({
        entityType: "team",
        entityId: team.id,
        limitType: "token_cost",
        limitValue: 5000000,
        model: ["gpt-4o"],
      });

      const orgLimit = await LimitModel.create({
        entityType: "organization",
        entityId: org.id,
        limitType: "token_cost",
        limitValue: 10000000,
        model: ["gpt-4o"],
      });

      await InteractionModel.create({
        profileId: agent.id,
        model: "gpt-4o",
        inputTokens: 80,
        outputTokens: 160,
        request: { model: "gpt-4o", messages: [] },
        response: {
          id: "r1",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4o",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      // Tick to the task queue effectively draining the microtask queue
      // TODO: if calls to InteractionModel.updateUsageAfterInteraction change might want to change the test as well
      await new Promise((resolve) => setTimeout(resolve, 100));

      const agentUsage = await LimitModel.getModelUsageBreakdown(agentLimit.id);
      expect(agentUsage).toHaveLength(1);
      expect(agentUsage[0].tokensIn).toBe(80);
      expect(agentUsage[0].tokensOut).toBe(160);

      const teamUsage = await LimitModel.getModelUsageBreakdown(teamLimit.id);
      expect(teamUsage).toHaveLength(1);
      expect(teamUsage[0].tokensIn).toBe(80);

      const orgUsage = await LimitModel.getModelUsageBreakdown(orgLimit.id);
      expect(orgUsage).toHaveLength(1);
      expect(orgUsage[0].tokensIn).toBe(80);
    });

    test("updates team all-models limit via interaction flow", async ({
      makeAgent,
      makeOrganization,
      makeAdmin,
      makeTeam,
    }) => {
      const org = await makeOrganization();
      const admin = await makeAdmin();
      const team = await makeTeam(org.id, admin.id);
      const agent = await makeAgent({ teams: [team.id], scope: "team" });

      // Also assign via agent_team table (like production)
      await AgentTeamModel.assignTeamsToAgent(agent.id, [team.id]);

      const teamLimit = await LimitModel.create({
        entityType: "team",
        entityId: team.id,
        limitType: "token_cost",
        limitValue: 5000000,
        model: null, // all-models
      });

      await InteractionModel.create({
        profileId: agent.id,
        model: "gpt-4o",
        inputTokens: 80,
        outputTokens: 160,
        request: { model: "gpt-4o", messages: [] },
        response: {
          id: "r1",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4o",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      // Tick to let background update run
      await new Promise((resolve) => setTimeout(resolve, 100));

      const teamUsage = await LimitModel.getModelUsageBreakdown(teamLimit.id);
      expect(teamUsage).toHaveLength(1);
      expect(teamUsage[0].tokensIn).toBe(80);
      expect(teamUsage[0].tokensOut).toBe(160);
    });

    test("updates team all-models limit with multiple teams", async ({
      makeAgent,
      makeOrganization,
      makeAdmin,
      makeTeam,
    }) => {
      const org = await makeOrganization();
      const admin = await makeAdmin();
      const team1 = await makeTeam(org.id, admin.id);
      const team2 = await makeTeam(org.id, admin.id);
      const agent = await makeAgent({
        teams: [team1.id, team2.id],
        scope: "team",
      });

      // Assign to both teams
      await AgentTeamModel.assignTeamsToAgent(agent.id, [team1.id, team2.id]);

      // Create all-models limit for ONLY team1
      const team1Limit = await LimitModel.create({
        entityType: "team",
        entityId: team1.id,
        limitType: "token_cost",
        limitValue: 5000000,
        model: null, // all-models
      });

      // Create specific model limit for team2
      const team2Limit = await LimitModel.create({
        entityType: "team",
        entityId: team2.id,
        limitType: "token_cost",
        limitValue: 5000000,
        model: ["gpt-4o"],
      });

      await InteractionModel.create({
        profileId: agent.id,
        model: "gpt-4o",
        inputTokens: 100,
        outputTokens: 200,
        request: { model: "gpt-4o", messages: [] },
        response: {
          id: "r1",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4o",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      // Tick to let background update run
      await new Promise((resolve) => setTimeout(resolve, 100));

      // team1 all-models limit should be updated
      const team1Usage = await LimitModel.getModelUsageBreakdown(team1Limit.id);
      expect(team1Usage).toHaveLength(1);
      expect(team1Usage[0].tokensIn).toBe(100);
      expect(team1Usage[0].tokensOut).toBe(200);

      // team2 specific-model limit should also be updated
      const team2Usage = await LimitModel.getModelUsageBreakdown(team2Limit.id);
      expect(team2Usage).toHaveLength(1);
      expect(team2Usage[0].tokensIn).toBe(100);
      expect(team2Usage[0].tokensOut).toBe(200);
    });

    test("handles missing userId and virtualKeyId gracefully", async ({
      makeAgent,
    }) => {
      const agent = await makeAgent();

      await expect(
        InteractionModel.create({
          profileId: agent.id,
          model: "gpt-4o",
          inputTokens: 50,
          outputTokens: 100,
          request: { model: "gpt-4o", messages: [] },
          response: {
            id: "r1",
            object: "chat.completion",
            created: Date.now(),
            model: "gpt-4o",
            choices: [],
          },
          type: "openai:chatCompletions",
        }),
      ).resolves.toBeDefined();
    });

    test("skips update when tokens are zero", async ({
      makeAgent,
      makeUser,
    }) => {
      const agent = await makeAgent();
      const user = await makeUser();

      const userLimit = await LimitModel.create({
        entityType: "user",
        entityId: user.id,
        limitType: "token_cost",
        limitValue: 1000000,
        model: ["gpt-4o"],
      });

      await InteractionModel.create({
        profileId: agent.id,
        userId: user.id,
        model: "gpt-4o",
        inputTokens: 0,
        outputTokens: 0,
        request: { model: "gpt-4o", messages: [] },
        response: {
          id: "r1",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4o",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      // Tick to the task queue effectively draining the microtask queue
      // TODO: if calls to InteractionModel.updateUsageAfterInteraction change might want to change the test as well
      await new Promise((resolve) => setTimeout(resolve, 100));

      const usage = await LimitModel.getModelUsageBreakdown(userLimit.id);
      expect(usage).toHaveLength(1);
      expect(usage[0].tokensIn).toBe(0);
      expect(usage[0].tokensOut).toBe(0);
    });

    test("updates user all-models limit via interaction flow", async ({
      makeAgent,
      makeUser,
    }) => {
      const agent = await makeAgent();
      const user = await makeUser();

      const userLimit = await LimitModel.create({
        entityType: "user",
        entityId: user.id,
        limitType: "token_cost",
        limitValue: 1_000_000,
        model: null, // all-models
      });

      await InteractionModel.create({
        profileId: agent.id,
        userId: user.id,
        model: "gpt-4o",
        inputTokens: 60,
        outputTokens: 120,
        request: { model: "gpt-4o", messages: [] },
        response: {
          id: "r1",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4o",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      // Tick to let background update run
      // TODO: if calls to InteractionModel.updateUsageAfterInteraction change might want to change the test as well
      await new Promise((resolve) => setTimeout(resolve, 100));

      const usage = await LimitModel.getModelUsageBreakdown(userLimit.id);
      expect(usage).toHaveLength(1);
      expect(usage[0].model).toBe("gpt-4o");
      expect(usage[0].tokensIn).toBe(60);
      expect(usage[0].tokensOut).toBe(120);
    });

    test("updates virtual_key all-models limit via interaction flow", async ({
      makeAgent,
      makeOrganization,
      makeVirtualApiKey,
    }) => {
      const agent = await makeAgent();
      const org = await makeOrganization();
      const virtualKey = await makeVirtualApiKey(org.id);

      const vkLimit = await LimitModel.create({
        entityType: "virtual_key",
        entityId: virtualKey.id,
        limitType: "token_cost",
        limitValue: 5_000_000,
        model: null, // all-models
      });

      await InteractionModel.create({
        profileId: agent.id,
        virtualKeyId: virtualKey.id,
        model: "gpt-4o",
        inputTokens: 40,
        outputTokens: 80,
        request: { model: "gpt-4o", messages: [] },
        response: {
          id: "r1",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4o",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      // Tick to let background update run
      // TODO: if calls to InteractionModel.updateUsageAfterInteraction change might want to change the test as well
      await new Promise((resolve) => setTimeout(resolve, 100));

      const usage = await LimitModel.getModelUsageBreakdown(vkLimit.id);
      expect(usage).toHaveLength(1);
      expect(usage[0].model).toBe("gpt-4o");
      expect(usage[0].tokensIn).toBe(40);
      expect(usage[0].tokensOut).toBe(80);
    });

    test("updates org all-models limit via interaction flow", async ({
      makeAgent,
      makeOrganization,
      makeAdmin,
      makeTeam,
      makeMember,
    }) => {
      const org = await makeOrganization();
      const admin = await makeAdmin();
      await makeMember(admin.id, org.id, { role: "admin" });
      const team = await makeTeam(org.id, admin.id);
      const agent = await makeAgent({
        teams: [team.id],
        scope: "team",
      });

      // Assign agent to team via junction table (like production)
      await AgentTeamModel.assignTeamsToAgent(agent.id, [team.id]);

      const orgLimit = await LimitModel.create({
        entityType: "organization",
        entityId: org.id,
        limitType: "token_cost",
        limitValue: 10_000_000,
        model: null, // all-models
      });

      await InteractionModel.create({
        profileId: agent.id,
        model: "gpt-4o",
        inputTokens: 90,
        outputTokens: 180,
        request: { model: "gpt-4o", messages: [] },
        response: {
          id: "r1",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4o",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      // Tick to let background update run
      // TODO: if calls to InteractionModel.updateUsageAfterInteraction change might want to change the test as well
      await new Promise((resolve) => setTimeout(resolve, 100));

      const usage = await LimitModel.getModelUsageBreakdown(orgLimit.id);
      expect(usage).toHaveLength(1);
      expect(usage[0].model).toBe("gpt-4o");
      expect(usage[0].tokensIn).toBe(90);
      expect(usage[0].tokensOut).toBe(180);
    });
  });

  describe("delta-encoded Claude interactions (read paths)", () => {
    const ANTHROPIC_RESPONSE = {
      id: "msg_test",
      type: "message",
      role: "assistant",
      model: "claude-3-5-sonnet",
      content: [{ type: "text", text: "ok" }],
      stop_reason: "end_turn",
      usage: { input_tokens: 1, output_tokens: 1 },
    };

    function createClaude(
      agentId: string,
      messages: unknown[],
      opts: { sessionId: string; tools?: unknown[]; createdAt?: Date },
    ) {
      return InteractionModel.create({
        profileId: agentId,
        sessionId: opts.sessionId,
        sessionSource: "claude_code",
        type: "anthropic:messages",
        request: {
          model: "claude-3-5-sonnet",
          max_tokens: 1024,
          messages,
          ...(opts.tools ? { tools: opts.tools } : {}),
        } as unknown as InsertInteraction["request"],
        response:
          ANTHROPIC_RESPONSE as unknown as InsertInteraction["response"],
        ...(opts.createdAt ? { createdAt: opts.createdAt } : {}),
      });
    }

    function messagesOf(request: unknown): unknown[] {
      return (request as { messages: unknown[] }).messages;
    }

    beforeEach(() => {
      InteractionDeltaManager.reset();
    });

    test("findAllPaginated returns full requests and classifies requestType on the reconstructed request", async ({
      makeAdmin,
    }) => {
      const admin = await makeAdmin();
      const agent = await AgentModel.create({
        name: "Agent",
        teams: [],
        scope: "org",
      });
      const tools = [
        { name: "Task", description: "spawn subagent", input_schema: {} },
      ];

      const m0 = {
        role: "user",
        content: "kick off a long running task please",
      };
      await createClaude(agent.id, [m0], {
        sessionId: "delta-find-all",
        tools,
      });
      // Continuation whose stored delta is a single short utility-looking message
      // ("count"). On the DELTA alone computeRequestType would say "subagent";
      // on the reconstructed full request (3 messages + Task tool) it is "main".
      const full = [
        m0,
        { role: "assistant", content: "ok" },
        { role: "user", content: "count" },
      ];
      const tip = await createClaude(agent.id, full, {
        sessionId: "delta-find-all",
        tools,
      });

      const result = await InteractionModel.findAllPaginated(
        { limit: 100, offset: 0 },
        undefined,
        admin.id,
        true,
        { sessionId: "delta-find-all" },
      );

      const tipRow = result.data.find((i) => i.id === tip.id);
      expect(messagesOf(tipRow?.request)).toEqual(full);
      expect(
        (tipRow as { requestType?: string } | undefined)?.requestType,
      ).toBe("main");
    });

    test("findAllPaginated classifies a Claude Desktop web-search request as a subagent", async ({
      makeAdmin,
    }) => {
      const admin = await makeAdmin();
      const agent = await AgentModel.create({
        name: "Agent",
        teams: [],
        scope: "org",
      });
      const sessionId = "desktop-subagent";

      const desktopRequest = (overrides: {
        system: unknown;
        tools: unknown[];
        messages: unknown[];
      }) =>
        ({
          model: "claude-3-5-sonnet",
          max_tokens: 1024,
          ...overrides,
        }) as unknown as InsertInteraction["request"];

      // Main Claude Desktop request: multi-turn, and — unlike Claude Code — its
      // main agent carries NO Task tool. It must still classify as "main".
      const main = await InteractionModel.create({
        profileId: agent.id,
        sessionId,
        sessionSource: "claude_desktop",
        type: "anthropic:messages",
        request: desktopRequest({
          system: [
            {
              type: "text",
              text: "You are a Claude agent, built on Anthropic's Claude Agent SDK.",
            },
          ],
          tools: [{ name: "Read", description: "read", input_schema: {} }],
          messages: [
            { role: "user", content: "help me research heaters" },
            { role: "assistant", content: "sure" },
            { role: "user", content: "find local prices" },
          ],
        }),
        response:
          ANTHROPIC_RESPONSE as unknown as InsertInteraction["response"],
      });

      // Web-search sub-agent: a fresh thread (new messages[0]), the Agent SDK
      // sub-agent system prompt, a single directive, and no Task tool.
      const sub = await InteractionModel.create({
        profileId: agent.id,
        sessionId,
        sessionSource: "claude_desktop",
        type: "anthropic:messages",
        request: desktopRequest({
          system: [
            {
              type: "text",
              text: "You are a Claude agent, built on Anthropic's Claude Agent SDK.",
            },
            {
              type: "text",
              text: "You are an assistant for performing a web search tool use",
            },
          ],
          tools: [
            { name: "web_search", description: "search", input_schema: {} },
          ],
          messages: [
            {
              role: "user",
              content: [
                {
                  type: "text",
                  text: "Perform a web search for the query: waste oil heater prices",
                },
              ],
            },
          ],
        }),
        response:
          ANTHROPIC_RESPONSE as unknown as InsertInteraction["response"],
      });

      const result = await InteractionModel.findAllPaginated(
        { limit: 100, offset: 0 },
        undefined,
        admin.id,
        true,
        { sessionId },
      );

      const requestTypeOf = (id: string) =>
        (
          result.data.find((i) => i.id === id) as
            | { requestType?: string }
            | undefined
        )?.requestType;

      expect(requestTypeOf(sub.id)).toBe("subagent");
      expect(requestTypeOf(main.id)).toBe("main");
    });

    test("getSessions reconstructs the last interaction request, even when the parent is outside the 20-row window", async ({
      makeAdmin,
    }) => {
      const admin = await makeAdmin();
      const agent = await AgentModel.create({
        name: "Agent",
        teams: [],
        scope: "org",
      });

      // 22 consecutive requests so the head/earliest rows fall outside the
      // 20-row selection window used by getLastInteractionsForSessions.
      // Use strictly increasing createdAt so the tip is unambiguously the most
      // recent row (defaultNow() can tie within the same millisecond under load,
      // which would make the "last main interaction" selection flaky).
      const baseTime = new Date("2020-01-01T00:00:00.000Z").getTime();
      const messages: unknown[] = [
        { role: "user", content: "turn 0 — kick things off with enough text" },
      ];
      let tipId = "";
      for (let i = 0; i < 22; i++) {
        if (i > 0) {
          messages.push({ role: "assistant", content: `reply ${i}` });
          messages.push({ role: "user", content: `turn ${i}` });
        }
        const row = await createClaude(agent.id, [...messages], {
          sessionId: "delta-sessions",
          createdAt: new Date(baseTime + i * 1000),
        });
        tipId = row.id;
      }
      const expectedLength = messages.length; // 1 + 21*2 = 43

      const sessions = await InteractionModel.getSessions(
        { limit: 100, offset: 0 },
        admin.id,
        true,
        { sessionId: "delta-sessions" },
      );

      expect(sessions.data).toHaveLength(1);
      expect(messagesOf(sessions.data[0].lastInteractionRequest)).toHaveLength(
        expectedLength,
      );
      // The reconstructed tip is the most recent interaction.
      const tip = await InteractionModel.findById(tipId);
      expect(messagesOf(tip?.request)).toHaveLength(expectedLength);
    });
  });
});
