// Mock all the things before any imports
jest.mock('#services/database/sequelize/sequelizeService');
jest.mock('#services/database/sequelize/initExport', () => ({}), { virtual: true });
jest.mock('#models/init-models', () => ({ initModels: jest.fn(() => ({})) }), { virtual: true });
jest.mock('#providers/NotificationMessageProvider');
jest.mock('#providers/NotificationReceiptProvider');
jest.mock('#providers/NotificationDeviceProvider');
jest.mock('#providers/NotificationDeliveryAttemptProvider');
jest.mock('#providers/NotificationPreferenceProvider');

jest.mock('#models/NotificationMessage', () => ({
	NotificationMessage: {
		create: jest.fn(),
		findAll: jest.fn(),
		findOne: jest.fn(),
		findByPk: jest.fn(),
		update: jest.fn(),
		destroy: jest.fn(),
		count: jest.fn(),
	},
}));

jest.mock('#models/NotificationDevice', () => ({
	NotificationDevice: {
		create: jest.fn(),
		findAll: jest.fn(),
		findOne: jest.fn(),
		findByPk: jest.fn(),
		update: jest.fn(),
		destroy: jest.fn(),
		count: jest.fn(),
	},
}));

jest.mock('#models/NotificationDeliveryAttempt', () => ({
	NotificationDeliveryAttempt: {
		create: jest.fn(),
		findAll: jest.fn(),
		findOne: jest.fn(),
		findByPk: jest.fn(),
		update: jest.fn(),
		destroy: jest.fn(),
		count: jest.fn(),
		belongsTo: jest.fn(),
		hasMany: jest.fn(),
	},
}));

jest.mock('#models/NotificationDeliveryAttempt', () => ({
	NotificationDeliveryAttempt: {
		create: jest.fn(),
		findAll: jest.fn(),
		findOne: jest.fn(),
		findByPk: jest.fn(),
		update: jest.fn(),
		destroy: jest.fn(),
		count: jest.fn(),
	},
}));

jest.mock('#models/NotificationPreference', () => ({
	NotificationPreference: {
		create: jest.fn(),
		findAll: jest.fn(),
		findOne: jest.fn(),
		findByPk: jest.fn(),
		update: jest.fn(),
		destroy: jest.fn(),
		count: jest.fn(),
	},
}));

// Mock providers
jest.mock('#providers/NotificationMessageProvider', () => ({
	NotificationMessageProvider: {
		getInstance: jest.fn(() => ({
			post: jest.fn(),
			create: jest.fn(),
			bulkCreate: jest.fn(),
			getAll: jest.fn(),
			getOne: jest.fn(),
			put: jest.fn(),
			delete: jest.fn(),
			getUnreadCount: jest.fn(),
			markAsRead: jest.fn(),
			markAllAsRead: jest.fn(),
			softDelete: jest.fn(),
			getByType: jest.fn(),
			getExpiredNotifications: jest.fn(),
			cleanupExpiredNotifications: jest.fn(),
		})),
	},
}));

jest.mock('#providers/NotificationReceiptProvider', () => ({
	NotificationReceiptProvider: {
		getInstance: jest.fn(() => ({
			post: jest.fn(),
			create: jest.fn(),
			bulkCreate: jest.fn(),
			getAll: jest.fn(),
			getOne: jest.fn(),
			put: jest.fn(),
			delete: jest.fn(),
		})),
	},
}));

jest.mock('#providers/NotificationDeviceProvider', () => ({
	NotificationDeviceProvider: {
		getInstance: jest.fn(() => ({
			post: jest.fn(),
			getAll: jest.fn(),
			getOne: jest.fn(),
			put: jest.fn(),
			delete: jest.fn(),
		})),
	},
}));

jest.mock('#providers/NotificationDeliveryAttemptProvider', () => ({
	NotificationDeliveryAttemptProvider: {
		getInstance: jest.fn(() => ({
			post: jest.fn(),
			create: jest.fn(),
			getAll: jest.fn(),
			getOne: jest.fn(),
			put: jest.fn(),
			delete: jest.fn(),
		})),
	},
}));

