import { TestBed } from '@automock/jest'
import { Reservation, ReservationSource, Settings, Table } from '@backend/domain'
import { ReservationRepository, SettingsRepository, TableRepository } from '@backend/repository'
import { AnyType, ReservationState } from '@core/types'
import { DateUtils } from '@core/utils'
import { BadRequestException } from '@nestjs/common'
import { addMinutes, set } from 'date-fns'
import { randomUUID } from 'node:crypto'
import { TimingRule } from '../../settings'
import { CreateReservationRequest, TimeSlot } from '../types'
import { ReservationService } from './reservation.service'

describe('Reservation Service', () => {
	let reservationService: ReservationService
	let reservationRepository: jest.Mocked<ReservationRepository>
	let tableRepository: jest.Mocked<TableRepository>
	let settingsRepository: jest.Mocked<SettingsRepository>

	beforeAll(() => {
		const { unit, unitRef } = TestBed.create(ReservationService).compile()
		reservationService = unit
		reservationRepository = unitRef.get(ReservationRepository)
		tableRepository = unitRef.get(TableRepository)
		settingsRepository = unitRef.get(SettingsRepository)
	})

	describe('create method', () => {
		xit('should create a new reservation', async () => {
			// Set system time to 05:00
			jest.useFakeTimers().setSystemTime(set(Date.now(), { hours: 5, minutes: 0 }))
			const tables = [{ id: randomUUID(), capacity: 2 }] as Table[]
			const request = {
				noOfPersons: 2,
				startDate: addMinutes(new Date(), 60),
				endDate: addMinutes(new Date(), 90),
				customerData: {},
			} as CreateReservationRequest
			const overlappingReservations = []
			jest
				.spyOn(reservationService as AnyType, 'findOverlappingReservations')
				.mockResolvedValue(overlappingReservations)
			tableRepository.find.mockResolvedValue(tables)
			reservationRepository.save.mockResolvedValue({} as Reservation)
			jest.spyOn(reservationService as AnyType, 'autoAcceptReservation').mockImplementation(() => undefined)
			await reservationService.create(request, ReservationSource.admin)
			expect(reservationRepository.save).toHaveBeenCalledTimes(1)
		})

		xit('should throw error if all tables have approved reservations in same time slot', async () => {
			const tables = [{ id: randomUUID() }] as Table[]
			const request = {
				startDate: new Date(),
				endDate: addMinutes(new Date(), 30),
			} as CreateReservationRequest
			const overlappingReservations = [
				{
					startDate: new Date(),
					endDate: addMinutes(new Date(), 30),
					state: ReservationState.approved,
				},
			] as Reservation[]
			jest
				.spyOn(reservationService as AnyType, 'findOverlappingReservations')
				.mockResolvedValue(overlappingReservations)
			tableRepository.find.mockResolvedValue(tables)
			await expect(reservationService.create(request, ReservationSource.admin)).rejects.toThrow(
				new BadRequestException('Selected Time Slot is not available'),
			)
		})
	})

	describe('assignTable method', () => {
		xit('should return table if there is no overlapping reservation', async () => {
			const tables = [{ id: randomUUID() }] as Table[]
			const request = {
				tableId: tables[0].id,
				startDate: new Date(),
				endDate: addMinutes(new Date(), 30),
			} as CreateReservationRequest
			const reservations = []
			const table = ReservationService.prototype['assignTable'].call(reservationService, request, tables, reservations)
			expect(table).toBe(tables[0])
		})
		xit('should throw error if provided table id has approved reservation in same time slot', async () => {
			const tables = [{ id: randomUUID() }, { id: randomUUID() }] as Table[]
			const request = {
				tableId: tables[0].id,
				startDate: new Date(),
				endDate: addMinutes(new Date(), 30),
			} as CreateReservationRequest
			const reservations = [
				{
					tables: [tables[0]],
					startDate: addMinutes(new Date(), 0),
					endDate: addMinutes(new Date(), 30),
					state: ReservationState.approved,
				},
			] as Reservation[]
			expect(() =>
				ReservationService.prototype['assignTable'].call(reservationService, request, tables, reservations),
			).toThrow(new BadRequestException('Selected Time Slot is not available'))
		})
	})

	describe('generateTimeSlots method', () => {
		xit('should create timeslots for all rules', async () => {
			// Set system time to 05:00
			jest.useFakeTimers().setSystemTime(set(new Date(), { hours: 5, minutes: 0 }))
			const date = DateUtils.normalizeDay(new Date())
			const tableCount = 4
			const rules = [
				{
					openingTime: { hours: 10, minutes: 0 },
					closingTime: { hours: 12, minutes: 30 },
				},
			] as TimingRule[]
			const reservations = []
			const timeSlots = ReservationService.prototype['generateTimeSlots'].call(
				reservationService,
				date,
				rules,
				tableCount,
				reservations,
			)
			const expected = [
				{ available: true, time: { hours: 10, minutes: 0 } },
				{ available: true, time: { hours: 10, minutes: 30 } },
				{ available: true, time: { hours: 11, minutes: 0 } },
				{ available: true, time: { hours: 11, minutes: 30 } },
				{ available: true, time: { hours: 12, minutes: 0 } },
				{ available: true, time: { hours: 12, minutes: 30 } },
			] as TimeSlot[]
			expect(timeSlots).toEqual(expected)
		})

		xit('should mark past time slots as not available', async () => {
			// Set system time to 11:15
			jest.useFakeTimers().setSystemTime(set(new Date(), { hours: 11, minutes: 15 }))
			const date = new Date()
			const tableCount = 4
			const rules = [
				{
					openingTime: { hours: 10, minutes: 0 },
					closingTime: { hours: 12, minutes: 30 },
				},
			] as TimingRule[]
			const reservations = []
			const timeSlots = ReservationService.prototype['generateTimeSlots'].call(
				reservationService,
				date,
				rules,
				tableCount,
				reservations,
			)
			const expected = [
				{ available: false, time: { hours: 10, minutes: 0 } },
				{ available: false, time: { hours: 10, minutes: 30 } },
				{ available: false, time: { hours: 11, minutes: 0 } },
				{ available: true, time: { hours: 11, minutes: 30 } },
				{ available: true, time: { hours: 12, minutes: 0 } },
				{ available: true, time: { hours: 12, minutes: 30 } },
			] as TimeSlot[]
			expect(timeSlots).toMatchObject(expected)
		})

		xit('should mark time slots as not available when all tables already have reservations on same slots', async () => {
			// Set system time to 05:00
			jest.useFakeTimers().setSystemTime(set(new Date(), { hours: 5, minutes: 0 }))
			const date = new Date()
			const tableCount = 2
			const rules = [
				{
					openingTime: { hours: 10, minutes: 0 },
					closingTime: { hours: 12, minutes: 30 },
				},
			] as TimingRule[]
			const reservations = [
				{
					startDate: set(new Date(), { hours: 11, minutes: 0 }),
					endDate: set(new Date(), { hours: 12, minutes: 0 }),
				} as Reservation,
				{
					startDate: set(new Date(), { hours: 11, minutes: 0 }),
					endDate: set(new Date(), { hours: 12, minutes: 0 }),
				} as Reservation,
			]
			const timeSlots = ReservationService.prototype['generateTimeSlots'].call(
				reservationService,
				date,
				rules,
				tableCount,
				reservations,
			)
			const expected = [
				{ available: true, time: { hours: 10, minutes: 0 } },
				{ available: true, time: { hours: 10, minutes: 30 } },
				{ available: false, time: { hours: 11, minutes: 0 } },
				{ available: false, time: { hours: 11, minutes: 30 } },
				{ available: true, time: { hours: 12, minutes: 0 } },
				{ available: true, time: { hours: 12, minutes: 30 } },
			] as TimeSlot[]
			expect(timeSlots).toMatchObject(expected)
		})

		xit('should add time offset difference when checking for slot availability', () => {
			// Set system time to 11:15
			jest.useFakeTimers().setSystemTime(set(new Date(), { hours: 10, minutes: 45 }))
			const date = new Date()
			const tableCount = 4
			const rules = [
				{
					openingTime: { hours: 10, minutes: 0 },
					closingTime: { hours: 12, minutes: 30 },
				},
			] as TimingRule[]
			const reservations = []
			const tzOffset = 60
			const timeSlots = ReservationService.prototype['generateTimeSlots'].call(
				reservationService,
				date,
				rules,
				tableCount,
				reservations,
				tzOffset,
			)
			const expected = [
				{ available: false, time: { hours: 10, minutes: 0 } },
				{ available: false, time: { hours: 10, minutes: 30 } },
				{ available: false, time: { hours: 11, minutes: 0 } },
				{ available: false, time: { hours: 11, minutes: 30 } },
				{ available: true, time: { hours: 12, minutes: 0 } },
				{ available: true, time: { hours: 12, minutes: 30 } },
			] as TimeSlot[]
			expect(timeSlots).toMatchObject(expected)
		})
	})

	describe('autoAcceptReservation method', () => {
		xit('should change reservation state to approved when auto reservation is enabled', async () => {
			const reservation = { state: ReservationState.unconfirmed } as Reservation
			const settings = {
				reservation: {
					autoApproveReservations: {
						enabled: true,
						guestLimitOverall: 10,
						guestLimitPerReservation: 5,
					},
				},
			} as Settings
			settingsRepository.findOne.mockResolvedValue(settings)
			reservationRepository.count.mockResolvedValue(2)
			await expect(
				ReservationService.prototype['autoAcceptReservation'].call(reservationService, reservation),
			).resolves.toMatchObject({
				state: ReservationState.approved,
			})
		})
		xit('should not update reservation state when autoAcceptReservation is disabled', async () => {
			const reservation = { state: ReservationState.unconfirmed } as Reservation
			const settings = {
				reservation: {
					autoApproveReservations: {
						enabled: false,
						guestLimitOverall: 10,
						guestLimitPerReservation: 5,
					},
				},
			} as Settings
			settingsRepository.findOne.mockResolvedValue(settings)
			reservationRepository.count.mockResolvedValue(2)
			await expect(
				ReservationService.prototype['autoAcceptReservation'].call(reservationService, reservation),
			).resolves.toMatchObject({
				state: ReservationState.unconfirmed,
			})
		})

		xit('should not update reservation state when autoAcceptReservationLimit is reached', async () => {
			const reservation = { state: ReservationState.unconfirmed } as Reservation
			const settings = {
				reservation: {
					autoApproveReservations: {
						enabled: false,
						guestLimitOverall: 10,
						guestLimitPerReservation: 5,
					},
				},
			} as Settings
			settingsRepository.findOne.mockResolvedValue(settings)
			reservationRepository.count.mockResolvedValue(5)
			await expect(
				ReservationService.prototype['autoAcceptReservation'].call(reservationService, reservation),
			).resolves.toMatchObject({
				state: ReservationState.unconfirmed,
			})
		})
	})
})
