// utils.test.ts
// Pull in the mocked implementations to configure them in tests
import { getServerSession } from "next-auth";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { prisma } from "@formbricks/database";
import { TEnvironment } from "@formbricks/types/environment";
import { AuthenticationError, AuthorizationError, ResourceNotFoundError } from "@formbricks/types/errors";
import { TMembership } from "@formbricks/types/memberships";
import { TOrganization } from "@formbricks/types/organizations";
import { TProject } from "@formbricks/types/project";
import { TUser } from "@formbricks/types/user";
import { hasUserEnvironmentAccess } from "@/lib/environment/auth";
import { getEnvironment } from "@/lib/environment/service";
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
import { getAccessFlags } from "@/lib/membership/utils";
import {
  getMonthlyOrganizationResponseCount,
  getOrganizationByEnvironmentId,
} from "@/lib/organization/service";
import { getProjectByEnvironmentId } from "@/lib/project/service";
import { getUser } from "@/lib/user/service";
import { getEnterpriseLicense } from "@/modules/ee/license-check/lib/license";
import { getAccessControlPermission } from "@/modules/ee/license-check/lib/utils";
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
// Pull in the mocked implementations to configure them in tests
import {
  environmentIdLayoutChecks,
  getEnvironmentAuth,
  getEnvironmentLayoutData,
  getEnvironmentWithRelations,
} from "./utils";

// Mock all external dependencies
vi.mock("@/lingodotdev/server", () => ({
  getTranslate: vi.fn(() => (key: string) => key),
}));

vi.mock("next-auth", () => ({
  getServerSession: vi.fn(),
}));

vi.mock("@/modules/auth/lib/authOptions", () => ({
  authOptions: {},
}));

vi.mock("@/modules/ee/teams/lib/roles", () => ({
  getProjectPermissionByUserId: vi.fn(),
}));

vi.mock("@/modules/ee/teams/utils/teams", () => ({
  getTeamPermissionFlags: vi.fn(),
}));

vi.mock("@/lib/environment/auth", () => ({
  hasUserEnvironmentAccess: vi.fn(),
}));

vi.mock("@/lib/environment/service", () => ({
  getEnvironment: vi.fn(),
}));

vi.mock("@/lib/membership/service", () => ({
  getMembershipByUserIdOrganizationId: vi.fn(),
}));

vi.mock("@/lib/membership/utils", () => ({
  getAccessFlags: vi.fn(),
}));

vi.mock("@/lib/organization/service", () => ({
  getOrganizationByEnvironmentId: vi.fn(),
  getMonthlyOrganizationResponseCount: vi.fn(),
}));

vi.mock("@/lib/project/service", () => ({
  getProjectByEnvironmentId: vi.fn(),
}));

vi.mock("@/lib/user/service", () => ({
  getUser: vi.fn(),
}));

vi.mock("@/modules/ee/license-check/lib/license", () => ({
  getEnterpriseLicense: vi.fn(),
}));

vi.mock("@/modules/ee/license-check/lib/utils", () => ({
  getAccessControlPermission: vi.fn(),
}));

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

vi.mock("@/lib/constants", () => ({
  IS_FORMBRICKS_CLOUD: false,
}));

vi.mock("@formbricks/types/errors", () => ({
  AuthenticationError: class AuthenticationError extends Error {
    constructor(message: string) {
      super(message);
      this.name = "AuthenticationError";
    }
  },
  AuthorizationError: class AuthorizationError extends Error {},
  DatabaseError: class DatabaseError extends Error {},
  ResourceNotFoundError: class ResourceNotFoundError extends Error {
    constructor(resource: string, id: string | null) {
      super(`${resource} not found${id ? `: ${id}` : ""}`);
      this.name = "ResourceNotFoundError";
    }
  },
}));