jest.mock('#providers/NotificationPreferenceProvider', () => ({
	NotificationPreferenceProvider: {
		getInstance: jest.fn(() => ({
			post: jest.fn(),
			getAll: jest.fn(),
			getOne: jest.fn(),
			put: jest.fn(),
			delete: jest.fn(),
			getPreferencesForUsers: jest.fn().mockResolvedValue(new Map()),
		})),
	},
}));

// Mock services
jest.mock('#services/notification/notificationPushService', () => ({
	__esModule: true,
	default: {
		sendNotification: jest.fn(),
	},
}));

jest.mock('#services/notification/notificationQueueService', () => ({
	__esModule: true,
	PRIORITY: {
		P1_CRITICAL: 1,
		P2_HIGH: 2,
		P3_LOW: 3,
	},
	default: {
		getInstance: jest.fn(() => ({
			sendNotification: jest.fn().mockResolvedValue({ id: 'job-1' }),
			sendBatch: jest.fn().mockResolvedValue({ id: 'job-1' }),
		})),
	},
}));

// Mock fetch
global.fetch = jest.fn();

// Mock the entire init-models
jest.mock('#models/init-models', () => ({}));

// Now import after all mocks are set up
import { Op } from 'sequelize';
import NotificationService from '#services/notification/notificationService';
import type { NotificationMessage } from '#models/NotificationMessage';

