import { beforeEach, describe, expect, test, vi } from "vitest";
import { prisma } from "@formbricks/database";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
import { getContactAttributeKeys } from "@/modules/ee/contacts/lib/contact-attribute-keys";
import {
  getContactAttributes,
  hasEmailAttribute,
  hasUserIdAttribute,
} from "@/modules/ee/contacts/lib/contact-attributes";
import { updateAttributes } from "./attributes";

vi.mock("@/lib/constants", () => ({
  MAX_ATTRIBUTE_CLASSES_PER_ENVIRONMENT: 2,
}));
vi.mock("@/lib/utils/validate", () => ({
  validateInputs: vi.fn(),
}));
vi.mock("@/modules/ee/contacts/lib/contact-attribute-keys", () => ({
  getContactAttributeKeys: vi.fn(),
}));
vi.mock("@/modules/ee/contacts/lib/contact-attributes", async () => {
  const actual = await vi.importActual("@/modules/ee/contacts/lib/contact-attributes");
  return {
    ...actual,
    getContactAttributes: vi.fn(),
    hasEmailAttribute: vi.fn(),
    hasUserIdAttribute: vi.fn(),
  };
});
vi.mock("@formbricks/database", () => ({
  prisma: {
    $transaction: vi.fn(),
    contactAttribute: { upsert: vi.fn(), findMany: vi.fn(), deleteMany: vi.fn() },
    contactAttributeKey: { create: vi.fn() },
  },
}));

const contactId = "contact-1";
const userId = "user-1";
const environmentId = "env-1";

const attributeKeys: TContactAttributeKey[] = [
  {
    id: "key-1",
    key: "name",
    createdAt: new Date(),
    updatedAt: new Date(),
    isUnique: false,
    name: "Name",
    description: null,
    type: "default",
    dataType: "string",
    environmentId,
  },
  {
    id: "key-2",
    key: "email",
    createdAt: new Date(),
    updatedAt: new Date(),
    isUnique: false,
    name: "Email",
    description: null,
    type: "default",
    dataType: "string",
    environmentId,
  },
  {
    id: "key-3",
    key: "customAttr",
    createdAt: new Date(),
    updatedAt: new Date(),
    isUnique: false,
    name: "Custom Attribute",
    description: null,
    type: "custom",
    dataType: "string",
    environmentId,
  },
];

