/**
 * @jest-environment node
 */

// Mock @/lib/db before imports
jest.mock("@/lib/db");

// Mock logger to suppress output
jest.mock("@/lib/logger", () => ({
  logger: {
    debug: jest.fn(),
    info: jest.fn(),
    warn: jest.fn(),
    error: jest.fn(),
  },
}));

import { prisma } from "@/lib/db";
import {
  getLanguagePreference,
  setLanguagePreference,
  deleteLanguagePreference,
  getOrganizationLanguagePreferences,
  getLanguageStats,
} from "@/lib/services/whatsapp-language.service";

const mockPrefs = prisma.user_language_preferences as unknown as {
  findUnique: jest.Mock;
  upsert: jest.Mock;
  deleteMany: jest.Mock;
  findMany: jest.Mock;
  count: jest.Mock;
};
const mockOrgs = prisma.organizations as unknown as {
  findUnique: jest.Mock;
};

const ORG_ID = "org_test_123";
const PHONE = "+97412345678";

beforeEach(() => {
  jest.clearAllMocks();
});

// =============================================================================
// getLanguagePreference
// =============================================================================
describe("getLanguagePreference", () => {
  it("returns language when preference exists", async () => {
    mockPrefs.findUnique.mockResolvedValue({ language: "ar" });

    const result = await getLanguagePreference(ORG_ID, PHONE);

    expect(result).toBe("ar");
    expect(mockPrefs.findUnique).toHaveBeenCalledWith({
      where: {
        organizationId_userPhone: {
          organizationId: ORG_ID,
          userPhone: PHONE,
        },
      },
      select: { language: true },
    });
  });

  it("returns null when no preference found", async () => {
    mockPrefs.findUnique.mockResolvedValue(null);

    const result = await getLanguagePreference(ORG_ID, PHONE);

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

  it("returns 'en' when preference is English", async () => {
    mockPrefs.findUnique.mockResolvedValue({ language: "en" });

    const result = await getLanguagePreference(ORG_ID, PHONE);

    expect(result).toBe("en");
  });

  it("throws on database error", async () => {
    mockPrefs.findUnique.mockRejectedValue(new Error("DB connection lost"));

    await expect(getLanguagePreference(ORG_ID, PHONE)).rejects.toThrow(
      "DB connection lost",
    );
  });

  // Phone normalization edge cases (tested through public API)
  it("normalizes phone with spaces: '+971 50 123 4567' → '+971501234567'", async () => {
    mockPrefs.findUnique.mockResolvedValue({ language: "ar" });

    await getLanguagePreference(ORG_ID, "+971 50 123 4567");

    expect(mockPrefs.findUnique).toHaveBeenCalledWith(
      expect.objectContaining({
        where: {
          organizationId_userPhone: {
            organizationId: ORG_ID,
            userPhone: "+971501234567",
          },
        },
      }),
    );
  });

  it("normalizes phone without plus: '971501234567' stays '971501234567'", async () => {
    mockPrefs.findUnique.mockResolvedValue({ language: "en" });

    await getLanguagePreference(ORG_ID, "971501234567");

    expect(mockPrefs.findUnique).toHaveBeenCalledWith(
      expect.objectContaining({
        where: {
          organizationId_userPhone: {
            organizationId: ORG_ID,
            userPhone: "971501234567",
          },
        },
      }),
    );
  });

  it("normalizes phone with dashes: '+971-50-123-4567' → '+971501234567'", async () => {
    mockPrefs.findUnique.mockResolvedValue(null);

    await getLanguagePreference(ORG_ID, "+971-50-123-4567");

    expect(mockPrefs.findUnique).toHaveBeenCalledWith(
      expect.objectContaining({
        where: {
          organizationId_userPhone: {
            organizationId: ORG_ID,
            userPhone: "+971501234567",
          },
        },
      }),
    );
  });

  it("normalizes phone with parentheses: '+1(555)123-4567' → '+15551234567'", async () => {
    mockPrefs.findUnique.mockResolvedValue(null);

    await getLanguagePreference(ORG_ID, "+1(555)123-4567");

    expect(mockPrefs.findUnique).toHaveBeenCalledWith(
      expect.objectContaining({
        where: {
          organizationId_userPhone: {
            organizationId: ORG_ID,
            userPhone: "+15551234567",
          },
        },
      }),
    );
  });
});

// =============================================================================
// setLanguagePreference
// =============================================================================
describe("setLanguagePreference", () => {
  const NOW = new Date("2026-02-19T10:00:00Z");

  beforeEach(() => {
    jest.useFakeTimers();
    jest.setSystemTime(NOW);
  });

  afterEach(() => {
    jest.useRealTimers();
  });

  it("creates preference when org exists", async () => {
    mockOrgs.findUnique.mockResolvedValue({ id: ORG_ID });
    mockPrefs.upsert.mockResolvedValue({
      id: "pref_1",
      organizationId: ORG_ID,
      userPhone: PHONE,
      language: "ar",
      selectedAt: NOW,
      updatedAt: NOW,
    });

    const result = await setLanguagePreference(ORG_ID, PHONE, "ar");

    expect(result).toEqual({
      id: "pref_1",
      organizationId: ORG_ID,
      userId: PHONE, // userPhone mapped to userId
      language: "ar",
      selectedAt: NOW,
    });

    expect(mockOrgs.findUnique).toHaveBeenCalledWith({
      where: { id: ORG_ID },
      select: { id: true },
    });

    expect(mockPrefs.upsert).toHaveBeenCalledWith({
      where: {
        organizationId_userPhone: {
          organizationId: ORG_ID,
          userPhone: PHONE,
        },
      },
      create: {
        organizationId: ORG_ID,
        userPhone: PHONE,
        language: "ar",
        selectedAt: NOW,
        updatedAt: NOW,
      },
      update: {
        language: "ar",
        selectedAt: NOW,
        updatedAt: NOW,
      },
    });
  });

  it("throws when organization not found", async () => {
    mockOrgs.findUnique.mockResolvedValue(null);

    await expect(setLanguagePreference(ORG_ID, PHONE, "en")).rejects.toThrow(
      `Organization not found: ${ORG_ID}`,
    );

    // Should NOT call upsert
    expect(mockPrefs.upsert).not.toHaveBeenCalled();
  });

  it("sets English preference", async () => {
    mockOrgs.findUnique.mockResolvedValue({ id: ORG_ID });
    mockPrefs.upsert.mockResolvedValue({
      id: "pref_2",
      organizationId: ORG_ID,
      userPhone: PHONE,
      language: "en",
      selectedAt: NOW,
      updatedAt: NOW,
    });

    const result = await setLanguagePreference(ORG_ID, PHONE, "en");

    expect(result.language).toBe("en");
  });

  it("normalizes phone with spaces before upserting", async () => {
    mockOrgs.findUnique.mockResolvedValue({ id: ORG_ID });
    mockPrefs.upsert.mockResolvedValue({
      id: "pref_3",
      organizationId: ORG_ID,
      userPhone: "+971501234567",
      language: "ar",
      selectedAt: NOW,
      updatedAt: NOW,
    });

    await setLanguagePreference(ORG_ID, "+971 50 123 4567", "ar");

    expect(mockPrefs.upsert).toHaveBeenCalledWith(
      expect.objectContaining({
        where: {
          organizationId_userPhone: {
            organizationId: ORG_ID,
            userPhone: "+971501234567",
          },
        },
      }),
    );
  });

  it("throws on database error during upsert", async () => {
    mockOrgs.findUnique.mockResolvedValue({ id: ORG_ID });
    mockPrefs.upsert.mockRejectedValue(new Error("Unique constraint failed"));

    await expect(setLanguagePreference(ORG_ID, PHONE, "ar")).rejects.toThrow(
      "Unique constraint failed",
    );
  });

  it("maps userPhone to userId in the returned result", async () => {
    mockOrgs.findUnique.mockResolvedValue({ id: ORG_ID });
    mockPrefs.upsert.mockResolvedValue({
      id: "pref_4",
      organizationId: ORG_ID,
      userPhone: "+97455512345",
      language: "en",
      selectedAt: NOW,
      updatedAt: NOW,
    });

    const result = await setLanguagePreference(ORG_ID, "+97455512345", "en");

    expect(result.userId).toBe("+97455512345");
    expect(result).not.toHaveProperty("userPhone");
  });
});

// =============================================================================
// deleteLanguagePreference
// =============================================================================
describe("deleteLanguagePreference", () => {
  it("returns true when preference deleted", async () => {
    mockPrefs.deleteMany.mockResolvedValue({ count: 1 });

    const result = await deleteLanguagePreference(ORG_ID, PHONE);

    expect(result).toBe(true);
    expect(mockPrefs.deleteMany).toHaveBeenCalledWith({
      where: {
        organizationId: ORG_ID,
        userPhone: PHONE,
      },
    });
  });

  it("returns false when no preference found to delete", async () => {
    mockPrefs.deleteMany.mockResolvedValue({ count: 0 });

    const result = await deleteLanguagePreference(ORG_ID, PHONE);

    expect(result).toBe(false);
  });

  it("normalizes phone with dashes before deleting", async () => {
    mockPrefs.deleteMany.mockResolvedValue({ count: 1 });

    await deleteLanguagePreference(ORG_ID, "+971-50-123-4567");

    expect(mockPrefs.deleteMany).toHaveBeenCalledWith({
      where: {
        organizationId: ORG_ID,
        userPhone: "+971501234567",
      },
    });
  });

  it("throws on database error", async () => {
    mockPrefs.deleteMany.mockRejectedValue(new Error("Connection refused"));

    await expect(deleteLanguagePreference(ORG_ID, PHONE)).rejects.toThrow(
      "Connection refused",
    );
  });
});

// =============================================================================
// getOrganizationLanguagePreferences
// =============================================================================
describe("getOrganizationLanguagePreferences", () => {
  it("returns mapped preferences with default limit", async () => {
    const dbRows = [
      {
        id: "pref_1",
        organizationId: ORG_ID,
        userPhone: "+97412345678",
        language: "ar",
        selectedAt: new Date("2026-02-18"),
      },
      {
        id: "pref_2",
        organizationId: ORG_ID,
        userPhone: "+97487654321",
        language: "en",
        selectedAt: new Date("2026-02-17"),
      },
    ];
    mockPrefs.findMany.mockResolvedValue(dbRows);

    const result = await getOrganizationLanguagePreferences(ORG_ID);

    expect(result).toEqual([
      {
        id: "pref_1",
        organizationId: ORG_ID,
        userId: "+97412345678",
        language: "ar",
        selectedAt: new Date("2026-02-18"),
      },
      {
        id: "pref_2",
        organizationId: ORG_ID,
        userId: "+97487654321",
        language: "en",
        selectedAt: new Date("2026-02-17"),
      },
    ]);

    expect(mockPrefs.findMany).toHaveBeenCalledWith({
      where: { organizationId: ORG_ID },
      orderBy: { selectedAt: "desc" },
      take: 100,
    });
  });

  it("respects custom limit parameter", async () => {
    mockPrefs.findMany.mockResolvedValue([]);

    await getOrganizationLanguagePreferences(ORG_ID, 10);

    expect(mockPrefs.findMany).toHaveBeenCalledWith(
      expect.objectContaining({ take: 10 }),
    );
  });

  it("returns empty array when no preferences exist", async () => {
    mockPrefs.findMany.mockResolvedValue([]);

    const result = await getOrganizationLanguagePreferences(ORG_ID);

    expect(result).toEqual([]);
  });

  it("maps userPhone to userId for each record", async () => {
    mockPrefs.findMany.mockResolvedValue([
      {
        id: "pref_1",
        organizationId: ORG_ID,
        userPhone: "+97400001111",
        language: "ar",
        selectedAt: new Date(),
      },
    ]);

    const result = await getOrganizationLanguagePreferences(ORG_ID);

    expect(result[0].userId).toBe("+97400001111");
    expect(result[0]).not.toHaveProperty("userPhone");
  });

  it("throws on database error", async () => {
    mockPrefs.findMany.mockRejectedValue(new Error("Timeout"));

    await expect(getOrganizationLanguagePreferences(ORG_ID)).rejects.toThrow(
      "Timeout",
    );
  });
});

// =============================================================================
// getLanguageStats
// =============================================================================
describe("getLanguageStats", () => {
  it("returns correct counts for en and ar", async () => {
    mockPrefs.count
      .mockResolvedValueOnce(15) // en count
      .mockResolvedValueOnce(25); // ar count

    const result = await getLanguageStats(ORG_ID);

    expect(result).toEqual({ en: 15, ar: 25, total: 40 });
  });

  it("calls count with correct filters", async () => {
    mockPrefs.count.mockResolvedValueOnce(0).mockResolvedValueOnce(0);

    await getLanguageStats(ORG_ID);

    expect(mockPrefs.count).toHaveBeenCalledTimes(2);
    expect(mockPrefs.count).toHaveBeenCalledWith({
      where: { organizationId: ORG_ID, language: "en" },
    });
    expect(mockPrefs.count).toHaveBeenCalledWith({
      where: { organizationId: ORG_ID, language: "ar" },
    });
  });

  it("returns zeros when no preferences exist", async () => {
    mockPrefs.count.mockResolvedValueOnce(0).mockResolvedValueOnce(0);

    const result = await getLanguageStats(ORG_ID);

    expect(result).toEqual({ en: 0, ar: 0, total: 0 });
  });

  it("handles all-English preferences", async () => {
    mockPrefs.count
      .mockResolvedValueOnce(50) // en
      .mockResolvedValueOnce(0); // ar

    const result = await getLanguageStats(ORG_ID);

    expect(result).toEqual({ en: 50, ar: 0, total: 50 });
  });

  it("handles all-Arabic preferences", async () => {
    mockPrefs.count
      .mockResolvedValueOnce(0) // en
      .mockResolvedValueOnce(30); // ar

    const result = await getLanguageStats(ORG_ID);

    expect(result).toEqual({ en: 0, ar: 30, total: 30 });
  });

  it("throws on database error", async () => {
    mockPrefs.count.mockRejectedValue(new Error("Pool exhausted"));

    await expect(getLanguageStats(ORG_ID)).rejects.toThrow("Pool exhausted");
  });
});
