import { TestBed } from '@automock/jest'
import { SessionStore } from '@backend/auth'
import { Settings } from '@backend/domain'
import { SettingsRepository } from '@backend/repository'
import { ForbiddenException } from '@nestjs/common'
import { randomUUID } from 'node:crypto'
import { SaveSettingsRequest } from '../types'
import { SettingsService } from './settings.service'

describe('Settings Service', () => {
	let settingsService: SettingsService
	let settingsRepository: jest.Mocked<SettingsRepository>
	const tenantId = randomUUID()

	const storedSettings = () =>
		({
			tenantId,
			customerApp: { displayName: 'Testhaus', logoUrl: 'https://example.test/logo.png', quickActions: [] },
			restaurant: {
				email: 'kontakt@example.test',
				address: 'Musterweg 1, 12345 Musterstadt',
				googlePlaceId: 'ChIJ_placeholder',
				timezone: 'Europe/Berlin',
			},
			reservation: {
				googleReservations: { enabled: false },
				defaultReservationTime: 60,
				dashboardReservationTimeLimit: 3,
			},
		}) as unknown as Settings

	beforeAll(() => {
		const { unit, unitRef } = TestBed.create(SettingsService).compile()
		settingsService = unit
		settingsRepository = unitRef.get(SettingsRepository)
	})

	beforeEach(() => {
		jest.clearAllMocks()
		jest.spyOn(SessionStore, 'tenantId', 'get').mockReturnValue(tenantId)
	})

	describe('save method', () => {
		it('should throw ForbiddenException without a tenant in the session', async () => {
			jest.spyOn(SessionStore, 'tenantId', 'get').mockReturnValue(undefined as unknown as string)
			await expect(settingsService.save({} as SaveSettingsRequest)).rejects.toThrow(ForbiddenException)
		})

		// Regression guard for the merge in save(): the existing row has to be read in full.
		// A narrowing `select` leaves the merged columns undefined, the spread becomes a no-op
		// and the request silently REPLACES the stored block instead of extending it.
		// This can only be asserted on the query itself — a mocked repository ignores `select`,
		// so no behavioural assertion would notice the projection.
		it('should read the existing settings without narrowing the selected columns', async () => {
			settingsRepository.findOne.mockResolvedValue(storedSettings())

			await settingsService.save({ restaurant: { email: 'neu@example.test' } } as SaveSettingsRequest)

			expect(settingsRepository.findOne).toHaveBeenCalledTimes(1)
			const options = settingsRepository.findOne.mock.calls[0][0]
			expect(options).toEqual({ where: { tenantId } })
			expect(options.select).toBeUndefined()
		})

		it('should keep stored restaurant fields that the request does not carry', async () => {
			settingsRepository.findOne.mockResolvedValue(storedSettings())

			await settingsService.save({
				restaurant: { email: 'neu@example.test', address: 'Musterweg 1, 12345 Musterstadt', phone: '+4915112345678' },
			} as SaveSettingsRequest)

			const [, written] = settingsRepository.update.mock.calls[0]
			expect(written.restaurant).toEqual({
				email: 'neu@example.test',
				address: 'Musterweg 1, 12345 Musterstadt',
				phone: '+4915112345678',
				googlePlaceId: 'ChIJ_placeholder',
				timezone: 'Europe/Berlin',
			})
		})

		it('should keep stored reservation fields when only one flag is sent', async () => {
			settingsRepository.findOne.mockResolvedValue(storedSettings())

			await settingsService.save({
				reservation: { googleReservations: { enabled: true } },
			} as unknown as SaveSettingsRequest)

			const [, written] = settingsRepository.update.mock.calls[0]
			expect(written.reservation).toEqual({
				googleReservations: { enabled: true },
				defaultReservationTime: 60,
				dashboardReservationTimeLimit: 3,
			})
		})

		it('should keep stored customerApp fields when only one is sent', async () => {
			settingsRepository.findOne.mockResolvedValue(storedSettings())

			await settingsService.save({ customerApp: { displayName: 'Neuer Name' } } as SaveSettingsRequest)

			const [, written] = settingsRepository.update.mock.calls[0]
			expect(written.customerApp).toEqual({
				displayName: 'Neuer Name',
				logoUrl: 'https://example.test/logo.png',
				quickActions: [],
			})
		})

		it('should insert without merging when no settings exist yet', async () => {
			settingsRepository.findOne.mockResolvedValue(null)
			const request = { restaurant: { email: 'neu@example.test' } } as SaveSettingsRequest

			await settingsService.save(request)

			expect(settingsRepository.save).toHaveBeenCalledWith(request)
			expect(settingsRepository.update).not.toHaveBeenCalled()
		})
	})
})
