import type { CalendarSubscriptionEventItem } from "@calcom/features/calendar-subscription/lib/CalendarSubscriptionPort.interface";
import type { BookingRepository } from "@calcom/lib/server/repository/booking";
import type { SelectedCalendar } from "@calcom/prisma/client";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { CalendarSyncService } from "../CalendarSyncService";

const { mockHandleCancelBooking, mockCreateBooking } = vi.hoisted(() => ({
  mockHandleCancelBooking: vi.fn().mockResolvedValue(undefined),
  mockCreateBooking: vi.fn().mockResolvedValue(undefined),
}));

vi.mock("@calcom/features/bookings/lib/handleCancelBooking", () => ({
  default: mockHandleCancelBooking,
}));

vi.mock("@calcom/features/bookings/di/RegularBookingService.container", () => ({
  getRegularBookingService: () => ({
    createBooking: mockCreateBooking,
  }),
}));

vi.mock("@calcom/lib/idempotencyKey/idempotencyKeyService", () => ({
  IdempotencyKeyService: {
    generate: vi.fn(() => "test-idempotency-key"),
  },
}));

vi.mock("@sentry/nextjs", () => ({
  metrics: {
    count: vi.fn(),
    distribution: vi.fn(),
  },
}));

const mockSelectedCalendar: SelectedCalendar = {
  id: "test-calendar-id",
  userId: 1,
  credentialId: 1,
  integration: "google_calendar",
  externalId: "test@example.com",
  eventTypeId: null,
  delegationCredentialId: null,
  googleChannelId: null,
  googleChannelKind: null,
  googleChannelResourceId: null,
  googleChannelResourceUri: null,
  googleChannelExpiration: null,
  error: null,
  lastErrorAt: null,
  watchAttempts: 0,
  maxAttempts: 3,
  unwatchAttempts: 0,
  createdAt: new Date(),
  updatedAt: new Date(),
  channelId: "test-channel-id",
  channelKind: "web_hook",
  channelResourceId: "test-resource-id",
  channelResourceUri: "test-resource-uri",
  channelExpiration: new Date(Date.now() + 86400000),
  syncSubscribedAt: new Date(),
  syncToken: "test-sync-token",
  syncedAt: new Date(),
  syncErrorAt: null,
  syncErrorCount: 0,
};

const mockBooking = {
  id: 1,
  uid: "test-booking-uid",
  userId: 1,
  userPrimaryEmail: "user@example.com",
  startTime: new Date("2023-12-01T10:00:00Z"),
  endTime: new Date("2023-12-01T11:00:00Z"),
  title: "Test Booking",
  description: "Initial notes",
  location: "Test Location",
  smsReminderNumber: "+123456789",
  responses: {
    name: "John Doe",
    email: "john@example.com",
    notes: "Initial notes",
    location: {
      value: "Test Location",
      label: "Test Location",
      optionValue: "Test Location",
    },
  },
  eventTypeId: 1,
  eventType: {
    id: 1,
    title: "Test Event Type",
  },
};

const mockCalComEvent: CalendarSubscriptionEventItem = {
  id: "event-1",
  iCalUID: "test-booking-uid@cal.com",
  start: new Date("2023-12-01T10:00:00Z"),
  end: new Date("2023-12-01T11:00:00Z"),
  busy: true,
  summary: "Test Event",
  description: "Test Description",
  location: "Test Location",
  status: "confirmed",
  isAllDay: false,
  timeZone: "UTC",
  recurringEventId: null,
  originalStartDate: null,
  createdAt: new Date(),
  updatedAt: new Date(),
  etag: "test-etag",
  kind: "calendar#event",
};