describe('NotificationService - Isolated Tests', () => {
	let service: NotificationService;

	beforeEach(() => {
		jest.clearAllMocks();
		service = NotificationService.getInstance();
	});

	describe('Singleton Pattern', () => {
		it('should return the same instance', () => {
			const instance1 = NotificationService.getInstance();
			const instance2 = NotificationService.getInstance();

			expect(instance1).toBe(instance2);
		});
	});

	describe('createAndSend', () => {
		it('should create notification for single user', async () => {
			const mockNotification = {
				id: 'test-id',
				title: 'Test',
				body: 'Test body',
				type: 'system_alert',
				channel: 'in_app',
				platform: null,
				data: null,
				tenant_id: null,
				expired_at: null,
				created_at: new Date(),
				updated_at: new Date(),
			} as unknown as NotificationMessage;

			// Get the provider instance and mock its methods
			const provider = (service as any).notificationProvider;
			const receiptProvider = (service as any).receiptProvider;
			provider.create.mockResolvedValue(mockNotification);
			receiptProvider.bulkCreate.mockResolvedValue([{ id: 'receipt-1' }]);

			// Mock sendNotification to avoid actual sending
			jest.spyOn(service as any, 'sendNotification').mockResolvedValue(undefined);

			const result = await service.createAndSend({
				userId: 'user-123',
				title: 'Test',
				body: 'Test body',
				type: 'system_alert',
				channel: 'in_app',
			});

			expect(result).toHaveLength(1);
			expect(result[0]).toEqual(mockNotification);
		});

		it('should handle empty user array', async () => {
			const receiptProvider = (service as any).receiptProvider;
			receiptProvider.bulkCreate.mockResolvedValue([]);

			const result = await service.createAndSend({
				userId: [],
				title: 'Test',
				body: 'Test body',
				type: 'system_alert',
				channel: 'in_app',
			});

			// The service creates a notification message even with empty user array
			// but creates no receipts, so it returns the message
			expect(result).toHaveLength(1);
		});

		it('should handle multiple users', async () => {
			const mockNotification = {
				id: 'test-id',
				title: 'Test',
				body: 'Test body',
			} as unknown as NotificationMessage;

			const provider = (service as any).notificationProvider;
			const receiptProvider = (service as any).receiptProvider;
			provider.create.mockResolvedValue(mockNotification);
			receiptProvider.bulkCreate.mockResolvedValue([{ id: 'receipt-1' }, { id: 'receipt-2' }]);

			jest.spyOn(service as any, 'sendNotification').mockResolvedValue(undefined);

			const result = await service.createAndSend({
				userId: ['user-1', 'user-2'],
				title: 'Test',
				body: 'Test body',
				type: 'system_alert',
				channel: 'in_app',
			});

			expect(result).toHaveLength(1);
		});
	});

	describe('loadUserDevices', () => {
		it('should return devices from provider', async () => {
			const mockDevices = [
				{
					id: 'device-1',
					user_id: 'user-123',
					platform: 'mobile',
					is_active: true,
				},
			];

			const provider = (service as any).channelService.deviceProvider;
			provider.getAll.mockResolvedValue({
				rows: mockDevices,
				count: 1,
			});

			const result = await (service as any).channelService.loadUserDevices('user-123', 'ios');

			expect(result).toEqual(mockDevices);
			expect(provider.getAll).toHaveBeenCalledWith({
				where: {
					user_id: 'user-123',
					is_active: true,
					platform: 'ios',
				},
				sortField: 'last_used_at',
				sortOrder: 'DESC',
				page: 1,
				pageSize: 100,
			});
		});

		it('should return empty array on error', async () => {
			const provider = (service as any).channelService.deviceProvider;
			provider.getAll.mockRejectedValue(new Error('DB Error'));

			const result = await (service as any).channelService.loadUserDevices('user-123');

			expect(result).toHaveLength(0);
		});
	});

	describe('checkUserPreferences', () => {
		it('should return true when no preference exists', async () => {
			const provider = (service as any).preferenceProvider;
			provider.getOne.mockResolvedValue(null);

			const result = await service.checkUserPreferences('user-123', 'system_alert', 'in_app');

			expect(result).toBe(true);
		});

		it('should return preference value when exists', async () => {
			const provider = (service as any).preferenceProvider;
			provider.getOne.mockResolvedValue({
				is_enabled: false,
			});

			const result = await service.checkUserPreferences('user-123', 'promotion', 'push');

			expect(result).toBe(false);
		});

		it('should return true on error (fail-safe)', async () => {
			const provider = (service as any).preferenceProvider;
			provider.getOne.mockRejectedValue(new Error('DB Error'));

			const result = await service.checkUserPreferences('user-123', 'system_alert', 'in_app');

			expect(result).toBe(true);
		});
	});

	describe('sendViaInApp', () => {
		it('should send via InApp successfully', async () => {
			const mockNotification = {
				id: 'notif-1',
				title: 'Test',
				body: 'Test body',
				type: 'system_alert',
				data: null,
				expired_at: null,
				created_at: new Date(),
			} as unknown as NotificationMessage;

			// Mock SSE service
			const sseService = (service as any).channelService.sseService;
			jest.spyOn(sseService, 'sendToUser').mockReturnValue(true);

			// Mock logDelivery
			jest.spyOn((service as any).channelService, 'logDelivery').mockResolvedValue(undefined);

			const result = await (service as any).channelService.sendViaInApp('user-1', mockNotification, 'receipt-2');

			expect(result).toBe(true);
			expect(sseService.sendToUser).toHaveBeenCalledWith('user-1', 'notification', {
				id: 'notif-1',
				title: 'Test',
				body: 'Test body',
				type: 'system_alert',
				data: null,
				is_read: null,
				expired_at: null,
				created_at: mockNotification.created_at,
			});
		});

		it('should handle InApp send failure', async () => {
			const mockNotification = {
				id: 'notif-1',
				title: 'Test',
				body: 'Test body',
			} as NotificationMessage;

			const sseService = (service as any).channelService.sseService;
			jest.spyOn(sseService, 'sendToUser').mockReturnValue(false);

			jest.spyOn((service as any).channelService, 'logDelivery').mockResolvedValue(undefined);

			const result = await (service as any).channelService.sendViaInApp('user-1', mockNotification, 'receipt-2');

			expect(result).toBe(false);
		});
	});

	describe('sendViaPush', () => {
		it('should send push notification successfully', async () => {
			const mockNotification = {
				id: 'notif-1',
				title: 'Test',
				body: 'Test body',
				type: 'system_alert',
			} as NotificationMessage;

			const mockDevices = [
				{
					id: 'device-1',
					push_token: 'token-123',
					one_signal_player_id: 'player-123',
				},
			];

			// Mock loadUserDevices
			jest.spyOn((service as any).channelService, 'loadUserDevices').mockResolvedValue(mockDevices);

			// Mock OneSignal
			const { default: OneSignalService } = require('#services/notification/notificationPushService');
			OneSignalService.sendNotification.mockResolvedValue({ id: 'onesignal-id' });

			// Mock logDelivery
			jest.spyOn((service as any).channelService, 'logDelivery').mockResolvedValue(undefined);

			const result = await (service as any).channelService.sendViaPush('user-1', mockNotification, 'receipt-3');

			expect(result).toHaveLength(1);
			expect(result[0]?.status).toBe('sent');
			expect(OneSignalService.sendNotification).toHaveBeenCalledWith({
				player_ids: ['player-123'],
				headings: { en: 'Test' },
				contents: { en: 'Test body' },
				data: {
					notificationId: 'notif-1',
					type: 'system_alert',
				},
			});
		});

		it('should skip devices without push token', async () => {
			const mockNotification = {
				id: 'notif-1',
				title: 'Test',
				body: 'Test body',
			} as NotificationMessage;

			const mockDevices = [
				{
					id: 'device-1',
					push_token: null,
				},
			];

			jest.spyOn((service as any).channelService, 'loadUserDevices').mockResolvedValue(mockDevices);

			const result = await (service as any).channelService.sendViaPush('user-1', mockNotification, 'receipt-3');

			expect(result).toHaveLength(1);
			expect(result[0]!.status).toBe('failed');
			expect(result[0]!.errorMessage).toBe('No OneSignal player ID');
		});
	});

	describe('sendViaZalo', () => {
		beforeEach(() => {
			// Mock environment variables
			process.env.ZALO_OA_ACCESS_TOKEN = 'test-oa-token';
			process.env.ZALO_ZNS_ACCESS_TOKEN = 'test-zns-token';
			process.env.ZALO_ZNS_TEMPLATE_ID = 'test-template-id';
		});

		it('should send Zalo OA message successfully', async () => {
			const mockNotification = {
				id: 'notif-1',
				title: 'Test',
				body: 'Test body',
			} as NotificationMessage;

			const mockDevices = [
				{
					id: 'device-1',
					platform: 'zalo_miniapp',
					device_id: 'zalo-user-123',
				},
			];

			jest.spyOn((service as any).channelService, 'loadUserDevices').mockResolvedValue(mockDevices);

			// Mock fetch for Zalo OA
			(global.fetch as jest.Mock).mockResolvedValueOnce({
				ok: true,
				json: () => Promise.resolve({ error: 0, data: { msg_id: 'zalo-msg-id' } }),
			});

			// Mock logDelivery
			jest.spyOn((service as any).channelService, 'logDelivery').mockResolvedValue(undefined);

			const result = await (service as any).channelService.sendViaZalo('user-1', mockNotification, 'receipt-4');

			expect(result).toBe(true);
			expect(global.fetch).toHaveBeenCalledWith(
				'https://openapi.zalo.me/v2.0/oa/message',
				expect.objectContaining({
					method: 'POST',
					headers: {
						'Content-Type': 'application/json',
						Authorization: 'Bearer test-oa-token',
					},
				}),
			);
		});

		it('should fail when no Zalo devices found', async () => {
			const mockNotification = {
				id: 'notif-1',
				title: 'Test',
				body: 'Test body',
			} as NotificationMessage;

			jest.spyOn((service as any).channelService, 'loadUserDevices').mockResolvedValue([]);

			const result = await (service as any).channelService.sendViaZalo('user-1', mockNotification, 'receipt-4');

			expect(result).toBe(false);
		});
	});

	describe('retryFailedNotifications', () => {
		it('should retry notifications within limit', async () => {
			const mockFailedLogs = {
				rows: [
					{
						id: 'log-1',
						receipt_id: 'receipt-1',
						status: 'failed',
						attempt_no: 1,
						receipt: {
							id: 'receipt-1',
							user_id: 'user-123',
							channel: 'push',
							message_id: 'message-1',
						},
					},
				],
			};

			const mockMessages = {
				rows: [
					{
						id: 'message-1',
						title: 'Test',
						body: 'Test body',
						type: 'system_alert',
						data: null,
					},
				],
			};

			const deliveryLogProvider = (service as any).deliveryLogProvider;
			const notificationProvider = (service as any).notificationProvider;
			deliveryLogProvider.getAll.mockResolvedValue(mockFailedLogs);
			notificationProvider.getAll.mockResolvedValue(mockMessages);
			deliveryLogProvider.put.mockResolvedValue(undefined);

			jest.spyOn(service as any, 'sendNotification').mockResolvedValue(undefined);

			const result = await service.retryFailedNotifications();

			expect(result).toBe(1);
			expect(deliveryLogProvider.put).toHaveBeenCalledWith('log-1', {
				id: 'log-1',
				attempt_no: 2,
				status: 'sent',
				sent_at: expect.any(Date),
				updated_at: expect.any(Date),
			});
		});

		it('should skip notifications exceeding retry limit', async () => {
			// When there are no failed logs, return 0
			const mockFailedLogs = {
				rows: [],
			};

			const deliveryLogProvider = (service as any).deliveryLogProvider;
			deliveryLogProvider.getAll.mockResolvedValue(mockFailedLogs);

			const result = await service.retryFailedNotifications();

			expect(result).toBe(0);

			// Verify the query was called
			expect(deliveryLogProvider.getAll).toHaveBeenCalledWith({
				where: {
					status: 'failed',
				},
				pageSize: 100,
				page: 1,
				include: [
					{
						association: 'receipt',
						attributes: ['id', 'user_id', 'channel', 'message_id'],
					},
				],
			});
		});
	});

	describe('logDelivery', () => {
		it('should create delivery log entry', async () => {
			const provider = (service as any).channelService.deliveryLogProvider;
			provider.create.mockResolvedValue(undefined);

			await (service as any).channelService.logDelivery({
				receiptId: 'receipt-1',
				deviceId: 'device-1',
				channel: 'push',
				status: 'sent',
			});

			// Verify the actual call has the right structure
			const actualCall = provider.create.mock.calls[0][0];
			expect(actualCall.receipt_id).toBe('receipt-1');
			expect(actualCall.device_id).toBe('device-1');
			expect(actualCall.channel).toBe('push');
			expect(actualCall.status).toBe('sent');
			expect(actualCall.error_message).toBeNull();
			expect(actualCall.attempt_no).toBe(1);
			expect(actualCall.sent_at).toBeInstanceOf(Date);
			expect(actualCall.delivered_at).toBeNull();
			expect(actualCall.failed_at).toBeNull();
			expect(actualCall.created_at).toBeInstanceOf(Date);
		});

		it('should include error message when status is failed', async () => {
			const provider = (service as any).channelService.deliveryLogProvider;
			provider.create.mockResolvedValue(undefined);

			await (service as any).channelService.logDelivery({
				receiptId: 'receipt-2',
				channel: 'push',
				status: 'failed',
				errorMessage: 'Test error',
			});

			// Verify the actual call has the right structure
			const actualCall = provider.create.mock.calls[0][0];
			expect(actualCall.receipt_id).toBe('receipt-2');
			expect(actualCall.channel).toBe('push');
			expect(actualCall.status).toBe('failed');
			expect(actualCall.error_message).toBe('Test error');
			expect(actualCall.attempt_no).toBe(1);
			expect(actualCall.sent_at).toBeNull();
			expect(actualCall.delivered_at).toBeNull();
			expect(actualCall.failed_at).toBeInstanceOf(Date);
		});
	});
});
