const mockGetUserById = jest.fn();
const mockUpdateUser = jest.fn();
const mockVerifyOTPOrBackupCode = jest.fn();
const mockGenerateTOTPSecret = jest.fn();
const mockGenerateBackupCodes = jest.fn();
const mockEncryptV3 = jest.fn();

jest.mock('@librechat/data-schemas', () => ({
  encryptV3: (...args) => mockEncryptV3(...args),
  logger: { error: jest.fn() },
}));

jest.mock('~/server/services/twoFactorService', () => ({
  verifyOTPOrBackupCode: (...args) => mockVerifyOTPOrBackupCode(...args),
  generateBackupCodes: (...args) => mockGenerateBackupCodes(...args),
  generateTOTPSecret: (...args) => mockGenerateTOTPSecret(...args),
  verifyBackupCode: jest.fn(),
  getTOTPSecret: jest.fn(),
  verifyTOTP: jest.fn(),
}));

jest.mock('~/models', () => ({
  getUserById: (...args) => mockGetUserById(...args),
  updateUser: (...args) => mockUpdateUser(...args),
}));

const { enable2FA, regenerateBackupCodes } = require('~/server/controllers/TwoFactorController');

function createRes() {
  const res = {};
  res.status = jest.fn().mockReturnValue(res);
  res.json = jest.fn().mockReturnValue(res);
  return res;
}

const PLAIN_CODES = ['code1', 'code2', 'code3'];
const CODE_OBJECTS = [
  { codeHash: 'h1', used: false, usedAt: null },
  { codeHash: 'h2', used: false, usedAt: null },
  { codeHash: 'h3', used: false, usedAt: null },
];

beforeEach(() => {
  jest.clearAllMocks();
  mockGenerateTOTPSecret.mockReturnValue('NEWSECRET');
  mockGenerateBackupCodes.mockResolvedValue({ plainCodes: PLAIN_CODES, codeObjects: CODE_OBJECTS });
  mockEncryptV3.mockReturnValue('encrypted-secret');
});

describe('enable2FA', () => {
  it('allows first-time setup without token — writes to pending fields', async () => {
    const req = { user: { id: 'user1' }, body: {} };
    const res = createRes();
    mockGetUserById.mockResolvedValue({ _id: 'user1', twoFactorEnabled: false, email: 'a@b.com' });
    mockUpdateUser.mockResolvedValue({ email: 'a@b.com' });

    await enable2FA(req, res);

    expect(res.status).toHaveBeenCalledWith(200);
    expect(res.json).toHaveBeenCalledWith(
      expect.objectContaining({ otpauthUrl: expect.any(String), backupCodes: PLAIN_CODES }),
    );
    expect(mockVerifyOTPOrBackupCode).not.toHaveBeenCalled();
    const updateCall = mockUpdateUser.mock.calls[0][1];
    expect(updateCall).toHaveProperty('pendingTotpSecret', 'encrypted-secret');
    expect(updateCall).toHaveProperty('pendingBackupCodes', CODE_OBJECTS);
    expect(updateCall).not.toHaveProperty('twoFactorEnabled');
    expect(updateCall).not.toHaveProperty('totpSecret');
    expect(updateCall).not.toHaveProperty('backupCodes');
  });

  it('re-enrollment writes to pending fields, leaving live 2FA intact', async () => {
    const req = { user: { id: 'user1' }, body: { token: '123456' } };
    const res = createRes();
    const existingUser = {
      _id: 'user1',
      twoFactorEnabled: true,
      totpSecret: 'enc-secret',
      email: 'a@b.com',
    };
    mockGetUserById.mockResolvedValue(existingUser);
    mockVerifyOTPOrBackupCode.mockResolvedValue({ verified: true });
    mockUpdateUser.mockResolvedValue({ email: 'a@b.com' });

    await enable2FA(req, res);

    expect(mockVerifyOTPOrBackupCode).toHaveBeenCalledWith({
      user: existingUser,
      token: '123456',
      backupCode: undefined,
      persistBackupUse: false,
    });
    expect(res.status).toHaveBeenCalledWith(200);
    const updateCall = mockUpdateUser.mock.calls[0][1];
    expect(updateCall).toHaveProperty('pendingTotpSecret', 'encrypted-secret');
    expect(updateCall).toHaveProperty('pendingBackupCodes', CODE_OBJECTS);
    expect(updateCall).not.toHaveProperty('twoFactorEnabled');
    expect(updateCall).not.toHaveProperty('totpSecret');
  });

  it('allows re-enrollment with valid backup code (persistBackupUse: false)', async () => {
    const req = { user: { id: 'user1' }, body: { backupCode: 'backup123' } };
    const res = createRes();
    const existingUser = {
      _id: 'user1',
      twoFactorEnabled: true,
      totpSecret: 'enc-secret',
      email: 'a@b.com',
    };
    mockGetUserById.mockResolvedValue(existingUser);
    mockVerifyOTPOrBackupCode.mockResolvedValue({ verified: true });
    mockUpdateUser.mockResolvedValue({ email: 'a@b.com' });

    await enable2FA(req, res);

    expect(mockVerifyOTPOrBackupCode).toHaveBeenCalledWith(
      expect.objectContaining({ persistBackupUse: false }),
    );
    expect(res.status).toHaveBeenCalledWith(200);
  });

  it('returns error when no token provided and 2FA is enabled', async () => {
    const req = { user: { id: 'user1' }, body: {} };
    const res = createRes();
    mockGetUserById.mockResolvedValue({
      _id: 'user1',
      twoFactorEnabled: true,
      totpSecret: 'enc-secret',
    });
    mockVerifyOTPOrBackupCode.mockResolvedValue({ verified: false, status: 400 });

    await enable2FA(req, res);

    expect(res.status).toHaveBeenCalledWith(400);
    expect(mockUpdateUser).not.toHaveBeenCalled();
  });

  it('returns 401 when invalid token provided and 2FA is enabled', async () => {
    const req = { user: { id: 'user1' }, body: { token: 'wrong' } };
    const res = createRes();
    mockGetUserById.mockResolvedValue({
      _id: 'user1',
      twoFactorEnabled: true,
      totpSecret: 'enc-secret',
    });
    mockVerifyOTPOrBackupCode.mockResolvedValue({
      verified: false,
      status: 401,
      message: 'Invalid token or backup code',
    });

    await enable2FA(req, res);

    expect(res.status).toHaveBeenCalledWith(401);
    expect(res.json).toHaveBeenCalledWith({ message: 'Invalid token or backup code' });
    expect(mockUpdateUser).not.toHaveBeenCalled();
  });
});

