import { TestBed } from '@automock/jest'
import { Reservation, Settings } from '@backend/domain'
import { SettingsRepository } from '@backend/repository'
import { ReservationState } from '@core/types'
import { randomUUID } from 'node:crypto'
import { EmailService, MAILER_TEMPLATES } from '../../../global'
import { ReservationEmailEvent } from '../types'
import { ReservationEmailService } from './email.service'
import { ICSService } from './ics.service'
import { ReservationStatsService } from './reservation-stats.service'

const settings = {
	customerApp: { displayName: 'Restaurant Testhaus', logoUrl: 'https://cdn.example/logo.png' },
	restaurant: {
		email: 'info@testhaus.example',
		phone: '+49 555 010000',
		address: 'Musterstraße 1, 12345 Musterstadt',
		googlePlaceId: 'place-id',
		timezone: 'Europe/Berlin',
	},
} as unknown as Settings

// 2026-08-20 15:30 UTC = 17:30 Europe/Berlin (Donnerstag)
const START = new Date('2026-08-20T15:30:00Z')
const END = new Date('2026-08-20T17:30:00Z')

function reservation(overrides: Partial<Reservation> = {}): Reservation {
	return new Reservation({
		id: randomUUID(),
		tenantId: randomUUID(),
		state: ReservationState.approved,
		noOfPersons: 4,
		startDate: START,
		endDate: END,
		comment: 'Bitte Kinderstuhl',
		customerData: {
			name: 'Maxi Musterperson',
			lastName: 'Musterperson',
			email: 'maxi@example.com',
			phoneNumber: '+49 555 0123',
		},
		managementToken: 'token-abcdefghijklmnopqrstuvwxyz012345',
		...overrides,
	} as Partial<Reservation>)
}

