const getMock = jest.fn().mockImplementationOnce((cb) => cb);
const virtualMock = jest.fn().mockImplementationOnce((name: string) => ({
  get: getMock,
}));

jest.mock('mongoose', () => ({
  Schema: jest.fn().mockImplementation((def: any, options: any) => ({
    constructor: jest.fn(),
    virtual: virtualMock,
    pre: jest.fn(),
    set: jest.fn(),
    methods: { comparePassword: jest.fn() },
    comparePassword: jest.fn(),
  })),
  SchemaTypes: jest.fn().mockImplementation(() => ({
    String: jest.fn(),
  })),
}));

import { anyFunction } from 'jest-mock-extended';
import {
  UserSchema,
  preSaveHook,
  nameGetHook,
  comparePasswordMethod,
} from './user.model';
import { hash } from 'bcrypt';
import { lastValueFrom } from 'rxjs';

describe('UserSchema', () => {
  it('should called Schame.virtual ', () => {
    expect(UserSchema).toBeDefined();

    expect(getMock).toBeCalled();
    expect(getMock).toBeCalledWith(anyFunction());
    expect(virtualMock).toBeCalled();
    expect(virtualMock).toHaveBeenNthCalledWith(1, 'name');
    expect(virtualMock).toHaveBeenNthCalledWith(2, 'posts', {
      foreignField: 'createdBy',
      localField: '_id',
      ref: 'Post',
    });
    expect(virtualMock).toBeCalledTimes(2);
  });
});

// see: https://stackoverflow.com/questions/58701700/how-do-i-test-if-statement-inside-my-mongoose-pre-save-hook
describe('preSaveHook', () => {
  test('should execute next middleware when password is not modified', async () => {
    const nextMock = jest.fn();
    const contextMock = {
      isModified: jest.fn(),
    };
    contextMock.isModified.mockReturnValueOnce(false);
    await preSaveHook.call(contextMock, nextMock);
    expect(contextMock.isModified).toBeCalledWith('password');
    expect(nextMock).toBeCalledTimes(1);
  });

  test('should set password when password is modified', async () => {
    const nextMock = jest.fn();
    const contextMock = {
      isModified: jest.fn(),
      set: jest.fn(),
      password: '123456',
    };
    contextMock.isModified.mockReturnValueOnce(true);
    await preSaveHook.call(contextMock, nextMock);
    expect(contextMock.isModified).toBeCalledWith('password');
    expect(nextMock).toBeCalledTimes(1);
    expect(contextMock.set).toBeCalledTimes(1);
  });
});

describe('nameGetHook', () => {
  test('should compute name with firstName and lastName', async () => {
    const contextMock = {
      firstName: 'Hantsy',
      lastName: 'Bai',
    };
    const name = await nameGetHook.call(contextMock);
    expect(name).toBe('Hantsy Bai');
  });
});

describe('comparePasswordMethod', () => {
  test('should be true if password is matched', async () => {
    const hashed = await hash('123456', 10);
    const contextMock = {
      password: hashed,
    };

    const result = await lastValueFrom(
      comparePasswordMethod.call(contextMock, '123456'),
    );
    expect(result).toBeTruthy();
  });

  test('should be false if password is not matched', async () => {
    const hashed = await hash('123456', 10);
    const contextMock = {
      password: hashed,
    };

    // input password is wrong
    const result = await lastValueFrom(
      comparePasswordMethod.call(contextMock, '000000'),
    );
    expect(result).toBeFalsy();
  });
});