describe('regenerateBackupCodes', () => {
  it('returns 404 when user not found', async () => {
    const req = { user: { id: 'user1' }, body: {} };
    const res = createRes();
    mockGetUserById.mockResolvedValue(null);

    await regenerateBackupCodes(req, res);

    expect(res.status).toHaveBeenCalledWith(404);
    expect(res.json).toHaveBeenCalledWith({ message: 'User not found' });
  });

  it('requires OTP when 2FA is enabled', async () => {
    const req = { user: { id: 'user1' }, body: { token: '123456' } };
    const res = createRes();
    mockGetUserById.mockResolvedValue({
      _id: 'user1',
      twoFactorEnabled: true,
      totpSecret: 'enc-secret',
    });
    mockVerifyOTPOrBackupCode.mockResolvedValue({ verified: true });
    mockUpdateUser.mockResolvedValue({});

    await regenerateBackupCodes(req, res);

    expect(mockVerifyOTPOrBackupCode).toHaveBeenCalled();
    expect(res.status).toHaveBeenCalledWith(200);
    expect(res.json).toHaveBeenCalledWith({
      backupCodes: PLAIN_CODES,
      backupCodesHash: CODE_OBJECTS,
    });
  });

  it('returns error when no token provided and 2FA is enabled', async () => {
    const req = { user: { id: 'user1' }, body: {} };
    const res = createRes();
    mockGetUserById.mockResolvedValue({
      _id: 'user1',
      twoFactorEnabled: true,
      totpSecret: 'enc-secret',
    });
    mockVerifyOTPOrBackupCode.mockResolvedValue({ verified: false, status: 400 });

    await regenerateBackupCodes(req, res);

    expect(res.status).toHaveBeenCalledWith(400);
  });

  it('returns 401 when invalid token provided and 2FA is enabled', async () => {
    const req = { user: { id: 'user1' }, body: { token: 'wrong' } };
    const res = createRes();
    mockGetUserById.mockResolvedValue({
      _id: 'user1',
      twoFactorEnabled: true,
      totpSecret: 'enc-secret',
    });
    mockVerifyOTPOrBackupCode.mockResolvedValue({
      verified: false,
      status: 401,
      message: 'Invalid token or backup code',
    });

    await regenerateBackupCodes(req, res);

    expect(res.status).toHaveBeenCalledWith(401);
    expect(res.json).toHaveBeenCalledWith({ message: 'Invalid token or backup code' });
  });

  it('includes backupCodesHash in response', async () => {
    const req = { user: { id: 'user1' }, body: { token: '123456' } };
    const res = createRes();
    mockGetUserById.mockResolvedValue({
      _id: 'user1',
      twoFactorEnabled: true,
      totpSecret: 'enc-secret',
    });
    mockVerifyOTPOrBackupCode.mockResolvedValue({ verified: true });
    mockUpdateUser.mockResolvedValue({});

    await regenerateBackupCodes(req, res);

    const responseBody = res.json.mock.calls[0][0];
    expect(responseBody).toHaveProperty('backupCodesHash', CODE_OBJECTS);
    expect(responseBody).toHaveProperty('backupCodes', PLAIN_CODES);
  });

  it('allows regeneration without token when 2FA is not enabled', async () => {
    const req = { user: { id: 'user1' }, body: {} };
    const res = createRes();
    mockGetUserById.mockResolvedValue({
      _id: 'user1',
      twoFactorEnabled: false,
    });
    mockUpdateUser.mockResolvedValue({});

    await regenerateBackupCodes(req, res);

    expect(mockVerifyOTPOrBackupCode).not.toHaveBeenCalled();
    expect(res.status).toHaveBeenCalledWith(200);
    expect(res.json).toHaveBeenCalledWith({
      backupCodes: PLAIN_CODES,
      backupCodesHash: CODE_OBJECTS,
    });
  });
});
