jest.mock('@librechat/data-schemas', () => ({
  logger: { info: jest.fn(), warn: jest.fn(), debug: jest.fn(), error: jest.fn() },
  DEFAULT_SESSION_EXPIRY: 900000,
  DEFAULT_REFRESH_TOKEN_EXPIRY: 604800000,
}));
jest.mock('librechat-data-provider', () => ({
  ErrorTypes: {},
  SystemRoles: { USER: 'USER', ADMIN: 'ADMIN' },
  errorsToString: jest.fn(),
}));
jest.mock('@librechat/api', () => ({
  isEnabled: jest.fn((val) => val === 'true' || val === true),
  checkEmailConfig: jest.fn(),
  isEmailDomainAllowed: jest.fn(),
  math: jest.fn((val, fallback) => (val ? Number(val) : fallback)),
  shouldUseSecureCookie: jest.fn(() => false),
  resolveAppConfigForUser: jest.fn(async (_getAppConfig, _user) => ({})),
}));
jest.mock('~/models', () => ({
  findUser: jest.fn(),
  findToken: jest.fn(),
  createUser: jest.fn(),
  updateUser: jest.fn(),
  countUsers: jest.fn(),
  getUserById: jest.fn(),
  findSession: jest.fn(),
  createToken: jest.fn(),
  deleteTokens: jest.fn(),
  deleteSession: jest.fn(),
  createSession: jest.fn(),
  generateToken: jest.fn(),
  deleteUserById: jest.fn(),
  generateRefreshToken: jest.fn(),
}));
jest.mock('~/strategies/validators', () => ({ registerSchema: { parse: jest.fn() } }));
jest.mock('~/server/services/Config', () => ({ getAppConfig: jest.fn() }));
jest.mock('~/server/utils', () => ({ sendEmail: jest.fn() }));

const {
  shouldUseSecureCookie,
  isEmailDomainAllowed,
  resolveAppConfigForUser,
} = require('@librechat/api');
const { findUser } = require('~/models');
const { getAppConfig } = require('~/server/services/Config');
const { setOpenIDAuthTokens, requestPasswordReset } = require('./AuthService');

/** Helper to build a mock Express response */
function mockResponse() {
  const cookies = {};
  const res = {
    cookie: jest.fn((name, value, options) => {
      cookies[name] = { value, options };
    }),
    _cookies: cookies,
  };
  return res;
}

/** Helper to build a mock Express request with session */
function mockRequest(sessionData = {}) {
  return {
    session: { openidTokens: null, ...sessionData },
  };
}

