import { ChatErrorCode } from "@shared";
import { beforeEach, describe, expect, test } from "@/test";
import { SelectInteractionSchema } from "@/types";
import AgentModel from "./agent";
import AgentTeamModel from "./agent-team";
import ConversationModel from "./conversation";
import ConversationChatErrorModel from "./conversation-chat-error";
import InteractionModel from "./interaction";
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("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 search filtering", () => {
    test("searches by request message content (case insensitive)", async ({
      makeAdmin,
    }) => {
      const admin = await makeAdmin();
      const agent = await AgentModel.create({
        name: "Agent",
        teams: [],
        scope: "org",
      });

      await InteractionModel.create({
        profileId: agent.id,
        sessionId: "session-1",
        request: {
          model: "gpt-4",
          messages: [
            { role: "user", content: "Tell me about quantum computing" },
          ],
        },
        response: {
          id: "r1",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [
            {
              index: 0,
              message: {
                role: "assistant",
                content: "Quantum computing is...",
                refusal: null,
              },
              finish_reason: "stop",
              logprobs: null,
            },
          ],
        },
        type: "openai:chatCompletions",
      });

      await InteractionModel.create({
        profileId: agent.id,
        sessionId: "session-2",
        request: {
          model: "gpt-4",
          messages: [{ role: "user", content: "How do I make a sandwich?" }],
        },
        response: {
          id: "r2",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [
            {
              index: 0,
              message: {
                role: "assistant",
                content: "To make a sandwich...",
                refusal: null,
              },
              finish_reason: "stop",
              logprobs: null,
            },
          ],
        },
        type: "openai:chatCompletions",
      });

      // Search with lowercase
      const sessions = await InteractionModel.getSessions(
        { limit: 100, offset: 0 },
        admin.id,
        true,
        { search: "quantum" },
      );

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

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

      await InteractionModel.create({
        profileId: agent.id,
        sessionId: "session-with-special-response",
        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:
                  "This response contains UniqueSearchableKeyword12345 for testing",
                refusal: null,
              },
              finish_reason: "stop",
              logprobs: null,
            },
          ],
        },
        type: "openai:chatCompletions",
      });

      await InteractionModel.create({
        profileId: agent.id,
        sessionId: "other-session",
        request: {
          model: "gpt-4",
          messages: [{ role: "user", content: "Test message" }],
        },
        response: {
          id: "r2",
          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 sessions = await InteractionModel.getSessions(
        { limit: 100, offset: 0 },
        admin.id,
        true,
        { search: "UniqueSearchableKeyword12345" },
      );

      expect(sessions.data).toHaveLength(1);
      expect(sessions.data[0].sessionId).toBe("session-with-special-response");
    });

    test("search returns multiple matching sessions", async ({ makeAdmin }) => {
      const admin = await makeAdmin();
      const agent = await AgentModel.create({
        name: "Agent",
        teams: [],
        scope: "org",
      });

      await InteractionModel.create({
        profileId: agent.id,
        sessionId: "python-session-1",
        request: {
          model: "gpt-4",
          messages: [{ role: "user", content: "Help me with Python code" }],
        },
        response: {
          id: "r1",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

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

      await InteractionModel.create({
        profileId: agent.id,
        sessionId: "javascript-session",
        request: {
          model: "gpt-4",
          messages: [{ role: "user", content: "JavaScript question" }],
        },
        response: {
          id: "r3",
          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,
        { search: "python" },
      );

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

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

      await InteractionModel.create({
        profileId: agent.id,
        sessionId: "test-session",
        request: {
          model: "gpt-4",
          messages: [{ role: "user", content: "Hello there" }],
        },
        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,
        { search: "nonexistentsearchterm987654" },
      );

      expect(sessions.data).toHaveLength(0);
    });

    test("search combined 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",
      });

      // Agent 1 with searchable content
      await InteractionModel.create({
        profileId: agent1.id,
        sessionId: "agent1-ml-session",
        request: {
          model: "gpt-4",
          messages: [{ role: "user", content: "Machine learning question" }],
        },
        response: {
          id: "r1",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      // Agent 2 with same searchable content
      await InteractionModel.create({
        profileId: agent2.id,
        sessionId: "agent2-ml-session",
        request: {
          model: "gpt-4",
          messages: [
            { role: "user", content: "Another machine learning topic" },
          ],
        },
        response: {
          id: "r2",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      // Search + profile filter
      const sessions = await InteractionModel.getSessions(
        { limit: 100, offset: 0 },
        admin.id,
        true,
        { search: "machine learning", profileId: agent1.id },
      );

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

    test("search combined with date filter", async ({ makeAdmin }) => {
      const admin = await makeAdmin();
      const agent = await AgentModel.create({
        name: "Agent",
        teams: [],
        scope: "org",
      });

      await InteractionModel.create({
        profileId: agent.id,
        sessionId: "searchable-session",
        request: {
          model: "gpt-4",
          messages: [{ role: "user", content: "Unique search term XYZ789" }],
        },
        response: {
          id: "r1",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      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,
        { search: "XYZ789", startDate, endDate },
      );

      expect(sessions.data.length).toBeGreaterThanOrEqual(1);
      expect(sessions.data[0].sessionId).toBe("searchable-session");
    });

    test("searches by conversation title (case insensitive)", async ({
      makeAdmin,
      makeUser,
      makeOrganization,
    }) => {
      const admin = await makeAdmin();
      const user = await makeUser();
      const org = await makeOrganization();
      const agent = await AgentModel.create({
        name: "Agent",
        teams: [],
        scope: "org",
      });

      // Create a conversation with a searchable title
      const conversation = await ConversationModel.create({
        userId: user.id,
        organizationId: org.id,
        agentId: agent.id,
        title: "UniqueConversationTitle789 about quantum physics",
      });

      // Create an interaction linked to the conversation via sessionId
      await InteractionModel.create({
        profileId: agent.id,
        sessionId: conversation.id, // Session ID = Conversation ID for chat sessions
        request: {
          model: "gpt-4",
          messages: [{ role: "user", content: "Tell me about atoms" }],
        },
        response: {
          id: "r1",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [
            {
              index: 0,
              message: {
                role: "assistant",
                content: "Atoms are...",
                refusal: null,
              },
              finish_reason: "stop",
              logprobs: null,
            },
          ],
        },
        type: "openai:chatCompletions",
      });

      // Create another conversation without the search term in title
      const conversation2 = await ConversationModel.create({
        userId: user.id,
        organizationId: org.id,
        agentId: agent.id,
        title: "Discussion about cooking",
      });

      await InteractionModel.create({
        profileId: agent.id,
        sessionId: conversation2.id,
        request: {
          model: "gpt-4",
          messages: [{ role: "user", content: "How to make pasta?" }],
        },
        response: {
          id: "r2",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      // Search by conversation title (case insensitive)
      const sessions = await InteractionModel.getSessions(
        { limit: 100, offset: 0 },
        admin.id,
        true,
        { search: "uniqueconversationtitle789" },
      );

      expect(sessions.data).toHaveLength(1);
      expect(sessions.data[0].sessionId).toBe(conversation.id);
      expect(sessions.data[0].conversationTitle).toBe(
        "UniqueConversationTitle789 about quantum physics",
      );
    });

    test("searches match conversation title OR request/response content", async ({
      makeAdmin,
      makeUser,
      makeOrganization,
    }) => {
      const admin = await makeAdmin();
      const user = await makeUser();
      const org = await makeOrganization();
      const agent = await AgentModel.create({
        name: "Agent",
        teams: [],
        scope: "org",
      });

      // Conversation with "SharedSearchTerm" in title
      const conversation1 = await ConversationModel.create({
        userId: user.id,
        organizationId: org.id,
        agentId: agent.id,
        title: "Discussion about SharedSearchTerm",
      });

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

      // Conversation with "SharedSearchTerm" in request content
      const conversation2 = await ConversationModel.create({
        userId: user.id,
        organizationId: org.id,
        agentId: agent.id,
        title: "Another discussion",
      });

      await InteractionModel.create({
        profileId: agent.id,
        sessionId: conversation2.id,
        request: {
          model: "gpt-4",
          messages: [
            { role: "user", content: "Question about SharedSearchTerm topic" },
          ],
        },
        response: {
          id: "r2",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      // Conversation without the search term
      const conversation3 = await ConversationModel.create({
        userId: user.id,
        organizationId: org.id,
        agentId: agent.id,
        title: "Unrelated conversation",
      });

      await InteractionModel.create({
        profileId: agent.id,
        sessionId: conversation3.id,
        request: {
          model: "gpt-4",
          messages: [{ role: "user", content: "Different question" }],
        },
        response: {
          id: "r3",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      // Search should find both conversations (title match + request content match)
      const sessions = await InteractionModel.getSessions(
        { limit: 100, offset: 0 },
        admin.id,
        true,
        { search: "SharedSearchTerm" },
      );

      expect(sessions.data).toHaveLength(2);
      const sessionIds = sessions.data.map((s) => s.sessionId);
      expect(sessionIds).toContain(conversation1.id);
      expect(sessionIds).toContain(conversation2.id);
    });

    test("conversation title search returns correct total count for pagination", async ({
      makeAdmin,
      makeUser,
      makeOrganization,
    }) => {
      // This test verifies that the count query includes the LEFT JOIN with conversations table
      // when searching by conversation title, ensuring pagination total is accurate
      const admin = await makeAdmin();
      const user = await makeUser();
      const org = await makeOrganization();
      const agent = await AgentModel.create({
        name: "Agent",
        teams: [],
        scope: "org",
      });

      // Create multiple conversations with searchable titles
      const searchTerm = "PaginationTestTitle";

      for (let i = 0; i < 5; i++) {
        const conversation = await ConversationModel.create({
          userId: user.id,
          organizationId: org.id,
          agentId: agent.id,
          title: `${searchTerm} conversation ${i}`,
        });

        await InteractionModel.create({
          profileId: agent.id,
          sessionId: conversation.id,
          request: {
            model: "gpt-4",
            messages: [{ role: "user", content: `Question ${i}` }],
          },
          response: {
            id: `r${i}`,
            object: "chat.completion",
            created: Date.now(),
            model: "gpt-4",
            choices: [],
          },
          type: "openai:chatCompletions",
        });
      }

      // Create a conversation without the search term (should not be included)
      const otherConversation = await ConversationModel.create({
        userId: user.id,
        organizationId: org.id,
        agentId: agent.id,
        title: "Unrelated title",
      });

      await InteractionModel.create({
        profileId: agent.id,
        sessionId: otherConversation.id,
        request: {
          model: "gpt-4",
          messages: [{ role: "user", content: "Unrelated question" }],
        },
        response: {
          id: "r-other",
          object: "chat.completion",
          created: Date.now(),
          model: "gpt-4",
          choices: [],
        },
        type: "openai:chatCompletions",
      });

      // Search with pagination limit smaller than total results
      const sessions = await InteractionModel.getSessions(
        { limit: 2, offset: 0 },
        admin.id,
        true,
        { search: searchTerm },
      );

      // Should return only 2 items due to limit, but total should be 5
      expect(sessions.data).toHaveLength(2);
      expect(sessions.pagination.total).toBe(5);

      // Verify second page works correctly
      const sessionsPage2 = await InteractionModel.getSessions(
        { limit: 2, offset: 2 },
        admin.id,
        true,
        { search: searchTerm },
      );

      expect(sessionsPage2.data).toHaveLength(2);
      expect(sessionsPage2.pagination.total).toBe(5);
    });
  });

  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("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();
    });
  });

  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);
    });
  });
});
