const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');

describe('workerTimeControl clockIn()', () => {
    const workerId = 9;
    const salesBossId = 19;
    const hankPymId = 1107;
    const jessicaJonesId = 1110;
    const HHRRId = 37;
    const teamBossId = 13;
    const monday = 1;
    const tuesday = 2;
    const thursday = 4;
    const friday = 5;
    const sunday = 7;
    const inTime = '2001-01-01T00:00:00.000Z';
    const activeCtx = {
        accessToken: {userId: 50},
    };
    const ctx = {req: activeCtx};

    beforeAll(() => {
        spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
            active: activeCtx
        });
    });

    it('should correctly clock in', async() => {
        const tx = await models.WorkerTimeControl.beginTransaction({});

        try {
            const options = {transaction: tx};
            await models.WorkerTimeControl.clockIn(workerId, inTime, 'in', 'test', options);
            const isClockIn = await models.WorkerTimeControl.findOne({
                where: {
                    userFk: workerId
                }
            }, options);

            expect(isClockIn).toBeDefined();
            expect(isClockIn.direction).toBe('in');
            await tx.rollback();
        } catch (e) {
            await tx.rollback();
            throw e;
        }
    });
    
    it('should throw an error trying to change a middle hour to out not resting 12h', async() => {
        activeCtx.accessToken.userId = HHRRId;
        const workerId = teamBossId;

        const tx = await models.WorkerTimeControl.beginTransaction({});
        try {
            const options = {transaction: tx};

            const entryTime = "2000-12-25T11:00:00.000Z";
            ctx.args = {timed: entryTime, direction: 'in'};
            await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);

            const middleTime ="2000-12-26T11:00:00.000Z";
            ctx.args = {timed: middleTime, direction: 'middle'};
            const middleEntryTime = await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
            
            const direction = 'out';
            await models.WorkerTimeControl.updateTimeEntry(ctx, middleEntryTime.id, direction, options);
            await tx.rollback();
        } catch (e) {
            expect(e.message).toBe('Superado el tiempo máximo entre entrada y salida');
            await tx.rollback();
        }
    });

    it('should updates the time entry direction and remaining not be manual', async() => {
        activeCtx.accessToken.userId = HHRRId;
        const workerId = teamBossId;

        const tx = await models.WorkerTimeControl.beginTransaction({});
        try {
            const options = {transaction: tx};

            const entryTime = "2000-12-25T11:00:00.000Z";
            ctx.args = {timed: entryTime, direction: 'in'};
            await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);

            const middleTime ="2000-12-25T16:00:00.000Z";
            ctx.args = {timed: middleTime, direction: 'middle'};
            const middleEntryTime = await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
            middleEntryTime.updateAttribute('manual', false);
            
            const direction = 'out';
            const outTimeEntryId = await models.WorkerTimeControl.updateTimeEntry(ctx, middleEntryTime.id, direction, options);

            const outTimeEntry = await models.WorkerTimeControl.findById(outTimeEntryId, null, options);
            expect(outTimeEntry.manual).toBeFalsy();
            await tx.rollback();
        } catch (e) {
            await tx.rollback();
        }
    });

    it('should throw an error trying to add an "in" entry if the last clockIn is not out', async() => {
        activeCtx.accessToken.userId = HHRRId;
        const workerId = teamBossId;
         const tx = await models.WorkerTimeControl.beginTransaction({});
        try {
            const options = {transaction: tx};

            ctx.args = {timed: "2000-12-25T21:00:00.000Z", direction: 'in'};
            await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);

            ctx.args = {timed: "2000-12-25T22:00:00.000Z", direction: 'middle'};
            await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
            
            ctx.args = {timed: "2000-12-25T22:30:00.000Z", direction: 'middle'};
            await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
            
            ctx.args = {timed: "2000-12-26T01:00:00.000Z", direction: 'in'};
            await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);

            await tx.rollback();
        } catch (e) {
            expect(e.message).toBe('Dirección incorrecta');
            await tx.rollback();
        }
    });

    describe('as Role errors', () => {
        it('should add if the current user is team boss and the target user is himself', async() => {
            activeCtx.accessToken.userId = teamBossId;
            const workerId = teamBossId;

            const tx = await models.WorkerTimeControl.beginTransaction({});
            try {
                const options = {transaction: tx};

                const todayAtOne = Date.vnNew();
                todayAtOne.setHours(1, 0, 0, 0);

                ctx.args = {timed: todayAtOne, direction: 'in'};
                const createdTimeEntry = await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);

                expect(createdTimeEntry.id).toBeDefined();

                await tx.rollback();
            } catch (e) {
                await tx.rollback();
                throw e;
            }
        });

        it('should delete the created time entry for the team boss as himself', async() => {
            activeCtx.accessToken.userId = teamBossId;
            const workerId = teamBossId;

            const tx = await models.WorkerTimeControl.beginTransaction({});
            try {
                const options = {transaction: tx};

                const todayAtOne = Date.vnNew();
                todayAtOne.setHours(1, 0, 0, 0);

                ctx.args = {timed: todayAtOne, direction: 'in'};
                const createdTimeEntry = await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);

                expect(createdTimeEntry.id).toBeDefined();

                await models.WorkerTimeControl.deleteTimeEntry(ctx, createdTimeEntry.id, options);

                const deletedTimeEntry = await models.WorkerTimeControl.findById(createdTimeEntry.id, null, options);

                expect(deletedTimeEntry).toBeNull();
                await tx.rollback();
            } catch (e) {
                await tx.rollback();
                throw e;
            }
        });

        it('should edit the created time entry for the team boss as HHRR', async() => {
            activeCtx.accessToken.userId = HHRRId;
            const workerId = teamBossId;

            const tx = await models.WorkerTimeControl.beginTransaction({});
            try {
                const options = {transaction: tx};

                const todayAtOne = Date.vnNew();
                todayAtOne.setHours(1, 0, 0, 0);

                ctx.args = {timed: todayAtOne, direction: 'in'};
              const entryTime = await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
                expect(entryTime.id).toBeDefined();
                const todayAtTwo = Date.vnNew();

                todayAtTwo.setHours(2, 0, 0, 0);
                ctx.args = {timed: todayAtTwo, direction: 'middle'};
                const middleTime = await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
                
                const direction = 'out';
                const outTimeEntryId = await models.WorkerTimeControl.updateTimeEntry(
                    ctx, middleTime.id, direction, options
                );

                const {direction: updatedDirection} = await models.WorkerTimeControl.findById(outTimeEntryId,{fields:['direction']},options);
                expect(updatedDirection).toEqual('out');
                
                await tx.rollback();
            } catch (e) {
                await tx.rollback();
                throw e;
            }
        });
    });

    describe('as saleBoss editor', () => {
        let workerId;
        beforeEach(() => {
            activeCtx.accessToken.userId = salesBossId;
            workerId = hankPymId;
        });

        it('should fail to add a time entry if the target user has an absence that day', async() => {
            const date = Date.vnNew();
            date.setHours(8, 0, 0);
            date.setDate(date.getDate() - 16);
            const tx = await models.WorkerTimeControl.beginTransaction({});
            const options = {transaction: tx};
            try {
                ctx.args = {timed: date, direction: 'in'};
                await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);

                await tx.rollback();
            } catch (e) {
                await tx.rollback();
                error = e;
            }

            expect(error.message).toBe(`No está permitido trabajar`);
        });

        it('should fail to add a time entry for a worker without an existing contract', async() => {
            const date = Date.vnNew();
            date.setFullYear(date.getFullYear() - 2);

            const tx = await models.WorkerTimeControl.beginTransaction({});
            const options = {transaction: tx};
            try {
                ctx.args = {timed: date, direction: 'in'};
                await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);

                await tx.rollback();
            } catch (e) {
                await tx.rollback();
                error = e;
            }

            expect(error.message).toBe(`No hay un contrato en vigor`);
        });

        it('should fail to add a time entry for a worker without an existing contract and exceeding time', async() => {
            let date = Date.vnNew();
            date.setDate(date.getDate() - 2);
            let error;

            const tx = await models.WorkerTimeControl.beginTransaction({});
            const options = {transaction: tx};
            date.setHours(0, 0, 0);
            ctx.args = {timed: date, direction: 'in'};
            await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);

            try {
                date.setHours(20, 0, 1);
                ctx.args = {timed: date, direction: 'out'};
                await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);

                await tx.rollback();
            } catch (e) {
                await tx.rollback();
                error = e;
            }

            expect(error.message).toBe(`Superado el tiempo máximo entre entrada y salida`);
        });

        describe('direction errors', () => {
            let date = Date.vnNew();
            date.setDate(date.getDate() - 1);
            let error;
            it('should throw an error when trying "in" direction twice', async() => {
                const tx = await models.WorkerTimeControl.beginTransaction({});
                const options = {transaction: tx};

                date.setHours(8, 0, 0);
                ctx.args = {timed: date, direction: 'in'};
                await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);

                try {
                    date.setHours(10, 0, 0);
                    ctx.args = {timed: date, direction: 'in'};
                    await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);

                    await tx.rollback();
                } catch (e) {
                    await tx.rollback();
                    error = e;
                }

                expect(error.message).toBe(`Dirección incorrecta`);
            });

            it('should throw an error when trying "in" direction after insert "in" and "middle"', async() => {
                const tx = await models.WorkerTimeControl.beginTransaction({});
                const options = {transaction: tx};

                date.setHours(8, 0, 0);
                ctx.args = {timed: date, direction: 'in'};
                await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);

                date.setHours(9, 0, 0);
                ctx.args = {timed: date, direction: 'middle'};
                await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);

                try {
                    date.setHours(10, 0, 0);
                    ctx.args = {timed: date, direction: 'in'};
                    await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);

                    await tx.rollback();
                } catch (e) {
                    await tx.rollback();
                    error = e;
                }

                expect(error.message).toBe(`Dirección incorrecta`);
            });

            it('Should throw an error when trying "out" before closing a "middle" couple', async() => {
                const tx = await models.WorkerTimeControl.beginTransaction({});
                const options = {transaction: tx};

                date.setHours(8, 0, 0);
                ctx.args = {timed: date, direction: 'in'};
                await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
                date.setHours(9, 0, 0);
                ctx.args = {timed: date, direction: 'middle'};
                await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);

                try {
                    date.setHours(10, 0, 0);
                    ctx.args = {timed: date, direction: 'out'};
                    await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);

                    await tx.rollback();
                } catch (e) {
                    await tx.rollback();
                    error = e;
                }

                expect(error.message).toBe(`Dirección incorrecta`);
            });

            it('should throw an error when trying "middle" after "out"', async() => {
                const tx = await models.WorkerTimeControl.beginTransaction({});
                const options = {transaction: tx};

                date.setHours(8, 0, 0);
                ctx.args = {timed: date, direction: 'in'};
                await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
                date.setHours(9, 0, 0);
                ctx.args = {timed: date, direction: 'out'};
                await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);

                try {
                    date.setHours(10, 0, 0);
                    ctx.args = {timed: date, direction: 'middle'};
                    await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);

                    await tx.rollback();
                } catch (e) {
                    await tx.rollback();
                    error = e;
                }

                expect(error.message).toBe(`Dirección incorrecta`);
            });

            it('should throw an error when trying "out" direction twice', async() => {
                const tx = await models.WorkerTimeControl.beginTransaction({});
                const options = {transaction: tx};

                date.setHours(8, 0, 0);
                ctx.args = {timed: date, direction: 'in'};
                await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
                date.setHours(9, 0, 0);
                ctx.args = {timed: date, direction: 'out'};
                await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);

                try {
                    date.setHours(10, 0, 0);
                    ctx.args = {timed: date, direction: 'out'};
                    await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);

                    await tx.rollback();
                } catch (e) {
                    await tx.rollback();
                    error = e;
                }

                expect(error.message).toBe(`Dirección incorrecta`);
            });
        });

        describe('12h rest', () => {
            activeCtx.accessToken.userId = salesBossId;
            const workerId = hankPymId;
            it('should throw an error when the 12h rest is not fulfilled yet', async() => {
                let date = Date.vnNew();
                date.setDate(date.getDate() - 2);
                date = weekDay(date, monday);
                let error;

                const tx = await models.WorkerTimeControl.beginTransaction({});
                const options = {transaction: tx};

                date.setHours(8, 0, 0);
                ctx.args = {timed: date, direction: 'in'};
                await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
                date.setHours(16, 0, 0);
                ctx.args = {timed: date, direction: 'out'};
                await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);

                try {
                    date = weekDay(date, tuesday);
                    date.setHours(4, 0, 0);
                    ctx.args = {timed: date, direction: 'in'};
                    await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);

                    await tx.rollback();
                } catch (e) {
                    await tx.rollback();
                    error = e;
                }

                expect(error.message).toBe(`Descanso diario`);
            });

            it('should not fail as the 12h rest is fulfilled', async() => {
                let date = Date.vnNew();
                date.setDate(date.getDate() - 2);
                date = weekDay(date, monday);
                let error;

                const tx = await models.WorkerTimeControl.beginTransaction({});
                const options = {transaction: tx};

                date.setHours(8, 0, 0);
                ctx.args = {timed: date, direction: 'in'};
                await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
                date.setHours(16, 0, 0);
                ctx.args = {timed: date, direction: 'out'};
                await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);

                try {
                    date = weekDay(date, tuesday);
                    date.setHours(4, 1, 0);
                    ctx.args = {timed: date, direction: 'in'};
                    await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);

                    await tx.rollback();
                } catch (e) {
                    await tx.rollback();
                    error = e;
                }

                expect(error).not.toBeDefined;
            });
        });

        describe('for 3500kg drivers with enforced 9h rest', () => {
            activeCtx.accessToken.userId = salesBossId;
            const workerId = jessicaJonesId;
            it('should throw an error when the 9h enforced rest is not fulfilled', async() => {
                let date = Date.vnNew();
                date.setDate(date.getDate() - 2);
                date = weekDay(date, monday);
                let error;

                const tx = await models.WorkerTimeControl.beginTransaction({});
                const options = {transaction: tx};

                date.setHours(8, 0, 0);
                ctx.args = {timed: date, direction: 'in'};
                await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
                date.setHours(16, 0, 0);
                ctx.args = {timed: date, direction: 'out'};
                await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);

                try {
                    date = weekDay(date, tuesday);
                    date.setHours(1, 0, 0);
                    ctx.args = {timed: date, direction: 'in'};
                    await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);

                    await tx.rollback();
                } catch (e) {
                    await tx.rollback();
                    error = e;
                }

                expect(error.message).toBe(`Descanso diario`);
            });

            it('should not fail when the 9h enforced rest is fulfilled', async() => {
                let date = Date.vnNew();
                date.setDate(date.getDate() - 2);
                date = weekDay(date, monday);
                let error;

                const tx = await models.WorkerTimeControl.beginTransaction({});
                const options = {transaction: tx};

                date.setHours(8, 0, 0);
                ctx.args = {timed: date, direction: 'in'};
                await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
                date.setHours(16, 0, 0);
                ctx.args = {timed: date, direction: 'out'};
                await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);

                try {
                    date = weekDay(date, tuesday);
                    date.setHours(1, 1, 0);
                    ctx.args = {timed: date, direction: 'in'};
                    await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);

                    await tx.rollback();
                } catch (e) {
                    await tx.rollback();
                    error = e;
                }

                expect(error).not.toBeDefined;
            });
        });

        describe('for 72h weekly rest', () => {
            
            it('should throw an error when work 11 consecutive days', async() => {
                let date = Date.vnNew();
                date.setMonth(date.getMonth() - 1);
                date.setDate(1);
                let error;

                const tx = await models.WorkerTimeControl.beginTransaction({});
                const options = {transaction: tx};

                await populateWeek(date, monday, sunday, ctx, workerId, options);
                date = nextWeek(date);
                await populateWeek(date, monday, thursday, ctx, workerId, options);
                try {
                    date = weekDay(date, friday);
                    date.setHours(10, 0, 1);
                    ctx.args = {timed: date, direction: 'in'};
                    await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
                    await tx.rollback();
                } catch (e) {
                    await tx.rollback();
                    error = e;
                }

                expect(error.message).toBe(`Descanso semanal`);
            });

            it('should throw an error when the 72h weekly rest is not fulfilled', async() => {

                let date = Date.vnNew();
                date.setMonth(date.getMonth() - 1);
                date.setDate(1);
                let error;

                const tx = await models.WorkerTimeControl.beginTransaction({});
                const options = {transaction: tx};

                await populateWeek(date, monday, sunday, ctx, workerId, options);
                date = nextWeek(date);
                await populateWeek(date, monday, thursday, ctx, workerId, options);

                try {
                    date = weekDay(date, sunday);
                    date.setHours(17, 59, 0);
                    ctx.args = {timed: date, direction: 'in'};
                    await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
                    await tx.rollback();
                } catch (e) {
                    await tx.rollback();
                    error = e;
                }

                expect(error.message).toBe(`Descanso semanal`);
            });
            
            it('should throw an error when the 72h weekly rest is fulfilled', async() => {

                let date = Date.vnNew();
                date.setMonth(date.getMonth() - 1);
                date.setDate(1);
                let error;

                const tx = await models.WorkerTimeControl.beginTransaction({});
                const options = {transaction: tx};

                await populateWeek(date, monday, sunday, ctx, workerId, options);
                date = nextWeek(date);
                await populateWeek(date, monday, thursday, ctx, workerId, options);

                try {
                    date = weekDay(date, sunday);
                    date.setHours(18, 00, 0);
                    ctx.args = {timed: date, direction: 'in'};
                    await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
                    await tx.rollback();
                } catch (e) {
                    await tx.rollback();
                    error = e;
                }

                expect(error).not.toBeDefined;
            });
        });
    });
});

function weekDay(date, dayToSet) {
    const currentDay = date.getDay();
    const distance = dayToSet - currentDay;

    date.setDate(date.getDate() + distance);
    return date;
}

function nextWeek(date) {
    const sunday = 7;
    const currentDay = date.getDay();
    let newDate = date;
    if (currentDay != 0)
        newDate = weekDay(date, sunday);

    newDate.setDate(newDate.getDate() + 1);
    return newDate;
}

async function populateWeek(date, dayStart, dayEnd, ctx, workerId, options) {
    const dateStart = new Date(weekDay(date, dayStart));
    const dateEnd = new Date(dateStart);
    dateEnd.setDate(dateStart.getDate() + dayEnd);

    for (let i = dayStart; i <= dayEnd; i++) {
        dateStart.setHours(10, 0, 0);
        ctx.args = {timed: dateStart, direction: 'in'};
        await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
        dateStart.setHours(18, 0, 0);
        ctx.args = {timed: dateStart, direction: 'out'};
        await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
        dateStart.setDate(dateStart.getDate() + 1);
    }
}