/**
 * Tests for Review Auto-Trigger Service
 */

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

jest.mock("@/lib/queue/email-queue", () => ({
  queueEmail: jest.fn(),
}));

jest.mock("@/lib/logger", () => ({
  logger: {
    info: jest.fn(),
    warn: jest.fn(),
    error: jest.fn(),
    debug: jest.fn(),
    scope: jest.fn().mockReturnValue({
      info: jest.fn(),
      warn: jest.fn(),
      error: jest.fn(),
      debug: jest.fn(),
    }),
  },
}));

jest.mock("@paralleldrive/cuid2", () => ({
  createId: jest.fn().mockReturnValue("mock-request-id"),
}));

jest.mock("crypto", () => ({
  ...jest.requireActual("crypto"),
  randomBytes: jest.fn().mockReturnValue({
    toString: jest.fn().mockReturnValue("mock-token-hex"),
  }),
}));

jest.mock("@/lib/services/twilio-whatsapp.service", () => ({
  twilioWhatsAppService: {
    getCredentialsForOrg: jest.fn(),
    sendTextMessage: jest.fn(),
  },
}));

import { prisma } from "@/lib/db";
import { queueEmail } from "@/lib/queue/email-queue";
import {
  processAutoReviewRequests,
  triggerReviewForBooking,
} from "@/lib/services/review-auto-trigger.service";
import { twilioWhatsAppService } from "@/lib/services/twilio-whatsapp.service";

const mockPrisma = prisma as jest.Mocked<typeof prisma>;
const mockQueueEmail = queueEmail as jest.MockedFunction<typeof queueEmail>;
const mockTwilio = twilioWhatsAppService as jest.Mocked<
  typeof twilioWhatsAppService
>;

function makeBooking(overrides: Record<string, unknown> = {}) {
  return {
    id: "booking-1",
    userId: "user-1",
    organizationId: "org-1",
    customerName: "Test Customer",
    customerPhone: "+97412345678",
    customerEmail: "test@example.com",
    service: "Consultation",
    users: { industry: "healthcare", language: "en" },
    ...overrides,
  };
}

