import crypto from 'crypto';
import EmailVerificationService from '#services/auth/emailVerificationService';
import { EmailVerifyToken } from '#models/EmailVerifyToken';
import { User } from '#models/User';
import { GenericError } from '#interfaces/error/generic';

// Mock the sequelize service so the test can exercise EmailVerificationService
// without booting a real database connection.
jest.mock('#services/database/sequelize/sequelizeService', () => {
	const transaction = { commit: jest.fn().mockResolvedValue(undefined), rollback: jest.fn().mockResolvedValue(undefined) };
	return {
		__esModule: true,
		default: {
			transaction: jest.fn().mockImplementation(async (fn: (tx: typeof transaction) => Promise<unknown>) => fn(transaction)),
		},
	};
});

// Mock the models — we want to control persistence outcomes from the test
// cases, not the real Sequelize layer.
jest.mock('#models/EmailVerifyToken', () => {
	const Model = jest.fn();
	(Model as unknown as { update: jest.Mock }).update = jest.fn();
	(Model as unknown as { create: jest.Mock }).create = jest.fn();
	(Model as unknown as { findOne: jest.Mock }).findOne = jest.fn();
	return { EmailVerifyToken: Model };
});
jest.mock('#models/User', () => {
	const Model = jest.fn();
	(Model as unknown as { findByPk: jest.Mock }).findByPk = jest.fn();
	return { User: Model };
});

jest.mock('#services/notification/notificationEmailService', () => ({
	__esModule: true,
	default: {
		getInstance: () => ({
			sendmail: jest.fn().mockResolvedValue({ mode: 'fallback', info: null }),
		}),
	},
}));

jest.mock('#utils/logger', () => ({
	__esModule: true,
	default: {
		info: jest.fn(),
		warn: jest.fn(),
		error: jest.fn(),
		debug: jest.fn(),
	},
}));

const mockedSequelize = jest.requireMock('#services/database/sequelize/sequelizeService').default;
const mockedEmailVerifyToken = EmailVerifyToken as unknown as {
	update: jest.Mock;
	create: jest.Mock;
	findOne: jest.Mock;
};
const mockedUser = User as unknown as { findByPk: jest.Mock };

describe('EmailVerificationService', () => {
	beforeEach(() => {
		jest.clearAllMocks();
	});

	describe('buildVerificationUrl', () => {
		it('builds a URL against Config.server.backendUrl with the token query', () => {
			const service = EmailVerificationService.getInstance();
			const url = service.buildVerificationUrl('abc123');
			expect(url).toMatch(/\/api\/v1\/auth\/verify-email\?token=abc123$/);
		});

		it('strips trailing slashes from the backend URL', () => {
			const originalBackendUrl = process.env.BACKEND_URL;
			process.env.BACKEND_URL = 'http://example.test///';
			jest.isolateModules(() => {
				// Re-import in the isolated module registry to pick up env change.
				const ServiceModule = require('#services/auth/emailVerificationService');
				const service = (ServiceModule.default.getInstance() as InstanceType<typeof ServiceModule.default>);
				const url = service.buildVerificationUrl('zzz');
				expect(url.startsWith('http://example.test/api/v1/auth/verify-email?token=zzz')).toBe(true);
			});
			process.env.BACKEND_URL = originalBackendUrl;
		});
	});

	describe('createToken', () => {
		it('persists a SHA-256 hash of the token and returns the plaintext', async () => {
			const created: Array<Record<string, unknown>> = [];
			mockedEmailVerifyToken.update.mockResolvedValueOnce([1]);
			mockedEmailVerifyToken.create.mockImplementationOnce((row: Record<string, unknown>) => {
				created.push(row);
				return Promise.resolve(row);
			});

			const service = EmailVerificationService.getInstance();
			const result = await service.createToken('user-id-1', 24);

			expect(result.token).toMatch(/^[0-9a-f]{64}$/);
			expect(result.expiresAt.getTime()).toBeGreaterThan(Date.now());
			expect(mockedEmailVerifyToken.update).toHaveBeenCalledWith(
				{ used_at: expect.any(Date) },
				{ where: { user_id: 'user-id-1', used_at: null }, transaction: expect.anything() },
			);
			expect(created).toHaveLength(1);
			const row = created[0]!;
			// hash must be the SHA-256 of the returned plaintext
			const expectedHash = crypto.createHash('sha256').update(result.token).digest('hex');
			expect(row.token_hash).toBe(expectedHash);
			expect(row.user_id).toBe('user-id-1');
		});
	});

	describe('verifyToken', () => {
		it('returns not_found when no row exists', async () => {
			mockedEmailVerifyToken.findOne.mockResolvedValueOnce(null);
			const service = EmailVerificationService.getInstance();
			const outcome = await service.verifyToken('whatever');
			expect(outcome).toEqual({ kind: 'not_found' });
		});

		it('returns used when the row already has used_at set', async () => {
			mockedEmailVerifyToken.findOne.mockResolvedValueOnce({
				used_at: new Date(),
				expires_at: new Date(Date.now() + 60_000),
				user_id: 'u-1',
			});
			const service = EmailVerificationService.getInstance();
			const outcome = await service.verifyToken('whatever');
			expect(outcome).toEqual({ kind: 'used' });
		});

		it('returns expired when the row is past its expires_at', async () => {
			mockedEmailVerifyToken.findOne.mockResolvedValueOnce({
				used_at: null,
				expires_at: new Date(Date.now() - 1_000),
				user_id: 'u-1',
			});
			const service = EmailVerificationService.getInstance();
			const outcome = await service.verifyToken('whatever');
			expect(outcome).toEqual({ kind: 'expired' });
		});

		it('returns ok and marks the user verified on a fresh token', async () => {
			const save = jest.fn().mockResolvedValue(undefined);
			const userInstance = {
				email_verified_at: null as Date | null,
				status: 'pending_verification' as string,
				save,
			};
			const record = {
				used_at: null as Date | null,
				expires_at: new Date(Date.now() + 60_000),
				user_id: 'u-2',
				save: jest.fn().mockResolvedValue(undefined),
			};
			mockedEmailVerifyToken.findOne.mockResolvedValueOnce(record);
			mockedUser.findByPk.mockResolvedValueOnce(userInstance);

			const service = EmailVerificationService.getInstance();
			const outcome = await service.verifyToken('whatever');

			expect(outcome).toEqual({ kind: 'ok', user: userInstance });
			expect(userInstance.email_verified_at).toBeInstanceOf(Date);
			expect(userInstance.status).toBe('active');
			expect(record.used_at).toBeInstanceOf(Date);
			expect(record.save).toHaveBeenCalledWith({ transaction: expect.anything() });
			expect(userInstance.save).toHaveBeenCalledWith({ transaction: expect.anything() });
			// Ensure the transaction wrapper was actually used.
			expect(mockedSequelize.transaction).toHaveBeenCalled();
		});
	});

	describe('resendVerificationEmail', () => {
		it('throws USER_ALREADY_VERIFIED for users with email_verified_at set', async () => {
			const service = EmailVerificationService.getInstance();
			const user = { id: 'u-3', email_verified_at: new Date() } as never;
			await expect(service.resendVerificationEmail(user)).rejects.toBeInstanceOf(GenericError);
		});
	});
});