describe("utils.ts", () => {
  beforeEach(() => {
    // Clear all mocks before each test
    vi.clearAllMocks();

    // Provide default mocks for successful scenario
    vi.mocked(getServerSession).mockResolvedValue({ user: { id: "user123" } });
    vi.mocked(getEnvironment).mockResolvedValue({ id: "env123" } as TEnvironment);
    vi.mocked(getProjectByEnvironmentId).mockResolvedValue({ id: "proj123" } as TProject);
    vi.mocked(getOrganizationByEnvironmentId).mockResolvedValue({ id: "org123" } as TOrganization);
    vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue({
      role: "member",
    } as unknown as TMembership);
    vi.mocked(getAccessFlags).mockReturnValue({
      isMember: true,
      isOwner: false,
      isManager: false,
      isBilling: false,
    });
    vi.mocked(getProjectPermissionByUserId).mockResolvedValue("read");
    vi.mocked(getTeamPermissionFlags).mockReturnValue({
      hasReadAccess: true,
      hasReadWriteAccess: true,
      hasManageAccess: true,
    });
    vi.mocked(hasUserEnvironmentAccess).mockResolvedValue(true);
    vi.mocked(getUser).mockResolvedValue({ id: "user123" } as TUser);
    vi.mocked(getEnterpriseLicense).mockResolvedValue({
      active: true,
      features: { isMultiOrgEnabled: false },
      lastChecked: new Date(),
      isPendingDowngrade: false,
      fallbackLevel: "none",
    } as any);
    vi.mocked(getAccessControlPermission).mockResolvedValue(true);
    vi.mocked(getMonthlyOrganizationResponseCount).mockResolvedValue(0);
  });

  describe("getEnvironmentAuth", () => {
    test("returns environment data on success", async () => {
      const result = await getEnvironmentAuth("env123");
      expect(result.environment.id).toBe("env123");
      expect(result.project.id).toBe("proj123");
      expect(result.organization.id).toBe("org123");
      expect(result.session.user.id).toBe("user123");
      expect(result.isReadOnly).toBe(true); // from mocks (isMember = true & hasReadAccess = true)
    });

    test("throws error if project not found", async () => {
      vi.mocked(getProjectByEnvironmentId).mockResolvedValueOnce(null);
      await expect(getEnvironmentAuth("env123")).rejects.toThrow(ResourceNotFoundError);
    });

    test("throws error if environment not found", async () => {
      vi.mocked(getEnvironment).mockResolvedValueOnce(null);
      await expect(getEnvironmentAuth("env123")).rejects.toThrow(ResourceNotFoundError);
    });

    test("throws error if session not found", async () => {
      vi.mocked(getServerSession).mockResolvedValueOnce(null);
      await expect(getEnvironmentAuth("env123")).rejects.toThrow(AuthenticationError);
    });

    test("throws error if organization not found", async () => {
      vi.mocked(getOrganizationByEnvironmentId).mockResolvedValueOnce(null);
      await expect(getEnvironmentAuth("env123")).rejects.toThrow(ResourceNotFoundError);
    });

    test("throws AuthorizationError if membership not found", async () => {
      vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValueOnce(null);
      await expect(getEnvironmentAuth("env123")).rejects.toThrow(AuthorizationError);
    });
  });

  describe("environmentIdLayoutChecks", () => {
    test("returns t, session, user, and organization on success", async () => {
      const result = await environmentIdLayoutChecks("env123");
      expect(result.t).toBeInstanceOf(Function);
      expect(result.session?.user.id).toBe("user123");
      expect(result.user?.id).toBe("user123");
      expect(result.organization?.id).toBe("org123");
    });

    test("returns session=null and user=null if session does not have user", async () => {
      vi.mocked(getServerSession).mockResolvedValueOnce({});
      const result = await environmentIdLayoutChecks("env123");
      expect(result.session).toBe(null);
      expect(result.user).toBe(null);
      expect(result.organization).toBe(null);
    });

    test("returns user=null if user is not found", async () => {
      vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user123" } });
      vi.mocked(getUser).mockResolvedValueOnce(null);
      const result = await environmentIdLayoutChecks("env123");
      expect(result.session?.user.id).toBe("user123");
      expect(result.user).toBe(null);
      expect(result.organization).toBe(null);
    });

    test("throws AuthorizationError if user has no environment access", async () => {
      vi.mocked(hasUserEnvironmentAccess).mockResolvedValueOnce(false);
      await expect(environmentIdLayoutChecks("env123")).rejects.toThrow(AuthorizationError);
    });

    test("throws error if organization not found", async () => {
      vi.mocked(getOrganizationByEnvironmentId).mockResolvedValueOnce(null);
      await expect(environmentIdLayoutChecks("env123")).rejects.toThrow(ResourceNotFoundError);
    });
  });

  describe("getEnvironmentWithRelations", () => {
    const mockPrismaData = {
      id: "env123",
      createdAt: new Date("2024-01-01"),
      updatedAt: new Date("2024-01-02"),
      type: "production" as const,
      projectId: "proj123",
      appSetupCompleted: true,
      project: {
        id: "proj123",
        createdAt: new Date("2024-01-01"),
        updatedAt: new Date("2024-01-02"),
        name: "Test Project",
        organizationId: "org123",
        languages: ["en"],
        recontactDays: 7,
        linkSurveyBranding: true,
        inAppSurveyBranding: true,
        config: {},
        placement: "bottomRight" as const,
        clickOutsideClose: true,
        overlay: "none",
        styling: {},
        logo: null,
        environments: [
          {
            id: "env123",
            type: "production" as const,
            createdAt: new Date("2024-01-01"),
            updatedAt: new Date("2024-01-02"),
            projectId: "proj123",
            appSetupCompleted: true,
          },
          {
            id: "env456",
            type: "development" as const,
            createdAt: new Date("2024-01-01"),
            updatedAt: new Date("2024-01-02"),
            projectId: "proj123",
            appSetupCompleted: false,
          },
        ],
        organization: {
          id: "org123",
          createdAt: new Date("2024-01-01"),
          updatedAt: new Date("2024-01-02"),
          name: "Test Organization",
          billing: { stripeCustomerId: null, limits: {}, usageCycleAnchor: new Date() },
          isAISmartToolsEnabled: false,
          isAIDataAnalysisEnabled: false,
          whitelabel: false,
          memberships: [
            {
              userId: "user123",
              organizationId: "org123",
              accepted: true,
              role: "owner" as const,
            },
          ],
        },
      },
    };

    beforeEach(() => {
      vi.mocked(prisma.environment.findUnique).mockResolvedValue(mockPrismaData as any);
    });

    test("returns combined environment, project, organization, and membership data", async () => {
      const result = await getEnvironmentWithRelations("env123", "user123");

      expect(result).toBeDefined();
      expect(result!.environment.id).toBe("env123");
      expect(result!.environment.type).toBe("production");
      expect(result!.project.id).toBe("proj123");
      expect(result!.project.name).toBe("Test Project");
      expect(result!.organization.id).toBe("org123");
      expect(result!.organization.name).toBe("Test Organization");
      expect(result!.environments).toHaveLength(2);
      expect(result!.membership).toEqual({
        userId: "user123",
        organizationId: "org123",
        accepted: true,
        role: "owner",
      });
    });

    test("fetches only current user's membership using database-level filtering", async () => {
      await getEnvironmentWithRelations("env123", "user123");

      expect(prisma.environment.findUnique).toHaveBeenCalledWith({
        where: { id: "env123" },
        select: expect.objectContaining({
          project: expect.objectContaining({
            select: expect.objectContaining({
              organization: expect.objectContaining({
                select: expect.objectContaining({
                  memberships: expect.objectContaining({
                    where: { userId: "user123" },
                    take: 1,
                  }),
                }),
              }),
            }),
          }),
        }),
      });
    });

    test("returns null when environment not found", async () => {
      vi.mocked(prisma.environment.findUnique).mockResolvedValueOnce(null);

      const result = await getEnvironmentWithRelations("env123", "user123");

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

    test("returns null membership when user has no membership", async () => {
      const dataWithoutMembership = {
        ...mockPrismaData,
        project: {
          ...mockPrismaData.project,
          organization: {
            ...mockPrismaData.project.organization,
            memberships: [], // No memberships
          },
        },
      };
      vi.mocked(prisma.environment.findUnique).mockResolvedValueOnce(dataWithoutMembership as any);

      const result = await getEnvironmentWithRelations("env123", "user123");

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

    test("throws error on database failure", async () => {
      // Mock a database error
      const dbError = new Error("Database connection failed");
      vi.mocked(prisma.environment.findUnique).mockRejectedValueOnce(dbError);

      // Verify function throws (specific error type depends on Prisma error detection)
      await expect(getEnvironmentWithRelations("env123", "user123")).rejects.toThrow();
    });

    // Note: Input validation for environmentId and userId is handled by
    // getEnvironmentLayoutData (the parent function), not here.
    // See getEnvironmentLayoutData tests for validation coverage.
  });

  describe("getEnvironmentLayoutData", () => {
    beforeEach(() => {
      vi.mocked(prisma.environment.findUnique).mockResolvedValue({
        id: "env123",
        createdAt: new Date("2024-01-01"),
        updatedAt: new Date("2024-01-02"),
        type: "production",
        projectId: "proj123",
        appSetupCompleted: true,
        project: {
          id: "proj123",
          createdAt: new Date("2024-01-01"),
          updatedAt: new Date("2024-01-02"),
          name: "Test Project",
          organizationId: "org123",
          languages: ["en"],
          recontactDays: 7,
          linkSurveyBranding: true,
          inAppSurveyBranding: true,
          config: {},
          placement: "bottomRight",
          clickOutsideClose: true,
          overlay: "none",
          styling: {},
          logo: null,
          environments: [
            {
              id: "env123",
              type: "production",
              createdAt: new Date("2024-01-01"),
              updatedAt: new Date("2024-01-02"),
              projectId: "proj123",
              appSetupCompleted: true,
            },
          ],
          organization: {
            id: "org123",
            createdAt: new Date("2024-01-01"),
            updatedAt: new Date("2024-01-02"),
            name: "Test Organization",
            billing: { stripeCustomerId: null, limits: {}, usageCycleAnchor: new Date() },
            isAISmartToolsEnabled: false,
            isAIDataAnalysisEnabled: false,
            whitelabel: false,
            memberships: [
              {
                userId: "user123",
                organizationId: "org123",
                accepted: true,
                role: "owner",
              },
            ],
          },
        },
      } as any);
    });

    test("returns complete layout data on success", async () => {
      const result = await getEnvironmentLayoutData("env123", "user123");

      expect(result).toBeDefined();
      expect(result.session).toBeDefined();
      expect(result.user).toBeDefined();
      expect(result.environment).toBeDefined();
      expect(result.project).toBeDefined();
      expect(result.organization).toBeDefined();
      expect(result.environments).toBeDefined();
      expect(result.membership).toBeDefined();
      expect(result.isAccessControlAllowed).toBeDefined();
      expect(result.projectPermission).toBeDefined();
      expect(result.license).toBeDefined();
      expect(result.responseCount).toBe(0);
    });

    test("validates environmentId input", async () => {
      await expect(getEnvironmentLayoutData("", "user123")).rejects.toThrow();
    });

    test("validates userId input", async () => {
      await expect(getEnvironmentLayoutData("env123", "")).rejects.toThrow();
    });

    test("throws error if session not found", async () => {
      vi.mocked(getServerSession).mockResolvedValueOnce(null);

      await expect(getEnvironmentLayoutData("env123", "user123")).rejects.toThrow(AuthenticationError);
    });

    test("throws error if userId doesn't match session", async () => {
      vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "different-user" } } as any);

      await expect(getEnvironmentLayoutData("env123", "user123")).rejects.toThrow("User ID mismatch");
    });

    test("throws error if user not found", async () => {
      vi.mocked(getUser).mockResolvedValueOnce(null);

      await expect(getEnvironmentLayoutData("env123", "user123")).rejects.toThrow(AuthenticationError);
    });

    test("throws error if environment data not found", async () => {
      vi.mocked(prisma.environment.findUnique).mockResolvedValueOnce(null);

      await expect(getEnvironmentLayoutData("env123", "user123")).rejects.toThrow(ResourceNotFoundError);
    });

    test("throws AuthorizationError if user has no environment access", async () => {
      vi.mocked(hasUserEnvironmentAccess).mockResolvedValueOnce(false);

      await expect(getEnvironmentLayoutData("env123", "user123")).rejects.toThrow(AuthorizationError);
    });

    test("throws ResourceNotFoundError if organization billing is missing", async () => {
      vi.mocked(prisma.environment.findUnique).mockResolvedValueOnce({
        id: "env123",
        createdAt: new Date("2024-01-01"),
        updatedAt: new Date("2024-01-02"),
        type: "production",
        projectId: "proj123",
        appSetupCompleted: true,
        project: {
          id: "proj123",
          createdAt: new Date("2024-01-01"),
          updatedAt: new Date("2024-01-02"),
          name: "Test Project",
          organizationId: "org123",
          languages: ["en"],
          recontactDays: 7,
          linkSurveyBranding: true,
          inAppSurveyBranding: true,
          config: {},
          placement: "bottomRight",
          clickOutsideClose: true,
          overlay: "none",
          styling: {},
          logo: null,
          environments: [
            {
              id: "env123",
              type: "production",
              createdAt: new Date("2024-01-01"),
              updatedAt: new Date("2024-01-02"),
              projectId: "proj123",
              appSetupCompleted: true,
            },
          ],
          organization: {
            id: "org123",
            createdAt: new Date("2024-01-01"),
            updatedAt: new Date("2024-01-02"),
            name: "Test Organization",
            billing: null,
            isAISmartToolsEnabled: false,
            isAIDataAnalysisEnabled: false,
            whitelabel: false,
            memberships: [
              {
                userId: "user123",
                organizationId: "org123",
                accepted: true,
                role: "owner",
              },
            ],
          },
        },
      } as any);

      await expect(getEnvironmentLayoutData("env123", "user123")).rejects.toThrow(
        new ResourceNotFoundError("OrganizationBilling", "org123")
      );
    });

    test("throws AuthorizationError if membership not found", async () => {
      vi.mocked(prisma.environment.findUnique).mockResolvedValueOnce({
        id: "env123",
        createdAt: new Date(),
        updatedAt: new Date(),
        type: "production",
        projectId: "proj123",
        appSetupCompleted: true,
        project: {
          id: "proj123",
          name: "Test Project",
          organizationId: "org123",
          createdAt: new Date(),
          updatedAt: new Date(),
          languages: ["en"],
          recontactDays: 7,
          linkSurveyBranding: true,
          inAppSurveyBranding: true,
          config: {},
          placement: "bottomRight",
          clickOutsideClose: true,
          overlay: "none",
          styling: {},
          logo: null,
          environments: [],
          organization: {
            id: "org123",
            name: "Test Organization",
            createdAt: new Date(),
            updatedAt: new Date(),
            billing: { stripeCustomerId: null, limits: {}, usageCycleAnchor: new Date() },
            isAISmartToolsEnabled: false,
            isAIDataAnalysisEnabled: false,
            whitelabel: false,
            memberships: [], // No membership
          },
        },
      } as any);

      await expect(getEnvironmentLayoutData("env123", "user123")).rejects.toThrow(AuthorizationError);
    });

    test("fetches user before auth check, then environment data after authorization", async () => {
      await getEnvironmentLayoutData("env123", "user123");

      // User is fetched first (needed for auth check)
      expect(getUser).toHaveBeenCalledWith("user123");
      // Environment data is fetched after authorization passes
      expect(prisma.environment.findUnique).toHaveBeenCalledWith(
        expect.objectContaining({
          where: { id: "env123" },
        })
      );
    });

    test("fetches permissions and license data in parallel", async () => {
      await getEnvironmentLayoutData("env123", "user123");

      expect(getAccessControlPermission).toHaveBeenCalled();
      expect(getProjectPermissionByUserId).toHaveBeenCalledWith("user123", "proj123");
      expect(getEnterpriseLicense).toHaveBeenCalled();
    });

    test("fetches cloud metrics when IS_FORMBRICKS_CLOUD is true", async () => {
      // Mock IS_FORMBRICKS_CLOUD to be true
      const constantsMock = await import("@/lib/constants");
      vi.mocked(constantsMock).IS_FORMBRICKS_CLOUD = true;

      await getEnvironmentLayoutData("env123", "user123");

      expect(getMonthlyOrganizationResponseCount).toHaveBeenCalledWith("org123");
    });

    test("caches results per environmentId and userId", async () => {
      // Call twice with same parameters
      await getEnvironmentLayoutData("env123", "user123");
      await getEnvironmentLayoutData("env123", "user123");

      // Due to React.cache, database should only be queried once
      // Note: React.cache behavior is per-request in production, but in tests
      // we can verify the function was called multiple times
      expect(prisma.environment.findUnique).toHaveBeenCalled();
    });

    test("returns different data for different environmentIds", async () => {
      vi.mocked(prisma.environment.findUnique).mockResolvedValueOnce({
        id: "env123",
        createdAt: new Date(),
        updatedAt: new Date(),
        type: "production",
        projectId: "proj123",
        appSetupCompleted: true,
        project: {
          id: "proj123",
          name: "Project 1",
          organizationId: "org123",
          createdAt: new Date(),
          updatedAt: new Date(),
          languages: ["en"],
          recontactDays: 7,
          linkSurveyBranding: true,
          inAppSurveyBranding: true,
          config: {},
          placement: "bottomRight",
          clickOutsideClose: true,
          overlay: "none",
          styling: {},
          logo: null,
          environments: [],
          organization: {
            id: "org123",
            name: "Org 1",
            createdAt: new Date(),
            updatedAt: new Date(),
            billing: { stripeCustomerId: null, limits: {}, usageCycleAnchor: new Date() },
            isAISmartToolsEnabled: false,
            isAIDataAnalysisEnabled: false,
            whitelabel: false,
            memberships: [{ userId: "user123", organizationId: "org123", role: "owner", accepted: true }],
          },
        },
      } as any);

      const result1 = await getEnvironmentLayoutData("env123", "user123");

      vi.mocked(prisma.environment.findUnique).mockResolvedValueOnce({
        id: "env456",
        createdAt: new Date(),
        updatedAt: new Date(),
        type: "development",
        projectId: "proj456",
        appSetupCompleted: true,
        project: {
          id: "proj456",
          name: "Project 2",
          organizationId: "org456",
          createdAt: new Date(),
          updatedAt: new Date(),
          languages: ["en"],
          recontactDays: 7,
          linkSurveyBranding: true,
          inAppSurveyBranding: true,
          config: {},
          placement: "bottomRight",
          clickOutsideClose: true,
          overlay: "none",
          styling: {},
          logo: null,
          environments: [],
          organization: {
            id: "org456",
            name: "Org 2",
            createdAt: new Date(),
            updatedAt: new Date(),
            billing: { stripeCustomerId: null, limits: {}, usageCycleAnchor: new Date() },
            isAISmartToolsEnabled: true,
            isAIDataAnalysisEnabled: true,
            whitelabel: true,
            memberships: [{ userId: "user123", organizationId: "org456", role: "member", accepted: true }],
          },
        },
      } as any);

      const result2 = await getEnvironmentLayoutData("env456", "user123");

      expect(result1.environment.id).not.toBe(result2.environment.id);
    });
  });
});