describe("updateAttributes", () => {
  beforeEach(() => {
    vi.clearAllMocks();
    // Set default mock return values - these will be overridden in individual tests
    vi.mocked(getContactAttributes).mockResolvedValue({});
    vi.mocked(hasUserIdAttribute).mockResolvedValue(false);
    vi.mocked(prisma.contactAttribute.deleteMany).mockResolvedValue({ count: 0 });
    vi.mocked(prisma.$transaction).mockResolvedValue(undefined);
  });

  test("updates existing attributes", async () => {
    vi.mocked(getContactAttributeKeys).mockResolvedValue(attributeKeys);
    vi.mocked(getContactAttributes).mockResolvedValue({ name: "Jane", email: "jane@example.com" });
    vi.mocked(hasEmailAttribute).mockResolvedValue(false);
    vi.mocked(hasUserIdAttribute).mockResolvedValue(false);
    vi.mocked(prisma.$transaction).mockResolvedValue(undefined);
    vi.mocked(prisma.contactAttribute.deleteMany).mockResolvedValue({ count: 0 });
    const attributes = { name: "John", email: "john@example.com" };
    const result = await updateAttributes(contactId, userId, environmentId, attributes);
    expect(prisma.$transaction).toHaveBeenCalled();
    expect(result.success).toBe(true);
    expect(result.messages).toBeUndefined();
  });

  test("skips updating email if it already exists", async () => {
    vi.mocked(getContactAttributeKeys).mockResolvedValue(attributeKeys);
    vi.mocked(getContactAttributes).mockResolvedValue({ name: "Jane", email: "jane@example.com" });
    vi.mocked(hasEmailAttribute).mockResolvedValue(true);
    vi.mocked(hasUserIdAttribute).mockResolvedValue(false);
    vi.mocked(prisma.$transaction).mockResolvedValue(undefined);
    vi.mocked(prisma.contactAttribute.deleteMany).mockResolvedValue({ count: 0 });
    const attributes = { name: "John", email: "john@example.com" };
    const result = await updateAttributes(contactId, userId, environmentId, attributes);
    expect(prisma.$transaction).toHaveBeenCalled();

    expect(result.success).toBe(true);
    expect(result.messages).toContainEqual({ code: "email_already_exists", params: {} });
  });

  test("skips updating userId if it already exists", async () => {
    const attributeKeysWithUserId: TContactAttributeKey[] = [
      ...attributeKeys,
      {
        id: "key-4",
        key: "userId",
        createdAt: new Date(),
        updatedAt: new Date(),
        isUnique: true,
        name: "User ID",
        description: null,
        type: "default",
        dataType: "string",
        environmentId,
      },
    ];
    vi.mocked(getContactAttributeKeys).mockResolvedValue(attributeKeysWithUserId);
    vi.mocked(getContactAttributes).mockResolvedValue({ name: "Jane", userId: "old-user-id" });
    vi.mocked(hasEmailAttribute).mockResolvedValue(false);
    vi.mocked(hasUserIdAttribute).mockResolvedValue(true);
    vi.mocked(prisma.$transaction).mockResolvedValue(undefined);
    vi.mocked(prisma.contactAttribute.deleteMany).mockResolvedValue({ count: 0 });
    const attributes = { name: "John", userId: "duplicate-user-id" };
    const result = await updateAttributes(contactId, userId, environmentId, attributes);
    expect(prisma.$transaction).toHaveBeenCalled();

    expect(result.success).toBe(true);
    expect(result.messages).toContainEqual({ code: "userid_already_exists", params: {} });
    expect(result.ignoreUserIdAttribute).toBe(true);
  });

  test("skips updating both email and userId if both already exist", async () => {
    const attributeKeysWithUserId: TContactAttributeKey[] = [
      ...attributeKeys,
      {
        id: "key-4",
        key: "userId",
        createdAt: new Date(),
        updatedAt: new Date(),
        isUnique: true,
        name: "User ID",
        description: null,
        type: "default",
        dataType: "string",
        environmentId,
      },
    ];
    vi.mocked(getContactAttributeKeys).mockResolvedValue(attributeKeysWithUserId);
    vi.mocked(getContactAttributes).mockResolvedValue({
      name: "Jane",
      email: "old@example.com",
      userId: "old-user-id",
    });
    vi.mocked(hasEmailAttribute).mockResolvedValue(true);
    vi.mocked(hasUserIdAttribute).mockResolvedValue(true);
    vi.mocked(prisma.$transaction).mockResolvedValue(undefined);
    vi.mocked(prisma.contactAttribute.deleteMany).mockResolvedValue({ count: 0 });
    const attributes = { name: "John", email: "duplicate@example.com", userId: "duplicate-user-id" };
    const result = await updateAttributes(contactId, userId, environmentId, attributes);
    expect(prisma.$transaction).toHaveBeenCalled();

    expect(result.success).toBe(true);
    expect(result.messages).toContainEqual({ code: "email_already_exists", params: {} });
    expect(result.messages).toContainEqual({ code: "userid_already_exists", params: {} });
    expect(result.ignoreEmailAttribute).toBe(true);
    expect(result.ignoreUserIdAttribute).toBe(true);
  });

  test("creates new attributes if under limit", async () => {
    // Use name and email keys (2 existing keys), MAX is mocked to 2
    // We update existing attributes, no new ones created
    vi.mocked(getContactAttributeKeys).mockResolvedValue([attributeKeys[0], attributeKeys[1]]); // name, email
    vi.mocked(getContactAttributes).mockResolvedValue({ name: "Jane", email: "jane@example.com" });
    vi.mocked(hasEmailAttribute).mockResolvedValue(false);
    vi.mocked(hasUserIdAttribute).mockResolvedValue(false);
    vi.mocked(prisma.$transaction).mockResolvedValue(undefined);
    vi.mocked(prisma.contactAttribute.deleteMany).mockResolvedValue({ count: 0 });
    const attributes = { name: "John", email: "john@example.com" };
    const result = await updateAttributes(contactId, userId, environmentId, attributes);
    expect(prisma.$transaction).toHaveBeenCalled();

    expect(result.success).toBe(true);
    expect(result.messages).toBeUndefined();
  });

  test("does not create new attributes if over the limit", async () => {
    vi.mocked(getContactAttributeKeys).mockResolvedValue(attributeKeys);
    vi.mocked(getContactAttributes).mockResolvedValue({ name: "Jane", email: "jane@example.com" });
    vi.mocked(hasEmailAttribute).mockResolvedValue(false);
    vi.mocked(hasUserIdAttribute).mockResolvedValue(false);
    vi.mocked(prisma.$transaction).mockResolvedValue(undefined);
    vi.mocked(prisma.contactAttribute.deleteMany).mockResolvedValue({ count: 0 });
    // Include email to satisfy the "at least one of email or userId" requirement
    const attributes = { name: "John", email: "john@example.com", new_attr: "val" };
    const result = await updateAttributes(contactId, userId, environmentId, attributes);
    expect(result.success).toBe(true);
    expect(result.messages?.[0]).toEqual(
      expect.objectContaining({
        code: "attribute_limit_exceeded",
        params: expect.objectContaining({ count: "1" }),
      })
    );
  });

  test("returns success with only email attribute", async () => {
    vi.mocked(getContactAttributeKeys).mockResolvedValue([attributeKeys[1]]); // email key
    vi.mocked(getContactAttributes).mockResolvedValue({ email: "existing@example.com" });
    vi.mocked(hasEmailAttribute).mockResolvedValue(false);
    vi.mocked(hasUserIdAttribute).mockResolvedValue(false);
    vi.mocked(prisma.$transaction).mockResolvedValue(undefined);
    vi.mocked(prisma.contactAttribute.deleteMany).mockResolvedValue({ count: 0 });
    const attributes = { email: "updated@example.com" };
    const result = await updateAttributes(contactId, userId, environmentId, attributes);
    expect(result.success).toBe(true);
    expect(result.messages).toBeUndefined();
  });

  test("deletes non-default attributes when deleteRemovedAttributes is true", async () => {
    // Reset mocks explicitly for this test
    vi.mocked(prisma.contactAttribute.deleteMany).mockClear();

    vi.mocked(getContactAttributeKeys).mockResolvedValue(attributeKeys);
    vi.mocked(getContactAttributes).mockResolvedValue({
      name: "Jane",
      email: "jane@example.com",
      customAttr: "oldValue",
    });
    vi.mocked(hasEmailAttribute).mockResolvedValue(false);
    vi.mocked(hasUserIdAttribute).mockResolvedValue(false);
    vi.mocked(prisma.$transaction).mockResolvedValue(undefined);
    vi.mocked(prisma.contactAttribute.deleteMany).mockResolvedValue({ count: 1 });
    const attributes = { name: "John", email: "john@example.com" };
    // Pass deleteRemovedAttributes: true to enable deletion behavior
    const result = await updateAttributes(contactId, userId, environmentId, attributes, true);
    // Only customAttr (key-3) should be deleted, not name (key-1) or email (key-2)
    expect(prisma.contactAttribute.deleteMany).toHaveBeenCalledWith({
      where: {
        contactId,
        attributeKeyId: {
          in: ["key-3"],
        },
      },
    });
    expect(result.success).toBe(true);
    expect(result.messages).toBeUndefined();
  });

  test("does not delete attributes when deleteRemovedAttributes is false (default behavior)", async () => {
    // Reset mocks explicitly for this test
    vi.mocked(prisma.contactAttribute.deleteMany).mockClear();

    vi.mocked(getContactAttributeKeys).mockResolvedValue(attributeKeys);
    vi.mocked(getContactAttributes).mockResolvedValue({
      name: "Jane",
      email: "jane@example.com",
      customAttr: "oldValue",
    });
    vi.mocked(hasEmailAttribute).mockResolvedValue(false);
    vi.mocked(prisma.$transaction).mockResolvedValue(undefined);
    const attributes = { name: "John", email: "john@example.com" };
    // Default behavior (deleteRemovedAttributes: false) should NOT delete existing attributes
    const result = await updateAttributes(contactId, userId, environmentId, attributes);
    // deleteMany should NOT be called since we're merging, not replacing
    expect(prisma.contactAttribute.deleteMany).not.toHaveBeenCalled();
    expect(result.success).toBe(true);
    expect(result.messages).toBeUndefined();
  });

  test("does not delete default attributes even when deleteRemovedAttributes is true", async () => {
    // Reset mocks explicitly for this test
    vi.mocked(prisma.contactAttribute.deleteMany).mockClear();

    // Note: DEFAULT_ATTRIBUTES includes: email, userId, firstName, lastName (not "name")
    const attributeKeysWithDefaults: TContactAttributeKey[] = [
      {
        id: "key-2",
        key: "email",
        createdAt: new Date(),
        updatedAt: new Date(),
        isUnique: false,
        name: "Email",
        description: null,
        type: "default",
        dataType: "string",
        environmentId,
      },
      {
        id: "key-4",
        key: "userId",
        createdAt: new Date(),
        updatedAt: new Date(),
        isUnique: false,
        name: "User ID",
        description: null,
        type: "default",
        dataType: "string",
        environmentId,
      },
      {
        id: "key-5",
        key: "firstName",
        createdAt: new Date(),
        updatedAt: new Date(),
        isUnique: false,
        name: "First Name",
        description: null,
        type: "default",
        dataType: "string",
        environmentId,
      },
      {
        id: "key-3",
        key: "customAttr",
        createdAt: new Date(),
        updatedAt: new Date(),
        isUnique: false,
        name: "Custom Attribute",
        description: null,
        type: "custom",
        dataType: "string",
        environmentId,
      },
    ];
    vi.mocked(getContactAttributeKeys).mockResolvedValue(attributeKeysWithDefaults);
    vi.mocked(getContactAttributes).mockResolvedValue({
      email: "test@example.com",
      userId: "user-123",
      firstName: "John",
    });
    vi.mocked(hasEmailAttribute).mockResolvedValue(false);
    vi.mocked(hasUserIdAttribute).mockResolvedValue(false);
    vi.mocked(prisma.$transaction).mockResolvedValue(undefined);
    vi.mocked(prisma.contactAttribute.deleteMany).mockResolvedValue({ count: 0 });
    const attributes = { customAttr: "value" };
    // Pass deleteRemovedAttributes: true to test that default attributes are still preserved
    const result = await updateAttributes(contactId, userId, environmentId, attributes, true);
    // Should not delete default attributes (email, userId, firstName) - deleteMany should not be called
    // since all current attributes are default attributes
    expect(prisma.contactAttribute.deleteMany).not.toHaveBeenCalled();
    expect(result.success).toBe(true);
  });

  test("preserves existing email when empty string is submitted", async () => {
    vi.mocked(getContactAttributeKeys).mockResolvedValue(attributeKeys);
    vi.mocked(getContactAttributes).mockResolvedValue({ name: "Jane", email: "existing@example.com" });
    vi.mocked(hasEmailAttribute).mockResolvedValue(false);
    vi.mocked(hasUserIdAttribute).mockResolvedValue(false);
    vi.mocked(prisma.$transaction).mockResolvedValue(undefined);
    vi.mocked(prisma.contactAttribute.deleteMany).mockResolvedValue({ count: 0 });

    // Attempt to clear email by submitting empty string
    const attributes = { name: "John", email: "" };
    const result = await updateAttributes(contactId, userId, environmentId, attributes);

    // Verify that the transaction was called with the preserved email
    expect(prisma.$transaction).toHaveBeenCalled();
    const transactionCall = vi.mocked(prisma.$transaction).mock.calls[0][0];
    // The email should be preserved (existing@example.com), not cleared
    expect(transactionCall).toHaveLength(2); // name and email
    expect(result.success).toBe(true);
  });

  test("allows clearing userId when empty string is submitted", async () => {
    const attributeKeysWithUserId: TContactAttributeKey[] = [
      ...attributeKeys,
      {
        id: "key-4",
        key: "userId",
        createdAt: new Date(),
        updatedAt: new Date(),
        isUnique: true,
        name: "User ID",
        description: null,
        type: "default",
        dataType: "string",
        environmentId,
      },
    ];
    vi.mocked(getContactAttributeKeys).mockResolvedValue(attributeKeysWithUserId);
    vi.mocked(getContactAttributes).mockResolvedValue({ name: "Jane", userId: "existing-user-id" });
    vi.mocked(hasEmailAttribute).mockResolvedValue(false);
    vi.mocked(hasUserIdAttribute).mockResolvedValue(false);
    vi.mocked(prisma.$transaction).mockResolvedValue(undefined);
    vi.mocked(prisma.contactAttribute.deleteMany).mockResolvedValue({ count: 0 });

    // Clear userId by submitting empty string - this should be allowed
    const attributes = { name: "John", userId: "" };
    const result = await updateAttributes(contactId, userId, environmentId, attributes);

    // Verify that the transaction was called
    expect(prisma.$transaction).toHaveBeenCalled();
    const transactionCall = vi.mocked(prisma.$transaction).mock.calls[0][0];
    // Only name and userId (empty) should be in the transaction
    expect(transactionCall).toHaveLength(2); // name and userId (with empty value)
    expect(result.success).toBe(true);
  });

  test("preserves existing values when both email and userId would be cleared", async () => {
    const attributeKeysWithBoth: TContactAttributeKey[] = [
      ...attributeKeys,
      {
        id: "key-4",
        key: "userId",
        createdAt: new Date(),
        updatedAt: new Date(),
        isUnique: true,
        name: "User ID",
        description: null,
        type: "default",
        dataType: "string",
        environmentId,
      },
    ];
    vi.mocked(getContactAttributeKeys).mockResolvedValue(attributeKeysWithBoth);
    vi.mocked(getContactAttributes).mockResolvedValue({
      name: "Jane",
      email: "existing@example.com",
      userId: "existing-user-id",
    });
    vi.mocked(hasEmailAttribute).mockResolvedValue(false);
    vi.mocked(hasUserIdAttribute).mockResolvedValue(false);
    vi.mocked(prisma.$transaction).mockResolvedValue(undefined);
    vi.mocked(prisma.contactAttribute.deleteMany).mockResolvedValue({ count: 0 });

    // Attempt to clear both email and userId
    const attributes = { name: "John", email: "", userId: "" };
    const result = await updateAttributes(contactId, userId, environmentId, attributes);

    expect(result.success).toBe(true);
    expect(result.messages).toContainEqual({ code: "email_or_userid_required", params: {} });
  });

  test("coerces boolean attribute values to strings", async () => {
    vi.mocked(getContactAttributeKeys).mockResolvedValue(attributeKeys);
    vi.mocked(getContactAttributes).mockResolvedValue({ name: "Jane", email: "jane@example.com" });
    vi.mocked(hasEmailAttribute).mockResolvedValue(false);
    vi.mocked(hasUserIdAttribute).mockResolvedValue(false);
    vi.mocked(prisma.$transaction).mockResolvedValue(undefined);
    vi.mocked(prisma.contactAttribute.deleteMany).mockResolvedValue({ count: 0 });

    const attributes = { name: true, email: "john@example.com" };
    const result = await updateAttributes(contactId, userId, environmentId, attributes);

    expect(result.success).toBe(true);
    expect(prisma.$transaction).toHaveBeenCalled();
    const transactionCall = vi.mocked(prisma.$transaction).mock.calls[0][0];
    // Both name (coerced from boolean) and email should be upserted
    expect(transactionCall).toHaveLength(2);
  });
});