describe("processAutoReviewRequests", () => {
  beforeEach(() => {
    jest.clearAllMocks();
    process.env.NEXT_PUBLIC_APP_URL = "https://test.mawidi.com";
    // Default: no customer found, no question template
    (mockPrisma.customers.findFirst as jest.Mock).mockResolvedValue(null);
    (
      mockPrisma.review_question_templates.findUnique as jest.Mock
    ).mockResolvedValue(null);
    (mockPrisma.review_requests.create as jest.Mock).mockResolvedValue({});
    (mockPrisma.review_requests.update as jest.Mock).mockResolvedValue({});
    mockQueueEmail.mockResolvedValue(undefined);
    (mockTwilio.getCredentialsForOrg as jest.Mock).mockResolvedValue(null);
  });

  it("creates review requests for completed bookings 23-25h ago", async () => {
    (mockPrisma.bookings.findMany as jest.Mock).mockResolvedValueOnce([
      makeBooking(),
    ]);

    const result = await processAutoReviewRequests();

    expect(result.processed).toBe(1);
    expect(result.errors).toBe(0);
    expect(mockPrisma.review_requests.create).toHaveBeenCalledWith(
      expect.objectContaining({
        data: expect.objectContaining({
          userId: "user-1",
          customerName: "Test Customer",
          token: "mock-token-hex",
          status: "SENT",
          source: "auto_booking",
          reminderCount: 0,
        }),
      }),
    );
  });

  it("queues email when customer has email", async () => {
    (mockPrisma.bookings.findMany as jest.Mock).mockResolvedValueOnce([
      makeBooking(),
    ]);

    await processAutoReviewRequests();

    expect(mockQueueEmail).toHaveBeenCalledWith(
      expect.objectContaining({
        to: "test@example.com",
        category: "review_request",
      }),
    );
  });

  it("attempts WhatsApp when customer has phone", async () => {
    (mockPrisma.bookings.findMany as jest.Mock).mockResolvedValueOnce([
      makeBooking(),
    ]);
    (mockTwilio.getCredentialsForOrg as jest.Mock).mockResolvedValueOnce({
      accountSid: "AC123",
      authToken: "tok",
      fromNumber: "+1234",
    });
    (mockTwilio.sendTextMessage as jest.Mock).mockResolvedValueOnce({
      success: true,
    });

    const result = await processAutoReviewRequests();

    expect(result.processed).toBe(1);
    expect(mockTwilio.sendTextMessage).toHaveBeenCalled();
  });

  it("handles per-item errors without failing the batch", async () => {
    (mockPrisma.bookings.findMany as jest.Mock).mockResolvedValueOnce([
      makeBooking({ id: "booking-ok" }),
      makeBooking({ id: "booking-fail" }),
    ]);

    // First call OK, second call throws
    (mockPrisma.review_requests.create as jest.Mock)
      .mockResolvedValueOnce({})
      .mockRejectedValueOnce(new Error("DB error"));

    const result = await processAutoReviewRequests();
    expect(result.processed).toBe(1);
    expect(result.errors).toBe(1);
  });

  it("returns correct counts when no eligible bookings", async () => {
    (mockPrisma.bookings.findMany as jest.Mock).mockResolvedValueOnce([]);

    const result = await processAutoReviewRequests();
    expect(result.processed).toBe(0);
    expect(result.skipped).toBe(0);
    expect(result.errors).toBe(0);
  });

  it("handles booking with no customer match gracefully", async () => {
    (mockPrisma.bookings.findMany as jest.Mock).mockResolvedValueOnce([
      makeBooking(),
    ]);
    (mockPrisma.customers.findFirst as jest.Mock).mockResolvedValueOnce(null);

    const result = await processAutoReviewRequests();
    expect(result.processed).toBe(1);
    expect(mockPrisma.review_requests.create).toHaveBeenCalledWith(
      expect.objectContaining({
        data: expect.objectContaining({ customerId: null }),
      }),
    );
  });

  it("sets customer ID when customer found", async () => {
    (mockPrisma.bookings.findMany as jest.Mock).mockResolvedValueOnce([
      makeBooking(),
    ]);
    (mockPrisma.customers.findFirst as jest.Mock).mockResolvedValueOnce({
      id: "cust-1",
    });

    await processAutoReviewRequests();
    expect(mockPrisma.review_requests.create).toHaveBeenCalledWith(
      expect.objectContaining({
        data: expect.objectContaining({ customerId: "cust-1" }),
      }),
    );
  });

  it("processes when email fails but WhatsApp succeeds", async () => {
    (mockPrisma.bookings.findMany as jest.Mock).mockResolvedValueOnce([
      makeBooking(),
    ]);
    mockQueueEmail.mockRejectedValueOnce(new Error("SMTP error"));
    (mockTwilio.getCredentialsForOrg as jest.Mock).mockResolvedValueOnce({
      accountSid: "AC",
      authToken: "t",
      fromNumber: "+1",
    });
    (mockTwilio.sendTextMessage as jest.Mock).mockResolvedValueOnce({
      success: true,
    });

    const result = await processAutoReviewRequests();
    expect(result.processed).toBe(1);
    // Should update with SENT status since WhatsApp succeeded
    expect(mockPrisma.review_requests.update).toHaveBeenCalledWith(
      expect.objectContaining({
        data: expect.objectContaining({ reminderStatus: "SENT" }),
      }),
    );
  });

  it("marks reminder status as FAILED when both channels fail", async () => {
    (mockPrisma.bookings.findMany as jest.Mock).mockResolvedValueOnce([
      makeBooking({
        customerEmail: "x@y.com",
        customerPhone: null,
        organizationId: null,
      }),
    ]);
    mockQueueEmail.mockRejectedValueOnce(new Error("SMTP error"));

    await processAutoReviewRequests();
    expect(mockPrisma.review_requests.update).toHaveBeenCalledWith(
      expect.objectContaining({
        data: expect.objectContaining({ reminderStatus: "FAILED" }),
      }),
    );
  });
});

// =============================================================================
// triggerReviewForBooking — immediate send when appointment is marked completed
// =============================================================================

jest.mock("@/lib/email/review-request-email", () => ({
  buildReviewRequestEmailHtml: jest.fn().mockReturnValue({
    subject: "We'd love your feedback!",
    html: "<html>test</html>",
    text: "test plain",
  }),
}));

const baseBooking = {
  id: "booking-1",
  userId: "user-1",
  organizationId: "org-1",
  customerName: "Alice",
  customerPhone: "+97412345678",
  customerEmail: "alice@test.com",
  service: "Check-up",
};