describe('ReservationEmailService', () => {
	let service: ReservationEmailService
	let emailService: jest.Mocked<EmailService>
	let settingsRepository: jest.Mocked<SettingsRepository>
	let statsService: jest.Mocked<ReservationStatsService>
	let icsService: jest.Mocked<ICSService>

	beforeAll(() => {
		const { unit, unitRef } = TestBed.create(ReservationEmailService).compile()
		service = unit
		emailService = unitRef.get(EmailService)
		settingsRepository = unitRef.get(SettingsRepository)
		statsService = unitRef.get(ReservationStatsService)
		icsService = unitRef.get(ICSService)
	})

	beforeEach(() => {
		jest.clearAllMocks()
		settingsRepository.findOne.mockResolvedValue(settings)
		statsService.getDayStats.mockResolvedValue({ reservationCount: 12, guestCount: 38 })
		icsService.generateIcs.mockReturnValue('BEGIN:VCALENDAR')
		emailService.send.mockResolvedValue(undefined)
	})

	function sentMails() {
		return emailService.send.mock.calls.map((call) => call[0])
	}

	it('createdConfirmed sends a German guest confirmation and an owner notification', async () => {
		await service.notifyEvent(ReservationEmailEvent.createdConfirmed, reservation())

		const mails = sentMails()
		expect(mails).toHaveLength(2)

		const guest = mails.find((mail) => mail.to === 'maxi@example.com')
		const owner = mails.find((mail) => mail.to === 'info@testhaus.example')

		expect(guest?.subject).toBe('Reservierung bestätigt – Restaurant Testhaus')
		expect(guest?.template).toBe(MAILER_TEMPLATES.RESERVATION.GUEST.CONFIRMED)
		expect(guest?.attachments).toHaveLength(1)
		expect(guest?.text).toContain('Donnerstag, 20. August 2026')
		expect((guest?.context as Record<string, unknown>)['manage_url']).toContain(
			'/manage/index.html?token=token-abcdefghijklmnopqrstuvwxyz012345',
		)
		expect((guest?.context as Record<string, unknown>)['date_long']).toBe('Donnerstag, 20. August 2026')
		expect((guest?.context as Record<string, unknown>)['time_start']).toBe('17:30')
		expect((guest?.context as Record<string, unknown>)['persons_label']).toBe('4 Personen')

		expect(owner?.subject).toBe('Neue Reservierung · 4 Personen · Do. 20.08. · 17:30 · Musterperson')
		expect(owner?.template).toBe(MAILER_TEMPLATES.RESERVATION.OWNER.NEW)
		expect((owner?.context as Record<string, unknown>)['day_reservation_count']).toBe(12)
		expect((owner?.context as Record<string, unknown>)['day_guest_count']).toBe(38)
		expect((owner?.context as Record<string, unknown>)['is_pending']).toBe(false)
	})

	it('createdPending sends a request-received guest mail and owner mail with action links', async () => {
		await service.notifyEvent(ReservationEmailEvent.createdPending, reservation({ state: ReservationState.unconfirmed }))

		const mails = sentMails()
		const guest = mails.find((mail) => mail.to === 'maxi@example.com')
		const owner = mails.find((mail) => mail.to === 'info@testhaus.example')

		expect(guest?.subject).toBe('Reservierungsanfrage erhalten – Restaurant Testhaus')
		expect(guest?.template).toBe(MAILER_TEMPLATES.RESERVATION.GUEST.PENDING)
		expect(owner?.subject).toContain('Neue Reservierungsanfrage')
		expect((owner?.context as Record<string, unknown>)['is_pending']).toBe(true)
		expect((owner?.context as Record<string, unknown>)['accept_url']).toContain('action=accept')
		expect((owner?.context as Record<string, unknown>)['decline_url']).toContain('action=decline')
	})

	it('updated renders change rows for date, time and persons', async () => {
		const oldStart = new Date('2026-08-20T15:30:00Z')
		const newStart = new Date('2026-08-21T16:00:00Z')

		await service.notifyEvent(ReservationEmailEvent.updated, reservation({ startDate: newStart }), [
			{ field: 'startDate', oldValue: oldStart, newValue: newStart },
			{ field: 'noOfPersons', oldValue: 4, newValue: 5 },
		])

		const guest = sentMails().find((mail) => mail.to === 'maxi@example.com')
		const changes = (guest?.context as Record<string, unknown>)['changes'] as Array<Record<string, string>>

		expect(guest?.subject).toBe('Ihre Reservierung wurde geändert – Restaurant Testhaus')
		expect(changes).toEqual(
			expect.arrayContaining([
				expect.objectContaining({ label: 'Datum', oldValue: 'Do. 20.08.', newValue: 'Fr. 21.08.' }),
				expect.objectContaining({ label: 'Uhrzeit', oldValue: '17:30 Uhr', newValue: '18:00 Uhr' }),
				expect.objectContaining({ label: 'Personen', oldValue: '4 Personen', newValue: '5 Personen' }),
			]),
		)
	})

	it('cancelledByGuest notifies guest AND owner with cancellation templates', async () => {
		await service.notifyEvent(
			ReservationEmailEvent.cancelledByGuest,
			reservation({ state: ReservationState.rejected }),
		)

		const mails = sentMails()
		const guest = mails.find((mail) => mail.to === 'maxi@example.com')
		const owner = mails.find((mail) => mail.to === 'info@testhaus.example')

		expect(guest?.template).toBe(MAILER_TEMPLATES.RESERVATION.GUEST.CANCELLED)
		expect(guest?.subject).toBe('Ihre Reservierung wurde storniert – Restaurant Testhaus')
		expect(owner?.template).toBe(MAILER_TEMPLATES.RESERVATION.OWNER.CANCELLED)
		expect(owner?.subject).toBe('Reservierung storniert · 4 Personen · Do. 20.08. · 17:30 · Musterperson')
		expect((owner?.context as Record<string, unknown>)['cancelled_by_guest']).toBe(true)
	})

	it('cancelledByRestaurant notifies only the guest (the restaurant acted itself)', async () => {
		await service.notifyEvent(
			ReservationEmailEvent.cancelledByRestaurant,
			reservation({ state: ReservationState.rejected }),
		)

		const mails = sentMails()
		expect(mails).toHaveLength(1)
		expect(mails[0].to).toBe('maxi@example.com')
		expect(mails[0].template).toBe(MAILER_TEMPLATES.RESERVATION.GUEST.CANCELLED)
	})

	it('confirmed notifies only the guest', async () => {
		await service.notifyEvent(ReservationEmailEvent.confirmed, reservation())

		const mails = sentMails()
		expect(mails).toHaveLength(1)
		expect(mails[0].to).toBe('maxi@example.com')
		expect(mails[0].subject).toBe('Reservierung bestätigt – Restaurant Testhaus')
	})

	it('singular persons label is used for one guest', async () => {
		await service.notifyEvent(ReservationEmailEvent.confirmed, reservation({ noOfPersons: 1 }))

		const guest = sentMails()[0]
		expect((guest.context as Record<string, unknown>)['persons_label']).toBe('1 Person')
	})

	it('skips the guest mail when the reservation has no email but still notifies the owner', async () => {
		const entity = reservation()
		entity.customerData.email = undefined

		await service.notifyEvent(ReservationEmailEvent.createdConfirmed, entity)

		const mails = sentMails()
		expect(mails).toHaveLength(1)
		expect(mails[0].to).toBe('info@testhaus.example')
	})

	it('sends companion mails without the management link', async () => {
		await service.notifyEvent(
			ReservationEmailEvent.createdConfirmed,
			reservation({ companionEmails: ['freund1@web.de', 'freund2@gmx.de'] }),
		)

		const mails = sentMails()
		expect(mails).toHaveLength(4) // Gast + Owner + 2 Companions

		const companion = mails.find((mail) => mail.to === 'freund1@web.de')
		expect(companion?.subject).toBe('Reservierung bestätigt – Restaurant Testhaus')
		expect((companion?.context as Record<string, unknown>)['manage_url']).toBeUndefined()
		expect(companion?.text).not.toContain('Reservierung verwalten')
		expect(companion?.attachments).toHaveLength(1)

		const booker = mails.find((mail) => mail.to === 'maxi@example.com')
		expect((booker?.context as Record<string, unknown>)['manage_url']).toContain('/manage/')
	})

	it('cancellation mails carry a calendar CANCEL attachment', async () => {
		icsService.generateIcs.mockImplementation((_r, _s, options) =>
			options?.cancelled ? 'METHOD:CANCEL' : 'METHOD:REQUEST',
		)

		await service.notifyEvent(
			ReservationEmailEvent.cancelledByGuest,
			reservation({ state: ReservationState.rejected }),
		)

		const guest = sentMails().find((mail) => mail.to === 'maxi@example.com')
		expect(guest?.attachments).toHaveLength(1)
		expect(guest?.attachments?.[0].content).toBe('METHOD:CANCEL')
		expect(guest?.attachments?.[0].contentType).toContain('method=CANCEL')
	})

	it('builds the maps link from the configured place id', async () => {
		await service.notifyEvent(ReservationEmailEvent.confirmed, reservation())

		const guest = sentMails()[0]
		const mapsUrl = (guest.context as Record<string, unknown>)['maps_url'] as string
		expect(mapsUrl).toContain('query_place_id=place-id')
	})

	it('a failing SES send is logged and never thrown', async () => {
		emailService.send.mockRejectedValue(new Error('SES down'))

		await expect(service.notifyEvent(ReservationEmailEvent.createdConfirmed, reservation())).resolves.toBeUndefined()
	})

	it('a failing day-stats query still sends the owner mail without stats', async () => {
		statsService.getDayStats.mockRejectedValue(new Error('db down'))

		await service.notifyEvent(ReservationEmailEvent.createdConfirmed, reservation())

		const owner = sentMails().find((mail) => mail.to === 'info@testhaus.example')
		expect(owner).toBeDefined()
		expect((owner?.context as Record<string, unknown>)['day_reservation_count']).toBeUndefined()
	})
})