describe('setOpenIDAuthTokens', () => {
  const env = process.env;

  beforeEach(() => {
    jest.clearAllMocks();
    process.env = {
      ...env,
      JWT_REFRESH_SECRET: 'test-refresh-secret',
      OPENID_REUSE_TOKENS: 'true',
    };
  });

  afterAll(() => {
    process.env = env;
  });

  describe('token selection (id_token vs access_token)', () => {
    it('should return id_token when both id_token and access_token are present', () => {
      const tokenset = {
        id_token: 'the-id-token',
        access_token: 'the-access-token',
        refresh_token: 'the-refresh-token',
      };
      const req = mockRequest();
      const res = mockResponse();

      const result = setOpenIDAuthTokens(tokenset, req, res, 'user-123');
      expect(result).toBe('the-id-token');
    });

    it('should return access_token when id_token is not available', () => {
      const tokenset = {
        access_token: 'the-access-token',
        refresh_token: 'the-refresh-token',
      };
      const req = mockRequest();
      const res = mockResponse();

      const result = setOpenIDAuthTokens(tokenset, req, res, 'user-123');
      expect(result).toBe('the-access-token');
    });

    it('should return access_token when id_token is undefined', () => {
      const tokenset = {
        id_token: undefined,
        access_token: 'the-access-token',
        refresh_token: 'the-refresh-token',
      };
      const req = mockRequest();
      const res = mockResponse();

      const result = setOpenIDAuthTokens(tokenset, req, res, 'user-123');
      expect(result).toBe('the-access-token');
    });

    it('should return access_token when id_token is null', () => {
      const tokenset = {
        id_token: null,
        access_token: 'the-access-token',
        refresh_token: 'the-refresh-token',
      };
      const req = mockRequest();
      const res = mockResponse();

      const result = setOpenIDAuthTokens(tokenset, req, res, 'user-123');
      expect(result).toBe('the-access-token');
    });

    it('should return id_token even when id_token and access_token differ', () => {
      const tokenset = {
        id_token: 'id-token-jwt-signed-by-idp',
        access_token: 'opaque-graph-api-token',
        refresh_token: 'refresh-token',
      };
      const req = mockRequest();
      const res = mockResponse();

      const result = setOpenIDAuthTokens(tokenset, req, res, 'user-123');
      expect(result).toBe('id-token-jwt-signed-by-idp');
      expect(result).not.toBe('opaque-graph-api-token');
    });
  });

  describe('session token storage', () => {
    it('should store the original access_token in session (not id_token)', () => {
      const tokenset = {
        id_token: 'the-id-token',
        access_token: 'the-access-token',
        refresh_token: 'the-refresh-token',
      };
      const req = mockRequest();
      const res = mockResponse();

      setOpenIDAuthTokens(tokenset, req, res, 'user-123');

      expect(req.session.openidTokens.accessToken).toBe('the-access-token');
      expect(req.session.openidTokens.refreshToken).toBe('the-refresh-token');
    });
  });

  describe('cookie secure flag', () => {
    it('should call shouldUseSecureCookie for every cookie set', () => {
      const tokenset = {
        id_token: 'the-id-token',
        access_token: 'the-access-token',
        refresh_token: 'the-refresh-token',
      };
      const req = mockRequest();
      const res = mockResponse();

      setOpenIDAuthTokens(tokenset, req, res, 'user-123');

      // token_provider + openid_user_id (session path, so no refreshToken/openid_access_token cookies)
      const secureCalls = shouldUseSecureCookie.mock.calls.length;
      expect(secureCalls).toBeGreaterThanOrEqual(2);

      // Verify all cookies use the result of shouldUseSecureCookie
      for (const [, cookie] of Object.entries(res._cookies)) {
        expect(cookie.options.secure).toBe(false);
      }
    });

    it('should set secure: true when shouldUseSecureCookie returns true', () => {
      shouldUseSecureCookie.mockReturnValue(true);

      const tokenset = {
        id_token: 'the-id-token',
        access_token: 'the-access-token',
        refresh_token: 'the-refresh-token',
      };
      const req = mockRequest();
      const res = mockResponse();

      setOpenIDAuthTokens(tokenset, req, res, 'user-123');

      for (const [, cookie] of Object.entries(res._cookies)) {
        expect(cookie.options.secure).toBe(true);
      }
    });

    it('should use shouldUseSecureCookie for cookie fallback path (no session)', () => {
      shouldUseSecureCookie.mockReturnValue(false);

      const tokenset = {
        id_token: 'the-id-token',
        access_token: 'the-access-token',
        refresh_token: 'the-refresh-token',
      };
      const req = { session: null };
      const res = mockResponse();

      setOpenIDAuthTokens(tokenset, req, res, 'user-123');

      // In the cookie fallback path, we get: refreshToken, openid_access_token, token_provider, openid_user_id
      expect(res.cookie).toHaveBeenCalledWith(
        'refreshToken',
        expect.any(String),
        expect.objectContaining({ secure: false }),
      );
      expect(res.cookie).toHaveBeenCalledWith(
        'openid_access_token',
        expect.any(String),
        expect.objectContaining({ secure: false }),
      );
      expect(res.cookie).toHaveBeenCalledWith(
        'token_provider',
        'openid',
        expect.objectContaining({ secure: false }),
      );
    });
  });

  describe('edge cases', () => {
    it('should return undefined when tokenset is null', () => {
      const req = mockRequest();
      const res = mockResponse();
      const result = setOpenIDAuthTokens(null, req, res, 'user-123');
      expect(result).toBeUndefined();
    });

    it('should return undefined when access_token is missing', () => {
      const tokenset = { refresh_token: 'refresh' };
      const req = mockRequest();
      const res = mockResponse();
      const result = setOpenIDAuthTokens(tokenset, req, res, 'user-123');
      expect(result).toBeUndefined();
    });

    it('should return undefined when no refresh token is available', () => {
      const tokenset = { access_token: 'access', id_token: 'id' };
      const req = mockRequest();
      const res = mockResponse();
      const result = setOpenIDAuthTokens(tokenset, req, res, 'user-123');
      expect(result).toBeUndefined();
    });

    it('should use existingRefreshToken when tokenset has no refresh_token', () => {
      const tokenset = {
        id_token: 'the-id-token',
        access_token: 'the-access-token',
      };
      const req = mockRequest();
      const res = mockResponse();

      const result = setOpenIDAuthTokens(tokenset, req, res, 'user-123', 'existing-refresh');
      expect(result).toBe('the-id-token');
      expect(req.session.openidTokens.refreshToken).toBe('existing-refresh');
    });
  });
});

describe('requestPasswordReset', () => {
  beforeEach(() => {
    jest.clearAllMocks();
    isEmailDomainAllowed.mockReturnValue(true);
    getAppConfig.mockResolvedValue({
      registration: { allowedDomains: ['example.com'] },
    });
    resolveAppConfigForUser.mockResolvedValue({
      registration: { allowedDomains: ['example.com'] },
    });
  });

  it('should fast-fail with base config before DB lookup for blocked domains', async () => {
    isEmailDomainAllowed.mockReturnValue(false);

    const req = { body: { email: 'blocked@evil.com' }, ip: '127.0.0.1' };
    const result = await requestPasswordReset(req);

    expect(getAppConfig).toHaveBeenCalledWith({ baseOnly: true });
    expect(findUser).not.toHaveBeenCalled();
    expect(result).toBeInstanceOf(Error);
  });

  it('should call resolveAppConfigForUser for tenant user', async () => {
    const user = {
      _id: 'user-tenant',
      email: 'user@example.com',
      tenantId: 'tenant-x',
      role: 'USER',
    };
    findUser.mockResolvedValue(user);

    const req = { body: { email: 'user@example.com' }, ip: '127.0.0.1' };
    await requestPasswordReset(req);

    expect(resolveAppConfigForUser).toHaveBeenCalledWith(getAppConfig, user);
  });

  it('should reuse baseConfig for non-tenant user without calling resolveAppConfigForUser', async () => {
    findUser.mockResolvedValue({ _id: 'user-no-tenant', email: 'user@example.com' });

    const req = { body: { email: 'user@example.com' }, ip: '127.0.0.1' };
    await requestPasswordReset(req);

    expect(resolveAppConfigForUser).not.toHaveBeenCalled();
  });

  it('should return generic response when tenant config blocks the domain (non-enumerable)', async () => {
    const user = {
      _id: 'user-tenant',
      email: 'user@example.com',
      tenantId: 'tenant-x',
      role: 'USER',
    };
    findUser.mockResolvedValue(user);
    isEmailDomainAllowed.mockReturnValueOnce(true).mockReturnValueOnce(false);

    const req = { body: { email: 'user@example.com' }, ip: '127.0.0.1' };
    const result = await requestPasswordReset(req);

    expect(result).not.toBeInstanceOf(Error);
    expect(result.message).toContain('If an account with that email exists');
  });
});
