/**
 * M-03: requireSensitiveConfirm — timing-safe OTP comparison
 * Verifies correct rejection/acceptance of OTP and password confirmation.
 */

const mockRedisGet = jest.fn();
const mockRedisDelete = jest.fn();
const mockVerifyPassword = jest.fn();
const mockFindByPk = jest.fn();

jest.mock('../../../src/services/storage/redisService', () => ({
	__esModule: true,
	default: {
		getInstance: jest.fn(() => ({
			get: mockRedisGet,
			delete: mockRedisDelete,
		})),
	},
}));

jest.mock('../../../src/services/auth/passwordService', () => ({
	PasswordService: { verifyPassword: mockVerifyPassword },
}));

jest.mock('../../../src/models/User', () => ({
	User: { findByPk: mockFindByPk },
}));

jest.mock('../../../src/models/UserAuth', () => ({ UserAuth: {} }));

import { requireSensitiveConfirm } from '../../../src/middlewares/sensitive-confirm';

const makeReq = (body: Record<string, any> = {}, userId = 'user-123'): any => ({
	user: { id: userId },
	body,
	cookies: {},
});

const makeRes = (): any => {
	const res: any = {};
	res.error = jest.fn();
	return res;
};

describe('requireSensitiveConfirm (M-03: timing-safe OTP)', () => {
	beforeEach(() => {
		mockRedisGet.mockReset();
		mockRedisDelete.mockReset();
		mockVerifyPassword.mockReset();
		mockFindByPk.mockReset();
	});

	it('calls next() when OTP matches stored value', async () => {
		mockRedisGet.mockResolvedValue('123456');
		const req = makeReq({ confirm_otp: '123456' });
		const res = makeRes();
		const next = jest.fn();
		await requireSensitiveConfirm()(req, res, next);
		expect(next).toHaveBeenCalled();
		expect(res.error).not.toHaveBeenCalled();
		expect(mockRedisDelete).toHaveBeenCalled();
	});

	it('calls res.error(403) when OTP does not match', async () => {
		mockRedisGet.mockResolvedValue('111111');
		const req = makeReq({ confirm_otp: '999999' });
		const res = makeRes();
		const next = jest.fn();
		await requireSensitiveConfirm()(req, res, next);
		expect(res.error).toHaveBeenCalledWith(expect.objectContaining({ code: 403 }));
		expect(next).not.toHaveBeenCalled();
	});

	it('calls res.error(403) when OTP not in Redis (expired/missing)', async () => {
		mockRedisGet.mockResolvedValue(null);
		const req = makeReq({ confirm_otp: '123456' });
		const res = makeRes();
		const next = jest.fn();
		await requireSensitiveConfirm()(req, res, next);
		expect(res.error).toHaveBeenCalledWith(expect.objectContaining({ code: 403 }));
		expect(next).not.toHaveBeenCalled();
	});

	it('calls res.error(403) when neither confirm_password nor confirm_otp provided', async () => {
		const req = makeReq({});
		const res = makeRes();
		const next = jest.fn();
		await requireSensitiveConfirm()(req, res, next);
		expect(res.error).toHaveBeenCalledWith(expect.objectContaining({ code: 403 }));
		expect(next).not.toHaveBeenCalled();
	});

	it('calls res.error(401) when no user on request', async () => {
		const req: any = { user: undefined, body: { confirm_otp: '123456' }, cookies: {} };
		const res = makeRes();
		const next = jest.fn();
		await requireSensitiveConfirm()(req, res, next);
		expect(res.error).toHaveBeenCalledWith(expect.objectContaining({ code: 401 }));
	});
});