describe("triggerReviewForBooking", () => {
  beforeEach(() => {
    jest.clearAllMocks();
    (
      mockPrisma.review_question_templates.findUnique as jest.Mock
    ).mockResolvedValue(null);
    (mockPrisma.review_requests.findFirst as jest.Mock).mockResolvedValue(null);
    (mockPrisma.review_requests.create as jest.Mock).mockResolvedValue({
      id: "mock-request-id",
    });
    (mockPrisma.review_requests.update as jest.Mock).mockResolvedValue({});
    (
      mockPrisma.review_widget_settings.findUnique as jest.Mock
    ).mockResolvedValue(null);
    (mockPrisma.users.findUnique as jest.Mock).mockResolvedValue({
      industry: "healthcare",
    });
    mockQueueEmail.mockResolvedValue(undefined);
  });

  it("returns skipped=true when booking not found", async () => {
    (mockPrisma.bookings.findFirst as jest.Mock).mockResolvedValue(null);
    const result = await triggerReviewForBooking("missing", "user-1");
    expect(result.sent).toBe(false);
    expect(result.skipped).toBe(true);
    expect(result.reason).toBe("booking not found");
  });

  it("returns skipped=true when no contact info", async () => {
    (mockPrisma.bookings.findFirst as jest.Mock).mockResolvedValue({
      ...baseBooking,
      customerEmail: null,
      customerPhone: null,
    });
    const result = await triggerReviewForBooking("booking-1", "user-1");
    expect(result.sent).toBe(false);
    expect(result.skipped).toBe(true);
    expect(result.reason).toBe("no contact info");
  });

  it("returns skipped=true when review request already exists", async () => {
    (mockPrisma.bookings.findFirst as jest.Mock).mockResolvedValue(baseBooking);
    (mockPrisma.review_requests.findFirst as jest.Mock).mockResolvedValue({
      id: "existing-req",
    });
    const result = await triggerReviewForBooking("booking-1", "user-1");
    expect(result.sent).toBe(false);
    expect(result.skipped).toBe(true);
    expect(result.reason).toBe("already requested");
  });

  it("creates review request and queues email when email available", async () => {
    (mockPrisma.bookings.findFirst as jest.Mock).mockResolvedValue(baseBooking);
    const result = await triggerReviewForBooking("booking-1", "user-1");
    expect(result.sent).toBe(true);
    expect(result.emailSent).toBe(true);
    expect(mockPrisma.review_requests.create).toHaveBeenCalledWith(
      expect.objectContaining({
        data: expect.objectContaining({
          userId: "user-1",
          customerName: "Alice",
          customerEmail: "alice@test.com",
          source: "auto_booking",
          status: "SENT",
          bookingId: "booking-1",
        }),
      }),
    );
    expect(mockQueueEmail).toHaveBeenCalledWith(
      expect.objectContaining({
        to: "alice@test.com",
        category: "review_request",
      }),
    );
  });

  it("marks reminderStatus=SENT when email queued", async () => {
    (mockPrisma.bookings.findFirst as jest.Mock).mockResolvedValue(baseBooking);
    await triggerReviewForBooking("booking-1", "user-1");
    expect(mockPrisma.review_requests.update).toHaveBeenCalledWith(
      expect.objectContaining({
        data: expect.objectContaining({ reminderStatus: "SENT" }),
      }),
    );
  });

  it("marks reminderStatus=FAILED when all channels fail", async () => {
    (mockPrisma.bookings.findFirst as jest.Mock).mockResolvedValue({
      ...baseBooking,
      customerEmail: "x@y.com",
      customerPhone: null,
      organizationId: null,
    });
    mockQueueEmail.mockRejectedValueOnce(new Error("SMTP down"));
    await triggerReviewForBooking("booking-1", "user-1");
    expect(mockPrisma.review_requests.update).toHaveBeenCalledWith(
      expect.objectContaining({
        data: expect.objectContaining({ reminderStatus: "FAILED" }),
      }),
    );
  });

  it("uses businessName from widget settings in email", async () => {
    (mockPrisma.bookings.findFirst as jest.Mock).mockResolvedValue(baseBooking);
    (
      mockPrisma.review_widget_settings.findUnique as jest.Mock
    ).mockResolvedValue({
      businessName: "My Clinic",
      businessNameAr: "عيادتي",
    });
    const { buildReviewRequestEmailHtml } = jest.requireMock(
      "@/lib/email/review-request-email",
    );
    await triggerReviewForBooking("booking-1", "user-1");
    expect(buildReviewRequestEmailHtml).toHaveBeenCalledWith(
      expect.objectContaining({ businessName: "My Clinic" }),
    );
  });

  it("still returns sent=true even when email silently fails", async () => {
    (mockPrisma.bookings.findFirst as jest.Mock).mockResolvedValue({
      ...baseBooking,
      customerPhone: null,
    });
    mockQueueEmail.mockRejectedValueOnce(new Error("SMTP error"));
    const result = await triggerReviewForBooking("booking-1", "user-1");
    // sent=false because emailSent=false and whatsAppSent=false
    expect(result.sent).toBe(true); // record was created
    expect(result.emailSent).toBe(false);
  });

  it("returns sent=false but does not throw on unexpected error", async () => {
    (mockPrisma.bookings.findFirst as jest.Mock).mockRejectedValue(
      new Error("DB down"),
    );
    const result = await triggerReviewForBooking("booking-1", "user-1");
    expect(result.sent).toBe(false);
    expect(result.emailSent).toBe(false);
  });
});