const mockNonCalComEvent: CalendarSubscriptionEventItem = {
  id: "event-2",
  iCalUID: "external-event@external.com",
  start: new Date("2023-12-01T12:00:00Z"),
  end: new Date("2023-12-01T13:00:00Z"),
  busy: true,
  summary: "External Event",
  description: "External Description",
  location: "External Location",
  status: "confirmed",
  isAllDay: false,
  timeZone: "UTC",
  recurringEventId: null,
  originalStartDate: null,
  createdAt: new Date(),
  updatedAt: new Date(),
  etag: "test-etag",
  kind: "calendar#event",
};

const mockCancelledEvent: CalendarSubscriptionEventItem = {
  ...mockCalComEvent,
  id: "event-3",
  iCalUID: "cancelled-booking-uid@cal.com",
  status: "cancelled",
};

describe("CalendarSyncService", () => {
  let service: CalendarSyncService;
  let mockBookingRepository: BookingRepository;

  beforeEach(() => {
    mockBookingRepository = {
      findBookingByUidWithEventType: vi.fn(),
    } as unknown as BookingRepository;

    service = new CalendarSyncService({
      bookingRepository: mockBookingRepository,
    });

    vi.clearAllMocks();
  });

  describe("handleEvents", () => {
    test("should process only Cal.diy events", async () => {
      const events = [mockCalComEvent, mockNonCalComEvent, mockCancelledEvent];

      mockBookingRepository.findBookingByUidWithEventType = vi
        .fn()
        .mockResolvedValueOnce(mockBooking)
        .mockResolvedValueOnce(mockBooking);

      await service.handleEvents(mockSelectedCalendar, events);

      expect(mockBookingRepository.findBookingByUidWithEventType).toHaveBeenCalledTimes(2);
      expect(mockBookingRepository.findBookingByUidWithEventType).toHaveBeenCalledWith({
        bookingUid: "test-booking-uid",
      });
      expect(mockBookingRepository.findBookingByUidWithEventType).toHaveBeenCalledWith({
        bookingUid: "cancelled-booking-uid",
      });
    });

    test("should return early when no Cal.diy events", async () => {
      const events = [mockNonCalComEvent];

      await service.handleEvents(mockSelectedCalendar, events);

      expect(mockBookingRepository.findBookingByUidWithEventType).not.toHaveBeenCalled();
    });

    test("should return early when no events", async () => {
      await service.handleEvents(mockSelectedCalendar, []);

      expect(mockBookingRepository.findBookingByUidWithEventType).not.toHaveBeenCalled();
    });

    test("should handle mixed case iCalUID", async () => {
      const eventWithMixedCase: CalendarSubscriptionEventItem = {
        ...mockCalComEvent,
        iCalUID: "test-booking-uid@CAL.COM",
      };

      mockBookingRepository.findBookingByUidWithEventType = vi.fn().mockResolvedValue(mockBooking);

      await service.handleEvents(mockSelectedCalendar, [eventWithMixedCase]);

      expect(mockBookingRepository.findBookingByUidWithEventType).toHaveBeenCalledWith({
        bookingUid: "test-booking-uid",
      });
    });

    test("should handle default Cal.diy iCalUID", async () => {
      const eventWithCalDiyUID: CalendarSubscriptionEventItem = {
        ...mockCalComEvent,
        iCalUID: "test-booking-uid@Cal.diy",
      };

      mockBookingRepository.findBookingByUidWithEventType = vi.fn().mockResolvedValue(mockBooking);

      await service.handleEvents(mockSelectedCalendar, [eventWithCalDiyUID]);

      expect(mockBookingRepository.findBookingByUidWithEventType).toHaveBeenCalledWith({
        bookingUid: "test-booking-uid",
      });
    });

    test("should handle events with null iCalUID", async () => {
      const eventWithNullUID: CalendarSubscriptionEventItem = {
        ...mockCalComEvent,
        iCalUID: null,
      };

      await service.handleEvents(mockSelectedCalendar, [eventWithNullUID]);

      expect(mockBookingRepository.findBookingByUidWithEventType).not.toHaveBeenCalled();
    });
  });

  describe("cancelBooking", () => {
    test("should successfully cancel a booking", async () => {
      mockBookingRepository.findBookingByUidWithEventType = vi.fn().mockResolvedValue(mockBooking);

      await service.cancelBooking(mockCancelledEvent, mockSelectedCalendar.userId);

      expect(mockBookingRepository.findBookingByUidWithEventType).toHaveBeenCalledWith({
        bookingUid: "cancelled-booking-uid",
      });

      expect(mockHandleCancelBooking).toHaveBeenCalledWith({
        userId: mockBooking.userId,
        bookingData: {
          uid: mockBooking.uid,
          cancellationReason: "Cancelled on user's calendar",
          cancelledBy: mockBooking.userPrimaryEmail,
          skipCalendarSyncTaskCancellation: true,
        },
      });
    });

    test("should return early when booking UID is missing", async () => {
      const eventWithoutUID: CalendarSubscriptionEventItem = {
        ...mockCancelledEvent,
        iCalUID: null,
      };

      await service.cancelBooking(eventWithoutUID, mockSelectedCalendar.userId);

      expect(mockBookingRepository.findBookingByUidWithEventType).not.toHaveBeenCalled();
      expect(mockHandleCancelBooking).not.toHaveBeenCalled();
    });

    test("should return early when booking UID is malformed", async () => {
      const eventWithMalformedUID: CalendarSubscriptionEventItem = {
        ...mockCancelledEvent,
        iCalUID: "@cal.com",
      };

      await service.cancelBooking(eventWithMalformedUID, mockSelectedCalendar.userId);

      expect(mockBookingRepository.findBookingByUidWithEventType).not.toHaveBeenCalled();
      expect(mockHandleCancelBooking).not.toHaveBeenCalled();
    });

    test("should return early when booking is not found", async () => {
      mockBookingRepository.findBookingByUidWithEventType = vi.fn().mockResolvedValue(null);

      await service.cancelBooking(mockCancelledEvent, mockSelectedCalendar.userId);

      expect(mockBookingRepository.findBookingByUidWithEventType).toHaveBeenCalledWith({
        bookingUid: "cancelled-booking-uid",
      });
      expect(mockHandleCancelBooking).not.toHaveBeenCalled();
    });

    test("should handle cancellation errors gracefully without throwing", async () => {
      mockBookingRepository.findBookingByUidWithEventType = vi.fn().mockResolvedValue(mockBooking);
      mockHandleCancelBooking.mockRejectedValue(new Error("Cancellation failed"));

      // Should not throw - errors are caught and logged
      await expect(
        service.cancelBooking(mockCancelledEvent, mockSelectedCalendar.userId)
      ).resolves.not.toThrow();

      expect(mockBookingRepository.findBookingByUidWithEventType).toHaveBeenCalled();
      expect(mockHandleCancelBooking).toHaveBeenCalled();
    });

    test("should handle database errors gracefully without throwing", async () => {
      mockBookingRepository.findBookingByUidWithEventType = vi
        .fn()
        .mockRejectedValue(new Error("DB connection failed"));

      await expect(
        service.cancelBooking(mockCancelledEvent, mockSelectedCalendar.userId)
      ).resolves.not.toThrow();

      expect(mockBookingRepository.findBookingByUidWithEventType).toHaveBeenCalled();
      expect(mockHandleCancelBooking).not.toHaveBeenCalled();
    });
  });

  describe("rescheduleBooking", () => {
    test("should successfully reschedule a booking preserving original duration", async () => {
      const updatedEvent: CalendarSubscriptionEventItem = {
        ...mockCalComEvent,
        start: new Date("2023-12-01T14:00:00Z"),
        end: new Date("2023-12-01T15:00:00Z"),
        summary: "Updated summary",
        description: "Updated description",
        location: "Updated location",
      };

      mockBookingRepository.findBookingByUidWithEventType = vi.fn().mockResolvedValue(mockBooking);

      await service.rescheduleBooking(updatedEvent, mockSelectedCalendar.userId);

      expect(mockBookingRepository.findBookingByUidWithEventType).toHaveBeenCalledWith({
        bookingUid: "test-booking-uid",
      });

      // Original booking is 10:00-11:00 (60min). New start is 14:00, so end should be 15:00 (60min preserved).
      expect(mockCreateBooking).toHaveBeenCalledWith({
        bookingData: {
          eventTypeId: mockBooking.eventTypeId,
          start: "2023-12-01T14:00:00.000Z",
          end: "2023-12-01T15:00:00.000Z",
          timeZone: "UTC",
          language: "en",
          metadata: expect.objectContaining({
            calendarSubscriptionEvent: expect.any(String),
          }),
          rescheduleUid: mockBooking.uid,
          idempotencyKey: expect.any(String),
          responses: expect.objectContaining({
            title: "Updated summary",
            notes: "Updated description",
            location: {
              label: "Updated location",
              optionValue: "Updated location",
              value: "Updated location",
            },
          }),
        },
        bookingMeta: {
          skipCalendarSyncTaskCreation: true,
          skipAvailabilityCheck: true,
          skipEventLimitsCheck: true,
        },
      });
      const lastCall = mockCreateBooking.mock.calls.at(-1)!;
      const parsedMetadataUpdated = JSON.parse(
        lastCall[0].bookingData.metadata.calendarSubscriptionEvent as string
      );
      expect(parsedMetadataUpdated).toEqual(
        expect.objectContaining({
          summary: "Updated summary",
          description: "Updated description",
          location: "Updated location",
        })
      );
    });

    test("should preserve original duration when external event has different duration", async () => {
      const stretchedEvent: CalendarSubscriptionEventItem = {
        ...mockCalComEvent,
        start: new Date("2023-12-01T14:00:00Z"),
        end: new Date("2023-12-01T15:30:00Z"), // 90min, but original booking is 60min
      };

      mockBookingRepository.findBookingByUidWithEventType = vi.fn().mockResolvedValue(mockBooking);

      await service.rescheduleBooking(stretchedEvent, mockSelectedCalendar.userId);

      // Original booking is 60min. External event is 90min. Duration should stay 60min.
      expect(mockCreateBooking).toHaveBeenCalledWith(
        expect.objectContaining({
          bookingData: expect.objectContaining({
            start: "2023-12-01T14:00:00.000Z",
            end: "2023-12-01T15:00:00.000Z", // 14:00 + 60min = 15:00, NOT 15:30
          }),
        })
      );
    });

    test("should skip reschedule when event times are null (no change detected)", async () => {
      const eventWithNullTimes: CalendarSubscriptionEventItem = {
        ...mockCalComEvent,
        start: null,
        end: null,
      };

      mockBookingRepository.findBookingByUidWithEventType = vi.fn().mockResolvedValue(mockBooking);

      await service.rescheduleBooking(eventWithNullTimes, mockSelectedCalendar.userId);

      // Null start means no change detected, reschedule is skipped
      expect(mockCreateBooking).not.toHaveBeenCalled();
    });

    test("should build fallback responses when booking responses are missing", async () => {
      const bookingWithoutResponses = {
        ...mockBooking,
        responses: null,
      };
      const eventWithDifferentStart: CalendarSubscriptionEventItem = {
        ...mockCalComEvent,
        start: new Date("2023-12-01T14:00:00Z"),
        end: new Date("2023-12-01T15:00:00Z"),
      };
      mockBookingRepository.findBookingByUidWithEventType = vi
        .fn()
        .mockResolvedValue(bookingWithoutResponses);

      await service.rescheduleBooking(eventWithDifferentStart, mockSelectedCalendar.userId);

      expect(mockCreateBooking).toHaveBeenCalledWith({
        bookingData: expect.objectContaining({
          responses: expect.objectContaining({
            name: bookingWithoutResponses.title,
            email: bookingWithoutResponses.userPrimaryEmail,
            guests: [],
            notes: mockCalComEvent.description,
            smsReminderNumber: bookingWithoutResponses.smsReminderNumber,
            location: {
              label: mockCalComEvent.location,
              value: mockCalComEvent.location,
              optionValue: mockCalComEvent.location,
            },
            title: mockCalComEvent.summary,
          }),
          metadata: expect.objectContaining({
            calendarSubscriptionEvent: expect.any(String),
          }),
          idempotencyKey: expect.any(String),
        }),
        bookingMeta: {
          skipCalendarSyncTaskCreation: true,
          skipAvailabilityCheck: true,
          skipEventLimitsCheck: true,
        },
      });
      const baseCall = mockCreateBooking.mock.calls.at(-1)!;
      const parsedMetadataBase = JSON.parse(
        baseCall[0].bookingData.metadata.calendarSubscriptionEvent as string
      );
      expect(parsedMetadataBase.summary).toEqual(mockCalComEvent.summary);
    });

    test("should return early when booking UID is missing", async () => {
      const eventWithoutUID: CalendarSubscriptionEventItem = {
        ...mockCalComEvent,
        iCalUID: null,
      };

      await service.rescheduleBooking(eventWithoutUID, mockSelectedCalendar.userId);

      expect(mockBookingRepository.findBookingByUidWithEventType).not.toHaveBeenCalled();
      expect(mockCreateBooking).not.toHaveBeenCalled();
    });

    test("should return early when booking UID is malformed", async () => {
      const eventWithMalformedUID: CalendarSubscriptionEventItem = {
        ...mockCalComEvent,
        iCalUID: "@cal.com",
      };

      await service.rescheduleBooking(eventWithMalformedUID, mockSelectedCalendar.userId);

      expect(mockBookingRepository.findBookingByUidWithEventType).not.toHaveBeenCalled();
      expect(mockCreateBooking).not.toHaveBeenCalled();
    });

    test("should return early when booking is not found", async () => {
      mockBookingRepository.findBookingByUidWithEventType = vi.fn().mockResolvedValue(null);

      await service.rescheduleBooking(mockCalComEvent, mockSelectedCalendar.userId);

      expect(mockBookingRepository.findBookingByUidWithEventType).toHaveBeenCalledWith({
        bookingUid: "test-booking-uid",
      });
      expect(mockCreateBooking).not.toHaveBeenCalled();
    });

    test("should handle rescheduling errors gracefully without throwing", async () => {
      const eventWithDifferentStart: CalendarSubscriptionEventItem = {
        ...mockCalComEvent,
        start: new Date("2023-12-01T14:00:00Z"),
        end: new Date("2023-12-01T15:00:00Z"),
      };
      mockBookingRepository.findBookingByUidWithEventType = vi.fn().mockResolvedValue(mockBooking);
      mockCreateBooking.mockRejectedValue(new Error("Rescheduling failed"));

      // Should not throw - errors are caught and logged
      await expect(
        service.rescheduleBooking(eventWithDifferentStart, mockSelectedCalendar.userId)
      ).resolves.not.toThrow();

      expect(mockBookingRepository.findBookingByUidWithEventType).toHaveBeenCalled();
      expect(mockCreateBooking).toHaveBeenCalled();
    });

    test("should handle database errors gracefully without throwing", async () => {
      mockBookingRepository.findBookingByUidWithEventType = vi
        .fn()
        .mockRejectedValue(new Error("DB connection failed"));

      await expect(
        service.rescheduleBooking(mockCalComEvent, mockSelectedCalendar.userId)
      ).resolves.not.toThrow();

      expect(mockBookingRepository.findBookingByUidWithEventType).toHaveBeenCalled();
      expect(mockCreateBooking).not.toHaveBeenCalled();
    });
  });
});
