import { createId } from "@paralleldrive/cuid2";
import { OrganizationRole, Prisma, Project, WidgetPlacement } from "@prisma/client";
import { afterEach, describe, expect, test, vi } from "vitest";
import { prisma } from "@formbricks/database";
import { DatabaseError, ValidationError } from "@formbricks/types/errors";
import { ITEMS_PER_PAGE } from "../constants";
import {
  getProject,
  getProjectByEnvironmentId,
  getProjects,
  getUserProjectEnvironmentsByOrganizationIds,
  getUserProjects,
} from "./service";

vi.mock("@formbricks/database", () => ({
  prisma: {
    project: {
      findUnique: vi.fn(),
      findFirst: vi.fn(),
      findMany: vi.fn(),
    },
    membership: {
      findFirst: vi.fn(),
      findMany: vi.fn(),
    },
  },
}));

describe("Project Service", () => {
  afterEach(() => {
    vi.clearAllMocks();
  });

  test("getProject should return a project when it exists", async () => {
    const mockProject = {
      id: createId(),
      name: "Test Project",
      organizationId: createId(),
      createdAt: new Date(),
      updatedAt: new Date(),
      languages: ["en"],
      recontactDays: 0,
      linkSurveyBranding: true,
      inAppSurveyBranding: true,
      config: {
        channel: null,
        industry: null,
      },
      placement: WidgetPlacement.bottomRight,
      clickOutsideClose: true,
      overlay: "none",
      environments: [],
      styling: {
        allowStyleOverwrite: true,
      },
      logo: null,
      brandColor: null,
      highlightBorderColor: null,
    };

    vi.mocked(prisma.project.findUnique).mockResolvedValue(mockProject as unknown as Project);

    const result = await getProject(mockProject.id);

    expect(result).toEqual(mockProject);
    expect(prisma.project.findUnique).toHaveBeenCalledWith({
      where: {
        id: mockProject.id,
      },
      select: expect.any(Object),
    });
  });

  test("getProject should return null when project does not exist", async () => {
    vi.mocked(prisma.project.findUnique).mockResolvedValue(null);

    const result = await getProject(createId());

    expect(result).toBeNull();
  });

  test("getProject should throw DatabaseError when prisma throws", async () => {
    const prismaError = new Prisma.PrismaClientKnownRequestError("Database error", {
      code: "P2002",
      clientVersion: "5.0.0",
    });
    vi.mocked(prisma.project.findUnique).mockRejectedValue(prismaError);

    await expect(getProject(createId())).rejects.toThrow(DatabaseError);
  });

  test("getProjectByEnvironmentId should return a project when it exists", async () => {
    const mockProject = {
      id: createId(),
      name: "Test Project",
      organizationId: createId(),
      createdAt: new Date(),
      updatedAt: new Date(),
      languages: ["en"],
      recontactDays: 0,
      linkSurveyBranding: true,
      inAppSurveyBranding: true,
      config: {
        channel: null,
        industry: null,
      },
      placement: WidgetPlacement.bottomRight,
      clickOutsideClose: true,
      overlay: "none",
      environments: [],
      styling: {
        allowStyleOverwrite: true,
      },
      logo: null,
      brandColor: null,
      highlightBorderColor: null,
    };

    vi.mocked(prisma.project.findFirst).mockResolvedValue(mockProject as unknown as Project);

    const result = await getProjectByEnvironmentId(createId());

    expect(result).toEqual(mockProject);
    expect(prisma.project.findFirst).toHaveBeenCalledWith({
      where: {
        environments: {
          some: {
            id: expect.any(String),
          },
        },
      },
      select: expect.any(Object),
    });
  });

  test("getProjectByEnvironmentId should return null when project does not exist", async () => {
    vi.mocked(prisma.project.findFirst).mockResolvedValue(null);

    const result = await getProjectByEnvironmentId(createId());

    expect(result).toBeNull();
  });

  test("getProjectByEnvironmentId should throw DatabaseError when prisma throws", async () => {
    const prismaError = new Prisma.PrismaClientKnownRequestError("Database error", {
      code: "P2002",
      clientVersion: "5.0.0",
    });
    vi.mocked(prisma.project.findFirst).mockRejectedValue(prismaError);

    await expect(getProjectByEnvironmentId(createId())).rejects.toThrow(DatabaseError);
  });

  test("getUserProjects should return projects for admin user", async () => {
    const userId = createId();
    const organizationId = createId();
    const mockProjects = [
      {
        id: createId(),
        name: "Test Project 1",
        organizationId,
        createdAt: new Date(),
        updatedAt: new Date(),
        languages: ["en"],
        recontactDays: 0,
        linkSurveyBranding: true,
        inAppSurveyBranding: true,
        config: {
          channel: null,
          industry: null,
        },
        placement: WidgetPlacement.bottomRight,
        clickOutsideClose: true,
        overlay: "none",
        environments: [],
        styling: {
          allowStyleOverwrite: true,
        },
        logo: null,
        brandColor: null,
        highlightBorderColor: null,
      },
      {
        id: createId(),
        name: "Test Project 2",
        organizationId,
        createdAt: new Date(),
        updatedAt: new Date(),
        languages: ["en"],
        recontactDays: 0,
        linkSurveyBranding: true,
        inAppSurveyBranding: true,
        config: {
          channel: null,
          industry: null,
        },
        placement: WidgetPlacement.bottomRight,
        clickOutsideClose: true,
        overlay: "none",
        environments: [],
        styling: {
          allowStyleOverwrite: true,
        },
        logo: null,
        brandColor: null,
        highlightBorderColor: null,
      },
    ];

    vi.mocked(prisma.membership.findFirst).mockResolvedValue({
      userId,
      organizationId,
      role: OrganizationRole.owner,
      accepted: true,
    });

    vi.mocked(prisma.project.findMany).mockResolvedValue(mockProjects as unknown as Project[]);

    const result = await getUserProjects(userId, organizationId);

    expect(result).toEqual(mockProjects);
    expect(prisma.project.findMany).toHaveBeenCalledWith({
      where: {
        organizationId,
      },
      select: expect.any(Object),
      take: undefined,
      skip: undefined,
    });
  });

  test("getUserProjects should return projects for member user", async () => {
    const userId = createId();
    const organizationId = createId();
    const mockProjects = [
      {
        id: createId(),
        name: "Test Project 1",
        organizationId,
        createdAt: new Date(),
        updatedAt: new Date(),
        languages: ["en"],
        recontactDays: 0,
        linkSurveyBranding: true,
        inAppSurveyBranding: true,
        config: {
          channel: null,
          industry: null,
        },
        placement: WidgetPlacement.bottomRight,
        clickOutsideClose: true,
        overlay: "none",
        environments: [],
        styling: {
          allowStyleOverwrite: true,
        },
        logo: null,
        brandColor: null,
        highlightBorderColor: null,
      },
    ];

    vi.mocked(prisma.membership.findFirst).mockResolvedValue({
      userId,
      organizationId,
      role: OrganizationRole.member,
      accepted: true,
    });

    vi.mocked(prisma.project.findMany).mockResolvedValue(mockProjects as unknown as Project[]);

    const result = await getUserProjects(userId, organizationId);

    expect(result).toEqual(mockProjects);
    expect(prisma.project.findMany).toHaveBeenCalledWith({
      where: {
        organizationId,
        projectTeams: {
          some: {
            team: {
              teamUsers: {
                some: {
                  userId,
                },
              },
            },
          },
        },
      },
      select: expect.any(Object),
      take: undefined,
      skip: undefined,
    });
  });

  test("getUserProjects should throw ValidationError when user is not a member of organization", async () => {
    const userId = createId();
    const organizationId = createId();

    vi.mocked(prisma.membership.findFirst).mockResolvedValue(null);

    await expect(getUserProjects(userId, organizationId)).rejects.toThrow(ValidationError);
  });

  test("getUserProjects should handle pagination", async () => {
    const userId = createId();
    const organizationId = createId();
    const mockProjects = [
      {
        id: createId(),
        name: "Test Project 1",
        organizationId,
        createdAt: new Date(),
        updatedAt: new Date(),
        languages: ["en"],
        recontactDays: 0,
        linkSurveyBranding: true,
        inAppSurveyBranding: true,
        config: {
          channel: null,
          industry: null,
        },
        placement: WidgetPlacement.bottomRight,
        clickOutsideClose: true,
        overlay: "none",
        environments: [],
        styling: {
          allowStyleOverwrite: true,
        },
        logo: null,
        brandColor: null,
        highlightBorderColor: null,
      },
    ];

    vi.mocked(prisma.membership.findFirst).mockResolvedValue({
      userId,
      organizationId,
      role: OrganizationRole.owner,
      accepted: true,
    });

    vi.mocked(prisma.project.findMany).mockResolvedValue(mockProjects as unknown as Project[]);

    const page = 2;
    const result = await getUserProjects(userId, organizationId, page);

    expect(result).toEqual(mockProjects);
    expect(prisma.project.findMany).toHaveBeenCalledWith({
      where: {
        organizationId,
      },
      select: expect.any(Object),
      take: ITEMS_PER_PAGE,
      skip: ITEMS_PER_PAGE * (page - 1),
    });
  });

  test("getProjects should return all projects for an organization", async () => {
    const organizationId = createId();
    const mockProjects = [
      {
        id: createId(),
        name: "Test Project 1",
        organizationId,
        createdAt: new Date(),
        updatedAt: new Date(),
        languages: ["en"],
        recontactDays: 0,
        linkSurveyBranding: true,
        inAppSurveyBranding: true,
        config: {
          channel: null,
          industry: null,
        },
        placement: WidgetPlacement.bottomRight,
        clickOutsideClose: true,
        overlay: "none",
        environments: [],
        styling: {
          allowStyleOverwrite: true,
        },
        logo: null,
        brandColor: null,
        highlightBorderColor: null,
      },
      {
        id: createId(),
        name: "Test Project 2",
        organizationId,
        createdAt: new Date(),
        updatedAt: new Date(),
        languages: ["en"],
        recontactDays: 0,
        linkSurveyBranding: true,
        inAppSurveyBranding: true,
        config: {
          channel: null,
          industry: null,
        },
        placement: WidgetPlacement.bottomRight,
        clickOutsideClose: true,
        overlay: "none",
        environments: [],
        styling: {
          allowStyleOverwrite: true,
        },
        logo: null,
        brandColor: null,
        highlightBorderColor: null,
      },
    ];

    vi.mocked(prisma.project.findMany).mockResolvedValue(mockProjects as unknown as Project[]);

    const result = await getProjects(organizationId);

    expect(result).toEqual(mockProjects);
    expect(prisma.project.findMany).toHaveBeenCalledWith({
      where: {
        organizationId,
      },
      select: expect.any(Object),
      take: undefined,
      skip: undefined,
    });
  });

  test("getProjects should handle pagination", async () => {
    const organizationId = createId();
    const mockProjects = [
      {
        id: createId(),
        name: "Test Project 1",
        organizationId,
        createdAt: new Date(),
        updatedAt: new Date(),
        languages: ["en"],
        recontactDays: 0,
        linkSurveyBranding: true,
        inAppSurveyBranding: true,
        config: {
          channel: null,
          industry: null,
        },
        placement: WidgetPlacement.bottomRight,
        clickOutsideClose: true,
        overlay: "none",
        environments: [],
        styling: {
          allowStyleOverwrite: true,
        },
        logo: null,
        brandColor: null,
        highlightBorderColor: null,
      },
    ];

    vi.mocked(prisma.project.findMany).mockResolvedValue(mockProjects as unknown as Project[]);

    const page = 2;
    const result = await getProjects(organizationId, page);

    expect(result).toEqual(mockProjects);
    expect(prisma.project.findMany).toHaveBeenCalledWith({
      where: {
        organizationId,
      },
      select: expect.any(Object),
      take: ITEMS_PER_PAGE,
      skip: ITEMS_PER_PAGE * (page - 1),
    });
  });

  test("getProjects should throw DatabaseError when prisma throws", async () => {
    const organizationId = createId();
    const prismaError = new Prisma.PrismaClientKnownRequestError("Database error", {
      code: "P2002",
      clientVersion: "5.0.0",
    });
    vi.mocked(prisma.project.findMany).mockRejectedValue(prismaError);

    await expect(getProjects(organizationId)).rejects.toThrow(DatabaseError);
  });

  test("getProjectsByOrganizationIds should return projects for given organization IDs", async () => {
    const organizationId1 = createId();
    const organizationId2 = createId();
    const userId = createId();
    const mockProjects = [
      {
        environments: [],
      },
      {
        environments: [],
      },
    ];

    vi.mocked(prisma.membership.findMany).mockResolvedValue([
      {
        userId,
        organizationId: organizationId1,
        role: "owner" as any,
        accepted: true,
      },
      {
        userId,
        organizationId: organizationId2,
        role: "owner" as any,
        accepted: true,
      },
    ]);

    vi.mocked(prisma.project.findMany).mockResolvedValue(mockProjects as any);

    const result = await getUserProjectEnvironmentsByOrganizationIds(
      [organizationId1, organizationId2],
      userId
    );

    expect(result).toEqual(mockProjects);
    expect(prisma.project.findMany).toHaveBeenCalledWith({
      where: {
        OR: [{ organizationId: organizationId1 }, { organizationId: organizationId2 }],
      },
      select: { environments: true },
    });
  });

  test("getProjectsByOrganizationIds should return empty array when no projects are found", async () => {
    const organizationId1 = createId();
    const organizationId2 = createId();
    const userId = createId();

    vi.mocked(prisma.membership.findMany).mockResolvedValue([
      {
        userId,
        organizationId: organizationId1,
        role: "owner" as any,
        accepted: true,
      },
      {
        userId,
        organizationId: organizationId2,
        role: "owner" as any,
        accepted: true,
      },
    ]);

    vi.mocked(prisma.project.findMany).mockResolvedValue([]);

    const result = await getUserProjectEnvironmentsByOrganizationIds(
      [organizationId1, organizationId2],
      userId
    );

    expect(result).toEqual([]);
    expect(prisma.project.findMany).toHaveBeenCalledWith({
      where: {
        OR: [{ organizationId: organizationId1 }, { organizationId: organizationId2 }],
      },
      select: { environments: true },
    });
  });

  test("getProjectsByOrganizationIds should throw DatabaseError when prisma throws", async () => {
    const organizationId1 = createId();
    const organizationId2 = createId();
    const userId = createId();
    const prismaError = new Prisma.PrismaClientKnownRequestError("Database error", {
      code: "P2002",
      clientVersion: "5.0.0",
    });

    vi.mocked(prisma.membership.findMany).mockResolvedValue([
      {
        userId,
        organizationId: organizationId1,
        role: "owner" as any,
        accepted: true,
      },
    ]);

    vi.mocked(prisma.project.findMany).mockRejectedValue(prismaError);

    await expect(
      getUserProjectEnvironmentsByOrganizationIds([organizationId1, organizationId2], userId)
    ).rejects.toThrow(DatabaseError);
  });

  test("getProjectsByOrganizationIds should throw ValidationError with wrong input", async () => {
    const userId = createId();
    await expect(getUserProjectEnvironmentsByOrganizationIds(["wrong-id"], userId)).rejects.toThrow(
      ValidationError
    );
  });

  test("getProjectsByOrganizationIds should return empty array when user has no memberships", async () => {
    const organizationId1 = createId();
    const organizationId2 = createId();
    const userId = createId();

    // Mock no memberships found
    vi.mocked(prisma.membership.findMany).mockResolvedValue([]);

    const result = await getUserProjectEnvironmentsByOrganizationIds(
      [organizationId1, organizationId2],
      userId
    );

    expect(result).toEqual([]);
    expect(prisma.membership.findMany).toHaveBeenCalledWith({
      where: {
        userId,
        organizationId: {
          in: [organizationId1, organizationId2],
        },
      },
    });
    // Should not call project.findMany when no memberships
    expect(prisma.project.findMany).not.toHaveBeenCalled();
  });

  test("getProjectsByOrganizationIds should handle member role with team access", async () => {
    const organizationId1 = createId();
    const organizationId2 = createId();
    const userId = createId();
    const mockProjects = [
      {
        environments: [],
      },
    ];

    // Mock membership where user is a member
    vi.mocked(prisma.membership.findMany).mockResolvedValue([
      {
        userId,
        organizationId: organizationId1,
        role: "member" as any,
        accepted: true,
      },
    ]);

    vi.mocked(prisma.project.findMany).mockResolvedValue(mockProjects as any);

    const result = await getUserProjectEnvironmentsByOrganizationIds(
      [organizationId1, organizationId2],
      userId
    );

    expect(result).toEqual(mockProjects);
    expect(prisma.project.findMany).toHaveBeenCalledWith({
      where: {
        OR: [
          {
            organizationId: organizationId1,
            projectTeams: {
              some: {
                team: {
                  teamUsers: {
                    some: {
                      userId,
                    },
                  },
                },
              },
            },
          },
        ],
      },
      select: { environments: true },
    });
  });
});
