import moment from 'moment';
import { v4 as generateUUID } from 'uuid';
import {
  generateRes,
  generateReq,
  generateTodo,
  generateDaily,
} from '../../../helpers/api-unit.helper';
import cronMiddleware from '../../../../website/server/middlewares/cron';
import { model as User } from '../../../../website/server/models/user';
import { model as Group } from '../../../../website/server/models/group';
import * as Tasks from '../../../../website/server/models/task';
import * as analyticsService from '../../../../website/server/libs/analyticsService';
import * as cronLib from '../../../../website/server/libs/cron';

const CRON_TIMEOUT_WAIT = new Date(60 * 60 * 1000).getTime();
const CRON_TIMEOUT_UNIT = new Date(60 * 1000).getTime();

describe('cron middleware', () => {
  let res; let
    req;
  let user;

  beforeEach(async () => {
    res = generateRes();
    req = generateReq();
    user = await res.locals.user.save();
    res.analytics = analyticsService;
  });

  afterEach(() => {
    sandbox.restore();
  });

  it('calls next when user is not attached', done => {
    res.locals.user = null;
    cronMiddleware(req, res, done);
  });

  it('calls next when days have not been missed', done => {
    cronMiddleware(req, res, done);
  });

  it('should clear todos older than 30 days for free users', async () => {
    user.lastCron = moment(new Date()).subtract({ days: 2 });
    const task = generateTodo(user);
    task.dateCompleted = moment(new Date()).subtract({ days: 31 });
    task.completed = true;
    await task.save();
    await user.save();

    await new Promise((resolve, reject) => {
      cronMiddleware(req, res, err => {
        if (err) return reject(err);

        Tasks.Task.findOne({ _id: task }).then(foundTask => {
          expect(foundTask).to.not.exist;
          resolve();
        });

        return null;
      });
    });
  });

  it('should not clear todos older than 30 days for subscribed users', async () => {
    user.purchased.plan.customerId = 'subscribedId';
    user.purchased.plan.dateUpdated = moment('012013', 'MMYYYY');
    user.lastCron = moment(new Date()).subtract({ days: 2 });
    const task = generateTodo(user);
    task.dateCompleted = moment(new Date()).subtract({ days: 31 });
    task.completed = true;
    await task.save();
    await user.save();

    await new Promise((resolve, reject) => {
      cronMiddleware(req, res, err => {
        if (err) return reject(err);
        Tasks.Task.findOne({ _id: task }).then(foundTask => {
          expect(foundTask).to.exist;
          return resolve();
        });
        return null;
      });
    });
  });

  it('should clear todos older than 90 days for subscribed users', async () => {
    user.purchased.plan.customerId = 'subscribedId';
    user.purchased.plan.dateUpdated = moment('012013', 'MMYYYY');
    user.lastCron = moment(new Date()).subtract({ days: 2 });

    const task = generateTodo(user);
    task.dateCompleted = moment(new Date()).subtract({ days: 91 });
    task.completed = true;
    await task.save();
    await user.save();

    await new Promise((resolve, reject) => {
      cronMiddleware(req, res, err => {
        if (err) return reject(err);
        Tasks.Task.findOne({ _id: task }).then(foundTask => {
          expect(foundTask).to.not.exist;
          return resolve();
        });
        return null;
      });
    });
  });

  it('should call next if user was not modified after cron', async () => {
    const hpBefore = user.stats.hp;
    user.lastCron = moment(new Date()).subtract({ days: 2 });
    await user.save();

    await new Promise((resolve, reject) => {
      cronMiddleware(req, res, err => {
        if (err) return reject(err);
        expect(hpBefore).to.equal(user.stats.hp);
        return resolve();
      });
    });
  });

  it('runs cron if previous cron was incomplete', async () => {
    user.lastCron = moment(new Date()).subtract({ days: 1 });
    user.auth.timestamps.loggedin = moment(new Date()).subtract({ days: 4 });
    const now = new Date();
    await user.save();

    await new Promise((resolve, reject) => {
      cronMiddleware(req, res, err => {
        if (err) return reject(err);
        expect(moment(now).isSame(user.lastCron, 'day'));
        expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day'));
        return resolve();
      });
    });
  });

  it('updates user.auth.timestamps.loggedin and lastCron', async () => {
    user.lastCron = moment(new Date()).subtract({ days: 2 });
    const now = new Date();
    await user.save();

    await new Promise((resolve, reject) => {
      cronMiddleware(req, res, err => {
        if (err) return reject(err);
        expect(moment(now).isSame(user.lastCron, 'day'));
        expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day'));
        return resolve();
      });
    });
  });

  it('does damage for missing dailies', async () => {
    const hpBefore = user.stats.hp;
    user.lastCron = moment(new Date()).subtract({ days: 2 });
    const daily = generateDaily(user);
    daily.startDate = moment(new Date()).subtract({ days: 2 });
    await daily.save();
    await user.save();

    await new Promise((resolve, reject) => {
      cronMiddleware(req, res, err => {
        if (err) return reject(err);
        return User.findOne({ _id: user._id }).then(updatedUser => {
          expect(updatedUser.stats.hp).to.be.lessThan(hpBefore);
          return resolve();
        });
      });
    });
  });

  it('updates tasks', async () => {
    user.lastCron = moment(new Date()).subtract({ days: 2 });
    const todo = generateTodo(user);
    const todoValueBefore = todo.value;
    await Promise.all([todo.save(), user.save()]);

    await new Promise((resolve, reject) => {
      cronMiddleware(req, res, err => {
        if (err) return reject(err);
        return Tasks.Task.findOne({ _id: todo._id }).then(todoFound => {
          expect(todoFound.value).to.be.lessThan(todoValueBefore);
          return resolve();
        });
      });
    });
  });

  it('applies quest progress', async () => {
    const hpBefore = user.stats.hp;
    user.lastCron = moment(new Date()).subtract({ days: 2 });
    const daily = generateDaily(user);
    daily.startDate = moment(new Date()).subtract({ days: 2 });
    await daily.save();

    const questKey = 'dilatory';
    user.party.quest.key = questKey;

    const party = new Group({
      type: 'party',
      name: generateUUID(),
      leader: user._id,
    });
    party.quest.members[user._id] = true;
    party.quest.key = questKey;
    await party.save();

    user.party._id = party._id;
    await user.save();

    party.startQuest(user);

    await new Promise((resolve, reject) => {
      cronMiddleware(req, res, err => {
        if (err) return reject(err);
        return User.findOne({ _id: user._id }).then(updatedUser => {
          expect(updatedUser.stats.hp).to.be.lessThan(hpBefore);
          return resolve();
        });
      });
    });
  });

  it('recovers from failed cron and does not error when user is already cronning', async () => {
    user.lastCron = moment(new Date()).subtract({ days: 2 });
    await user.save();

    const updatedUser = user.toObject();
    updatedUser.matchedCount = 0;

    sandbox.spy(cronLib, 'recoverCron');

    sandbox.stub(User, 'updateOne')
      .withArgs({
        _id: user._id,
        $or: [
          { _cronSignature: 'NOT_RUNNING' },
          { _cronSignature: { $lt: sinon.match.number } },
        ],
      })
      .returns({
        exec () {
          return Promise.resolve(updatedUser);
        },
      });

    await new Promise((resolve, reject) => {
      cronMiddleware(req, res, err => {
        if (err) return reject(err);
        expect(cronLib.recoverCron).to.be.calledOnce;

        return resolve();
      });
    });
  });

  it('cronSignature less than an hour ago should error', async () => {
    user.lastCron = moment(new Date()).subtract({ days: 2 });
    const now = new Date();
    await User.updateOne({
      _id: user._id,
    }, {
      $set: {
        _cronSignature: now.getTime() - CRON_TIMEOUT_WAIT + CRON_TIMEOUT_UNIT,
      },
    }).exec();
    await user.save();
    const expectedErrMessage = `Impossible to recover from cron for user ${user._id}.`;

    await new Promise((resolve, reject) => {
      cronMiddleware(req, res, err => {
        if (!err) return reject(new Error('Cron should have failed.'));
        expect(err.message).to.be.equal(expectedErrMessage);
        return resolve();
      });
    });
  });

  it('cronSignature longer than an hour ago should allow cron', async () => {
    user.lastCron = moment(new Date()).subtract({ days: 2 });
    const now = new Date();
    await User.updateOne({
      _id: user._id,
    }, {
      $set: {
        _cronSignature: now.getTime() - CRON_TIMEOUT_WAIT - CRON_TIMEOUT_UNIT,
      },
    }).exec();
    await user.save();

    await new Promise((resolve, reject) => {
      cronMiddleware(req, res, err => {
        if (err) return reject(err);
        expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day'));
        expect(user._cronSignature).to.be.equal('NOT_RUNNING');
        return resolve();
      });
    });
  });

  it('cron should not run more than once', async () => {
    user.lastCron = moment(new Date()).subtract({ days: 2 });
    await user.save();

    sandbox.spy(cronLib, 'cron');

    await Promise.all([new Promise((resolve, reject) => {
      cronMiddleware(req, res, err => {
        if (err) return reject(err);
        return resolve();
      });
    }), new Promise((resolve, reject) => {
      cronMiddleware(req, res, err => {
        if (err) return reject(err);
        return resolve();
      });
    }), new Promise((resolve, reject) => {
      setTimeout(() => {
        cronMiddleware(req, res, err => {
          if (err) return reject(err);
          return resolve();
        });
      }, 400);
    }),
    ]);

    expect(cronLib.cron).to.be.calledOnce;
  });
});
