import { ProjectShareModel } from "@/models";
import ConversationModel from "@/models/conversation";
import ConversationShareModel from "@/models/conversation-share";
import MessageModel from "@/models/message";
import type { FastifyInstanceWithZod } from "@/server";
import { createFastifyInstance } from "@/server";
import { projectService } from "@/services/project";
import { afterEach, beforeEach, describe, expect, test } from "@/test";
import type { User } from "@/types";

describe("chat share routes", () => {
  let app: FastifyInstanceWithZod;
  let currentUser: User;
  let organizationId: string;

  beforeEach(async ({ makeOrganization, makeUser }) => {
    currentUser = await makeUser();
    const organization = await makeOrganization();
    organizationId = organization.id;

    app = createFastifyInstance();
    app.addHook("onRequest", async (request) => {
      (request as typeof request & { user: typeof currentUser }).user =
        currentUser;
      (request as typeof request & { organizationId: string }).organizationId =
        organizationId;
    });

    const { default: chatRoutes } = await import("./routes");
    await app.register(chatRoutes);
  });

  afterEach(async () => {
    await app.close();
  });

  test("shares a conversation with selected teams", async ({
    makeAgent,
    makeMember,
    makeTeam,
  }) => {
    await makeMember(currentUser.id, organizationId);
    const agent = await makeAgent({
      organizationId,
      teams: [],
    });
    const team = await makeTeam(organizationId, currentUser.id, {
      name: "Engineering",
    });
    const conversation = await ConversationModel.create({
      userId: currentUser.id,
      organizationId,
      agentId: agent.id,
    });

    const response = await app.inject({
      method: "POST",
      url: `/api/chat/conversations/${conversation.id}/share`,
      payload: {
        visibility: "team",
        teamIds: [team.id],
      },
    });

    expect(response.statusCode).toBe(200);
    expect(response.json()).toMatchObject({
      conversationId: conversation.id,
      visibility: "team",
      teamIds: [team.id],
      userIds: [],
    });
  });

  test("rejects users outside the organization", async ({
    makeAgent,
    makeMember,
    makeUser,
  }) => {
    await makeMember(currentUser.id, organizationId);
    const outsider = await makeUser();
    const agent = await makeAgent({
      organizationId,
      teams: [],
    });
    const conversation = await ConversationModel.create({
      userId: currentUser.id,
      organizationId,
      agentId: agent.id,
    });

    const response = await app.inject({
      method: "POST",
      url: `/api/chat/conversations/${conversation.id}/share`,
      payload: {
        visibility: "user",
        userIds: [outsider.id],
      },
    });

    expect(response.statusCode).toBe(400);
    expect(response.json().error.message).toBe(
      "One or more selected users are invalid",
    );
  });

  test("blocks users who are outside the share scope", async ({
    makeAgent,
    makeMember,
    makeUser,
  }) => {
    const owner = currentUser;
    const invitedUser = await makeUser();
    const outsider = await makeUser();

    await makeMember(owner.id, organizationId);
    await makeMember(invitedUser.id, organizationId);
    await makeMember(outsider.id, organizationId);

    const agent = await makeAgent({
      organizationId,
      teams: [],
    });
    const conversation = await ConversationModel.create({
      userId: owner.id,
      organizationId,
      agentId: agent.id,
    });

    const share = await ConversationShareModel.upsert({
      conversationId: conversation.id,
      organizationId,
      createdByUserId: owner.id,
      visibility: "user",
      teamIds: [],
      userIds: [invitedUser.id],
    });

    currentUser = outsider;

    const response = await app.inject({
      method: "GET",
      url: `/api/chat/shared/${share.id}`,
    });

    expect(response.statusCode).toBe(404);
    expect(response.json().error.message).toBe("Shared conversation not found");
  });

  test("forks a shared conversation with the original accessible agent", async ({
    makeAgent,
    makeMember,
    makeUser,
  }) => {
    const owner = currentUser;
    const viewer = await makeUser();

    await makeMember(owner.id, organizationId);
    await makeMember(viewer.id, organizationId);

    const sharedAgent = await makeAgent({
      organizationId,
      teams: [],
    });
    const conversation = await ConversationModel.create({
      userId: owner.id,
      organizationId,
      agentId: sharedAgent.id,
    });
    await MessageModel.create({
      conversationId: conversation.id,
      role: "assistant",
      content: {
        id: "message-1",
        role: "assistant",
        parts: [{ type: "text", text: "Shared conversation result" }],
      },
    });
    const share = await ConversationShareModel.upsert({
      conversationId: conversation.id,
      organizationId,
      createdByUserId: owner.id,
      visibility: "organization",
      teamIds: [],
      userIds: [],
    });

    currentUser = viewer;

    const response = await app.inject({
      method: "POST",
      url: `/api/chat/shared/${share.id}/fork`,
      payload: {
        agentId: sharedAgent.id,
      },
    });

    expect(response.statusCode).toBe(200);
    expect(response.json()).toMatchObject({
      agentId: sharedAgent.id,
      userId: viewer.id,
      messages: [
        expect.objectContaining({
          id: expect.any(String),
          parts: [{ type: "text", text: "Shared conversation result" }],
        }),
      ],
    });
  });

  test("keeps the project link when forking a shared conversation in a project the forker can access", async ({
    makeAgent,
    makeMember,
    makeUser,
  }) => {
    const owner = currentUser;
    const viewer = await makeUser();

    await makeMember(owner.id, organizationId);
    await makeMember(viewer.id, organizationId);

    const sharedAgent = await makeAgent({ organizationId, teams: [] });

    const project = await projectService.create({
      organizationId,
      userId: owner.id,
      name: "shared-project",
      description: null,
    });
    // Share the project org-wide so the viewer can access it (and so a chat
    // started from the shared chat belongs in the project).
    await ProjectShareModel.upsert({
      projectId: project.id,
      organizationId,
      createdByUserId: owner.id,
      visibility: "organization",
      teamIds: [],
    });

    const conversation = await ConversationModel.create({
      userId: owner.id,
      organizationId,
      agentId: sharedAgent.id,
      projectId: project.id,
    });
    const share = await ConversationShareModel.upsert({
      conversationId: conversation.id,
      organizationId,
      createdByUserId: owner.id,
      visibility: "organization",
      teamIds: [],
      userIds: [],
    });

    currentUser = viewer;

    const response = await app.inject({
      method: "POST",
      url: `/api/chat/shared/${share.id}/fork`,
      payload: { agentId: sharedAgent.id },
    });

    expect(response.statusCode).toBe(200);
    expect(response.json().projectId).toBe(project.id);
  });

  test("drops the project link when the forker cannot access the source's project", async ({
    makeAgent,
    makeMember,
    makeUser,
  }) => {
    const owner = currentUser;
    const viewer = await makeUser();

    await makeMember(owner.id, organizationId);
    await makeMember(viewer.id, organizationId);

    const sharedAgent = await makeAgent({ organizationId, teams: [] });

    // Owner-only project (no share row): the conversation is shared, but the
    // project is not, so the fork must not attach to a project the viewer
    // cannot see (which would be invisible and unmanageable to them).
    const project = await projectService.create({
      organizationId,
      userId: owner.id,
      name: "private-project",
      description: null,
    });

    const conversation = await ConversationModel.create({
      userId: owner.id,
      organizationId,
      agentId: sharedAgent.id,
      projectId: project.id,
    });
    const share = await ConversationShareModel.upsert({
      conversationId: conversation.id,
      organizationId,
      createdByUserId: owner.id,
      visibility: "organization",
      teamIds: [],
      userIds: [],
    });

    currentUser = viewer;

    const response = await app.inject({
      method: "POST",
      url: `/api/chat/shared/${share.id}/fork`,
      payload: { agentId: sharedAgent.id },
    });

    expect(response.statusCode).toBe(200);
    expect(response.json().projectId).toBeNull();
  });

  test("does not fork a shared conversation with an inaccessible agent", async ({
    makeAgent,
    makeMember,
    makeTeam,
    makeUser,
  }) => {
    const owner = currentUser;
    const viewer = await makeUser();

    await makeMember(owner.id, organizationId);
    await makeMember(viewer.id, organizationId);

    const ownerOnlyTeam = await makeTeam(organizationId, owner.id, {
      name: "Owner Only",
    });
    const sharedAgent = await makeAgent({
      organizationId,
      teams: [],
    });
    const restrictedAgent = await makeAgent({
      organizationId,
      scope: "team",
      teams: [ownerOnlyTeam.id],
    });
    const conversation = await ConversationModel.create({
      userId: owner.id,
      organizationId,
      agentId: sharedAgent.id,
    });
    const share = await ConversationShareModel.upsert({
      conversationId: conversation.id,
      organizationId,
      createdByUserId: owner.id,
      visibility: "organization",
      teamIds: [],
      userIds: [],
    });

    currentUser = viewer;

    const response = await app.inject({
      method: "POST",
      url: `/api/chat/shared/${share.id}/fork`,
      payload: {
        agentId: restrictedAgent.id,
      },
    });

    expect(response.statusCode).toBe(404);
    expect(response.json().error.message).toBe("Agent not found");
  });
});
