/**
 * Unit tests for confirm-reset handler — C-01 (OTP verification)
 *
 * The handler MUST compare the submitted `code` against the stored bcrypt hash.
 * A missing or wrong code must return 400; only a correct code may reset the password.
 *
 * NOTE: jest.config.ts has restoreMocks:true which resets all jest.fn()
 * implementations between tests. We therefore re-apply ALL mock implementations
 * in beforeEach rather than relying on factory defaults.
 */

import bcrypt from 'bcryptjs';

// ── Captured mock references ─────────────────────────────────────────────────
// These are populated when jest.mock factories run (module load time).
// We keep references so we can re-apply implementations in beforeEach.

const mockTransaction = {
	commit: jest.fn(),
	rollback: jest.fn(),
};

const mockUserProviderInstance = {
	transaction: jest.fn(),
	getAll: jest.fn(),
	logError: jest.fn(),
};

const mockUserAuthProviderInstance = {
	transaction: jest.fn(),
	getAll: jest.fn(),
	put: jest.fn(),
	logError: jest.fn(),
};

const mockCodeProviderInstance = {
	getAll: jest.fn(),
	put: jest.fn(),
};

const mockHashPassword = jest.fn();
const mockValidateOtp = jest.fn();
const mockGetInstance = jest.fn();
const mockCreateAndSend = jest.fn();
const mockIsPasswordRecentlyUsed = jest.fn();
const mockSavePasswordToHistory = jest.fn();
const mockCleanupPasswordHistory = jest.fn();

jest.mock('#providers/UserProvider', () => ({
	UserProvider: jest.fn().mockImplementation(() => mockUserProviderInstance),
}));
jest.mock('#providers/UserAuthProvider', () => ({
	UserAuthProvider: jest.fn().mockImplementation(() => mockUserAuthProviderInstance),
}));
jest.mock('#providers/PasswordResetTokenProvider', () => ({
	PasswordResetTokenProvider: jest.fn().mockImplementation(() => mockCodeProviderInstance),
}));
const mockLogoutAll = jest.fn();

jest.mock('#services/auth', () => ({
	AuthService: {},
	PasswordService: {
		hashPassword: (...args: any[]) => mockHashPassword(...args),
		validatePasswordResetOtp: (...args: any[]) => mockValidateOtp(...args),
	},
	SessionService: {
		logoutAll: (...args: any[]) => mockLogoutAll(...args),
	},
}));

// Mock authUtils functions that use Sequelize models
jest.mock('#utils/authUtils', () => ({
	isPasswordRecentlyUsed: (...args: any[]) => mockIsPasswordRecentlyUsed(...args),
	savePasswordToHistory: (...args: any[]) => mockSavePasswordToHistory(...args),
	cleanupPasswordHistory: (...args: any[]) => mockCleanupPasswordHistory(...args),
}));
jest.mock('#services/notification/notificationService', () => ({
	__esModule: true,
	default: {
		getInstance: (...args: any[]) => mockGetInstance(...args),
	},
}));
jest.mock('#middlewares/query-modifier', () => ({
	queryModifier: (_req: any, _res: any, next: any) => next(),
}));
jest.mock('#middlewares/validators', () => ({
	validateZod: () => (_req: any, _res: any, next: any) => next(),
}));

// ── Helpers ──────────────────────────────────────────────────────────────────
import { createMockResponse } from '@helpers/request';

function makeReq(body: Record<string, any>) {
	return { body, query: {}, params: {}, headers: {}, cookies: {} } as any;
}

function makeRes() {
	const res = createMockResponse() as any;
	res.json = jest.fn().mockReturnThis();
	res.sendOk = jest.fn().mockReturnThis();
	res.error = jest.fn().mockReturnThis();
	return res;
}

// ── Test data ────────────────────────────────────────────────────────────────
const VALID_CODE = '482917';
const HASHED_CODE = bcrypt.hashSync(VALID_CODE, 4); // Low rounds for test speed

const MOCK_USER = { id: 'user-uuid-1', email: 'alice@example.com', username: 'alice' };
const MOCK_USER_AUTH = { id: 'userauth-uuid-1', user_id: MOCK_USER.id };
const FUTURE_DATE = new Date(Date.now() + 10 * 60 * 1000);
const PAST_DATE = new Date(Date.now() - 1);

function MOCK_RESET_CODE(overrides: Record<string, any> = {}) {
	return {
		id: 'code-uuid-1',
		user_id: MOCK_USER.id,
		token_hash: HASHED_CODE,
		used_at: null,
		expires_at: FUTURE_DATE,
		...overrides,
	};
}

// ── Load the module under test ────────────────────────────────────────────────
import confirmResetModule from '#controllers/api/v1/auth/forgot-password/confirm-reset';

function getHandler() {
	const resource = confirmResetModule({} as any);
	return (resource.post! as any).handler;
}

