const jwtDecode = require('jsonwebtoken/decode');
const { Issuer, Strategy: OpenIDStrategy } = require('openid-client');
const mongoose = require('mongoose');
const { MongoMemoryServer } = require('mongodb-memory-server');
const User = require('~/models/User');
const setupOpenId = require('./openidStrategy');

jest.mock('jsonwebtoken/decode');
jest.mock('openid-client');

jest.mock('~/server/services/Files/strategies', () => ({
  getStrategyFunctions: jest.fn(() => ({
    saveBuffer: jest.fn(),
  })),
}));

Issuer.discover = jest.fn().mockResolvedValue({
  Client: jest.fn(),
});

describe('setupOpenId', () => {
  const OLD_ENV = process.env;
  describe('OpenIDStrategy', () => {
    let validateFn, mongoServer;

    beforeAll(async () => {
      mongoServer = await MongoMemoryServer.create();
      const mongoUri = mongoServer.getUri();
      await mongoose.connect(mongoUri);
    });

    afterAll(async () => {
      process.env = OLD_ENV;
      await mongoose.disconnect();
      await mongoServer.stop();
    });

    beforeEach(async () => {
      jest.clearAllMocks();
      await User.deleteMany({});
      process.env = {
        ...OLD_ENV,
        OPENID_ISSUER: 'https://fake-issuer.com',
        OPENID_CLIENT_ID: 'fake_client_id',
        OPENID_CLIENT_SECRET: 'fake_client_secret',
        DOMAIN_SERVER: 'https://example.com',
        OPENID_CALLBACK_URL: '/callback',
        OPENID_SCOPE: 'openid profile email',
        OPENID_REQUIRED_ROLE: 'requiredRole',
        OPENID_REQUIRED_ROLE_PARAMETER_PATH: 'roles',
        OPENID_REQUIRED_ROLE_TOKEN_KIND: 'id',
      };

      jwtDecode.mockReturnValue({
        roles: ['requiredRole'],
      });

      //call setup so we can grab a reference to the validate function
      await setupOpenId();
      validateFn = OpenIDStrategy.mock.calls[0][1];
    });

    const tokenset = {
      id_token: 'fake_id_token',
    };

    const userinfo = {
      sub: '1234',
      email: 'test@example.com',
      email_verified: true,
      given_name: 'First',
      family_name: 'Last',
      name: 'My Full',
      username: 'flast',
    };

    const userModel = {
      openidId: userinfo.sub,
      email: userinfo.email,
    };

    it('should set username correctly for a new user when username claim exists', async () => {
      const expectUsername = userinfo.username.toLowerCase();
      await validateFn(tokenset, userinfo, (err, user) => {
        expect(err).toBe(null);
        expect(user.username).toBe(expectUsername);
      });

      await expect(User.exists({ username: expectUsername })).resolves.not.toBeNull();
    });

    it('should set username correctly for a new user when given_name claim exists, but username does not', async () => {
      let userinfo_modified = { ...userinfo };
      delete userinfo_modified.username;
      const expectUsername = userinfo.given_name.toLowerCase();

      await validateFn(tokenset, userinfo_modified, (err, user) => {
        expect(err).toBe(null);
        expect(user.username).toBe(expectUsername);
      });
      await expect(User.exists({ username: expectUsername })).resolves.not.toBeNull();
    });

    it('should set username correctly for a new user when email claim exists, but username and given_name do not', async () => {
      let userinfo_modified = { ...userinfo };
      delete userinfo_modified.username;
      delete userinfo_modified.given_name;
      const expectUsername = userinfo.email.toLowerCase();

      await validateFn(tokenset, userinfo_modified, (err, user) => {
        expect(err).toBe(null);
        expect(user.username).toBe(expectUsername);
      });
      await expect(User.exists({ username: expectUsername })).resolves.not.toBeNull();
    });

    it('should set username correctly for a new user when using OPENID_USERNAME_CLAIM', async () => {
      process.env.OPENID_USERNAME_CLAIM = 'sub';
      const expectUsername = userinfo.sub.toLowerCase();

      await validateFn(tokenset, userinfo, (err, user) => {
        expect(err).toBe(null);
        expect(user.username).toBe(expectUsername);
      });
      await expect(User.exists({ username: expectUsername })).resolves.not.toBeNull();
    });

    it('should set name correctly for a new user with first and last names', async () => {
      const expectName = userinfo.given_name + ' ' + userinfo.family_name;
      await validateFn(tokenset, userinfo, (err, user) => {
        expect(err).toBe(null);
        expect(user.name).toBe(expectName);
      });
      await expect(User.exists({ name: expectName })).resolves.not.toBeNull();
    });

    it('should set name correctly for a new user using OPENID_NAME_CLAIM', async () => {
      const expectName = 'Custom Name';
      process.env.OPENID_NAME_CLAIM = 'name';
      let userinfo_modified = { ...userinfo, name: expectName };

      await validateFn(tokenset, userinfo_modified, (err, user) => {
        expect(err).toBe(null);
        expect(user.name).toBe(expectName);
      });
      await expect(User.exists({ name: expectName })).resolves.not.toBeNull();
    });

    it('should should update existing user after login', async () => {
      const expectUsername = userinfo.username.toLowerCase();
      await User.create(userModel);

      await validateFn(tokenset, userinfo, (err) => {
        expect(err).toBe(null);
      });
      const newUser = await User.findOne({ openidId: userModel.openidId });
      await expect(newUser.provider).toBe('openid');
      await expect(newUser.username).toBe(expectUsername);
      await expect(newUser.name).toBe(userinfo.given_name + ' ' + userinfo.family_name);
    });

    it('should should enforce required role', async () => {
      jwtDecode.mockReturnValue({
        roles: ['SomeOtherRole'],
      });
      await validateFn(tokenset, userinfo, (err, user, details) => {
        expect(err).toBe(null);
        expect(user).toBe(false);
        expect(details.message).toBe(
          'You must have the "' + process.env.OPENID_REQUIRED_ROLE + '" role to log in.',
        );
      });
    });
  });
});
