import { TestBed } from '@automock/jest'
import { Reservation } from '@backend/domain'
import { ReservationState } from '@core/types'
import { randomUUID } from 'node:crypto'
import { ReservationEmailEvent } from '../types'
import { ReservationEmailService } from './email.service'
import { ReservationUtilsService } from './utils.service'

const flushPromises = () => new Promise(setImmediate)

describe('ReservationUtilsService', () => {
	let service: ReservationUtilsService
	let emailService: jest.Mocked<ReservationEmailService>
	let loggerErrorSpy: jest.SpyInstance

	beforeAll(() => {
		const { unit, unitRef } = TestBed.create(ReservationUtilsService).compile()
		service = unit
		emailService = unitRef.get(ReservationEmailService)
	})

	beforeEach(() => {
		jest.clearAllMocks()
		// eslint-disable-next-line @typescript-eslint/no-explicit-any
		loggerErrorSpy = jest.spyOn((service as any).logger, 'error').mockImplementation()
	})

	describe('notifyNewReservation', () => {
		const reservation = (state: ReservationState) => new Reservation({ id: randomUUID(), state, noOfPersons: 2 })

		it('should notify the createdConfirmed event for approved reservations', async () => {
			emailService.notifyEvent.mockResolvedValue(undefined)

			service.notifyNewReservation(reservation(ReservationState.approved))
			await flushPromises()

			expect(emailService.notifyEvent).toHaveBeenCalledTimes(1)
			expect(emailService.notifyEvent).toHaveBeenCalledWith(
				ReservationEmailEvent.createdConfirmed,
				expect.objectContaining({ state: ReservationState.approved }),
			)
			expect(loggerErrorSpy).not.toHaveBeenCalled()
		})

		it('should notify the createdPending event for not yet approved reservations', async () => {
			emailService.notifyEvent.mockResolvedValue(undefined)

			service.notifyNewReservation(reservation(ReservationState.unconfirmed))
			await flushPromises()

			expect(emailService.notifyEvent).toHaveBeenCalledTimes(1)
			expect(emailService.notifyEvent).toHaveBeenCalledWith(
				ReservationEmailEvent.createdPending,
				expect.objectContaining({ state: ReservationState.unconfirmed }),
			)
			expect(loggerErrorSpy).not.toHaveBeenCalled()
		})

		// Regression: a rejected email promise was fire-and-forgotten with
		// .then() and crashed the process as an unhandled rejection (KAR-722)
		it('should log instead of rejecting unhandled when the email fails', async () => {
			emailService.notifyEvent.mockRejectedValue(new Error('SES is down'))

			expect(() => service.notifyNewReservation(reservation(ReservationState.approved))).not.toThrow()
			await flushPromises()

			expect(loggerErrorSpy).toHaveBeenCalledWith(
				expect.objectContaining({
					message: 'Failed to send email for new reservation',
					error: 'SES is down',
				}),
			)
		})
	})

	describe('notifyReservationUpdate', () => {
		const reservation = (state: ReservationState) => new Reservation({ id: randomUUID(), state, noOfPersons: 2 })

		it('should forward an explicit event to the email service', async () => {
			emailService.notifyEvent.mockResolvedValue(undefined)

			await service.notifyReservationUpdate({
				updatedReservation: reservation(ReservationState.rejected),
				updates: [{ field: 'state', oldValue: ReservationState.approved, newValue: ReservationState.rejected }],
				event: ReservationEmailEvent.cancelledByGuest,
			})

			expect(emailService.notifyEvent).toHaveBeenCalledWith(
				ReservationEmailEvent.cancelledByGuest,
				expect.anything(),
				expect.anything(),
			)
		})

		it('should derive cancelledByRestaurant from a state update to rejected when no event is given', async () => {
			emailService.notifyEvent.mockResolvedValue(undefined)

			await service.notifyReservationUpdate({
				updatedReservation: reservation(ReservationState.rejected),
				updates: [{ field: 'state', oldValue: ReservationState.approved, newValue: ReservationState.rejected }],
			})

			expect(emailService.notifyEvent).toHaveBeenCalledWith(
				ReservationEmailEvent.cancelledByRestaurant,
				expect.anything(),
				expect.anything(),
			)
		})

		it('should derive updated for non-state changes when no event is given', async () => {
			emailService.notifyEvent.mockResolvedValue(undefined)

			await service.notifyReservationUpdate({
				updatedReservation: reservation(ReservationState.approved),
				updates: [{ field: 'noOfPersons', oldValue: 2, newValue: 4 }],
			})

			expect(emailService.notifyEvent).toHaveBeenCalledWith(
				ReservationEmailEvent.updated,
				expect.anything(),
				expect.anything(),
			)
		})
	})
})