// ── Tests ────────────────────────────────────────────────────────────────────
describe('confirm-reset › OTP verification (C-01)', () => {
	beforeEach(() => {
		// Re-apply ALL implementations because restoreMocks:true resets them
		mockTransaction.commit.mockResolvedValue(undefined);
		mockTransaction.rollback.mockResolvedValue(undefined);
		mockUserAuthProviderInstance.transaction.mockResolvedValue(mockTransaction);
		mockUserAuthProviderInstance.getAll.mockResolvedValue({ rows: [MOCK_USER_AUTH] });
		mockUserAuthProviderInstance.put.mockResolvedValue(undefined);
		mockUserAuthProviderInstance.logError.mockResolvedValue(undefined);
		mockCodeProviderInstance.getAll.mockResolvedValue({ rows: [MOCK_RESET_CODE()] });
		mockCodeProviderInstance.put.mockResolvedValue(undefined);
		mockHashPassword.mockResolvedValue('$2b$10$hashedpassword');
		mockValidateOtp.mockResolvedValue({ user: MOCK_USER, resetCode: MOCK_RESET_CODE() });
		mockLogoutAll.mockResolvedValue(undefined);
		mockCreateAndSend.mockResolvedValue(undefined);
		mockGetInstance.mockReturnValue({ createAndSend: mockCreateAndSend });
		mockIsPasswordRecentlyUsed.mockResolvedValue(false);
		mockSavePasswordToHistory.mockResolvedValue(undefined);
		mockCleanupPasswordHistory.mockResolvedValue(undefined);
	});

	it('resets password when correct OTP is submitted', async () => {
		const req = makeReq({
			email: MOCK_USER.email,
			code: VALID_CODE,
			newPassword: 'NewPass@123',
			confirmPassword: 'NewPass@123',
		});
		const res = makeRes();

		await getHandler()(req, res);

		expect(res.json).toHaveBeenCalledWith(
			expect.objectContaining({
				success: true,
				data: null,
			}),
		);
		expect(res.error).not.toHaveBeenCalled();
		expect(mockTransaction.commit).toHaveBeenCalledTimes(1);
		expect(mockTransaction.rollback).not.toHaveBeenCalled();
	});

	it('returns 400 when OTP is wrong', async () => {
		mockValidateOtp.mockRejectedValue({ type: 'INVALID_OTP', code: 400, message: {} });
		const req = makeReq({
			email: MOCK_USER.email,
			code: '000000',
			newPassword: 'NewPass@123',
			confirmPassword: 'NewPass@123',
		});
		const res = makeRes();

		await getHandler()(req, res);

		expect(res.error).toHaveBeenCalledWith(expect.objectContaining({ code: 400, type: 'INVALID_OTP' }));
		expect(res.json).not.toHaveBeenCalled();
		expect(mockTransaction.rollback).toHaveBeenCalledTimes(1);
	});

	it('returns 400 when OTP is missing from body', async () => {
		mockValidateOtp.mockRejectedValue({ type: 'INVALID_OTP', code: 400, message: {} });
		const req = makeReq({ email: MOCK_USER.email, newPassword: 'NewPass@123', confirmPassword: 'NewPass@123' });
		const res = makeRes();

		await getHandler()(req, res);

		expect(res.error).toHaveBeenCalledWith(expect.objectContaining({ code: 400 }));
		expect(mockTransaction.rollback).toHaveBeenCalledTimes(1);
	});

	it('returns 400 when code is already used (used_at is set)', async () => {
		mockValidateOtp.mockRejectedValue({ type: 'CODE_USED', httpStatus: 400, message: {} });
		const req = makeReq({
			email: MOCK_USER.email,
			code: VALID_CODE,
			newPassword: 'NewPass@123',
			confirmPassword: 'NewPass@123',
		});
		const res = makeRes();

		await getHandler()(req, res);

		expect(res.error).toHaveBeenCalledWith(expect.objectContaining({ type: 'CODE_USED' }));
	});

	it('returns 400 when code has expired', async () => {
		mockValidateOtp.mockRejectedValue({ type: 'CODE_EXPIRED', httpStatus: 400, message: {} });
		const req = makeReq({
			email: MOCK_USER.email,
			code: VALID_CODE,
			newPassword: 'NewPass@123',
			confirmPassword: 'NewPass@123',
		});
		const res = makeRes();

		await getHandler()(req, res);

		expect(res.error).toHaveBeenCalledWith(expect.objectContaining({ type: 'CODE_EXPIRED' }));
	});

	it('returns 404 when email does not exist', async () => {
		mockValidateOtp.mockRejectedValue({ type: 'NOT_FOUND', httpStatus: 404, message: {} });
		const req = makeReq({
			email: 'nobody@example.com',
			code: VALID_CODE,
			newPassword: 'NewPass@123',
			confirmPassword: 'NewPass@123',
		});
		const res = makeRes();

		await getHandler()(req, res);

		expect(res.error).toHaveBeenCalledWith(expect.objectContaining({ httpStatus: 404 }));
	});

	it('marks code as used after successful reset', async () => {
		const req = makeReq({
			email: MOCK_USER.email,
			code: VALID_CODE,
			newPassword: 'NewPass@123',
			confirmPassword: 'NewPass@123',
		});
		const res = makeRes();

		await getHandler()(req, res);

		expect(mockCodeProviderInstance.put).toHaveBeenCalledWith(
			'code-uuid-1',
			expect.objectContaining({ used_at: expect.any(Date) }),
			expect.anything(),
		);
	});
});
