const request = require('supertest');
const app = require('../../app');
const { Task, RecurringCompletion, sequelize } = require('../../models');
const { createTestUser } = require('../helpers/testUtils');
const {
    calculateNextDueDate,
} = require('../../modules/tasks/recurringTaskService');

describe('Recurring Tasks', () => {
    let user, agent;

    beforeEach(async () => {
        user = await createTestUser({
            email: 'test@example.com',
        });

        // Create authenticated agent
        agent = request.agent(app);
        await agent.post('/api/login').send({
            email: 'test@example.com',
            password: 'password123',
        });
    });

    describe('Initial Due Date Calculation', () => {
        describe('Daily Recurrence', () => {
            it('should set correct due date for daily recurring task', async () => {
                const today = new Date();
                today.setUTCHours(0, 0, 0, 0);
                const tomorrow = new Date(today);
                tomorrow.setDate(tomorrow.getDate() + 1);

                const taskData = {
                    name: 'Daily Exercise',
                    recurrence_type: 'daily',
                    recurrence_interval: 1,
                    due_date: today.toISOString().split('T')[0],
                };

                const response = await agent.post('/api/task').send(taskData);

                expect(response.status).toBe(201);
                expect(response.body.recurrence_type).toBe('daily');
                expect(response.body.recurrence_interval).toBe(1);

                // Verify the task was created with the correct due date
                const createdTask = await Task.findByPk(response.body.id);
                expect(createdTask.due_date).toBeDefined();
                expect(
                    new Date(createdTask.due_date).toISOString().split('T')[0]
                ).toBe(today.toISOString().split('T')[0]);
            });

            it('should handle daily recurrence with interval of 2', async () => {
                const today = new Date();
                today.setUTCHours(0, 0, 0, 0);

                const taskData = {
                    name: 'Every Other Day Task',
                    recurrence_type: 'daily',
                    recurrence_interval: 2,
                    due_date: today.toISOString().split('T')[0],
                };

                const response = await agent.post('/api/task').send(taskData);

                expect(response.status).toBe(201);
                expect(response.body.recurrence_type).toBe('daily');
                expect(response.body.recurrence_interval).toBe(2);

                // Calculate next occurrence
                const task = await Task.findByPk(response.body.id);
                const nextDate = calculateNextDueDate(
                    task,
                    new Date(task.due_date)
                );
                const expectedDate = new Date(today);
                expectedDate.setUTCDate(expectedDate.getUTCDate() + 2);

                expect(nextDate.toISOString().split('T')[0]).toBe(
                    expectedDate.toISOString().split('T')[0]
                );
            });

            it('should handle daily recurrence with interval of 7', async () => {
                const today = new Date();
                today.setUTCHours(0, 0, 0, 0);

                const taskData = {
                    name: 'Weekly via Daily',
                    recurrence_type: 'daily',
                    recurrence_interval: 7,
                    due_date: today.toISOString().split('T')[0],
                };

                const response = await agent.post('/api/task').send(taskData);

                expect(response.status).toBe(201);

                const task = await Task.findByPk(response.body.id);
                const nextDate = calculateNextDueDate(
                    task,
                    new Date(task.due_date)
                );
                const expectedDate = new Date(today);
                expectedDate.setUTCDate(expectedDate.getUTCDate() + 7);

                expect(nextDate.toISOString().split('T')[0]).toBe(
                    expectedDate.toISOString().split('T')[0]
                );
            });
        });

        describe('Weekly Recurrence', () => {
            it('should set correct due date for weekly recurring task', async () => {
                const today = new Date();
                today.setUTCHours(0, 0, 0, 0);
                const todayWeekday = today.getUTCDay();

                const taskData = {
                    name: 'Weekly Meeting',
                    recurrence_type: 'weekly',
                    recurrence_interval: 1,
                    recurrence_weekday: todayWeekday,
                    due_date: today.toISOString().split('T')[0],
                };

                const response = await agent.post('/api/task').send(taskData);

                expect(response.status).toBe(201);
                expect(response.body.recurrence_type).toBe('weekly');
                expect(response.body.recurrence_weekday).toBe(todayWeekday);

                const createdTask = await Task.findByPk(response.body.id);
                expect(createdTask.due_date).toBeDefined();
            });

            it('should calculate next week for weekly recurrence', async () => {
                const today = new Date();
                today.setUTCHours(0, 0, 0, 0);
                const todayWeekday = today.getUTCDay();

                const taskData = {
                    name: 'Weekly Review',
                    recurrence_type: 'weekly',
                    recurrence_interval: 1,
                    recurrence_weekday: todayWeekday,
                    due_date: today.toISOString().split('T')[0],
                };

                const response = await agent.post('/api/task').send(taskData);
                const task = await Task.findByPk(response.body.id);

                const nextDate = calculateNextDueDate(
                    task,
                    new Date(task.due_date)
                );
                const expectedDate = new Date(today);
                expectedDate.setUTCDate(expectedDate.getUTCDate() + 7);

                expect(nextDate.toISOString().split('T')[0]).toBe(
                    expectedDate.toISOString().split('T')[0]
                );
            });

            it('should handle bi-weekly recurrence', async () => {
                const today = new Date();
                today.setUTCHours(0, 0, 0, 0);
                const todayWeekday = today.getUTCDay();

                const taskData = {
                    name: 'Bi-weekly Task',
                    recurrence_type: 'weekly',
                    recurrence_interval: 2,
                    recurrence_weekday: todayWeekday,
                    due_date: today.toISOString().split('T')[0],
                };

                const response = await agent.post('/api/task').send(taskData);
                const task = await Task.findByPk(response.body.id);

                const nextDate = calculateNextDueDate(
                    task,
                    new Date(task.due_date)
                );
                const expectedDate = new Date(today);
                expectedDate.setUTCDate(expectedDate.getUTCDate() + 14);

                expect(nextDate.toISOString().split('T')[0]).toBe(
                    expectedDate.toISOString().split('T')[0]
                );
            });

            it('should handle weekly recurrence on a different weekday', async () => {
                const today = new Date();
                today.setUTCHours(0, 0, 0, 0);
                const todayWeekday = today.getUTCDay();
                // Target a different weekday (e.g., if today is Monday (1), target Friday (5))
                const targetWeekday = (todayWeekday + 4) % 7;

                const taskData = {
                    name: 'Weekly on Different Day',
                    recurrence_type: 'weekly',
                    recurrence_interval: 1,
                    recurrence_weekday: targetWeekday,
                    due_date: today.toISOString().split('T')[0],
                };

                const response = await agent.post('/api/task').send(taskData);
                const task = await Task.findByPk(response.body.id);

                const nextDate = calculateNextDueDate(
                    task,
                    new Date(task.due_date)
                );
                expect(nextDate.getUTCDay()).toBe(targetWeekday);
            });

            it('should advance to next selected weekday with multiple weekdays (Issue #715)', async () => {
                // Use a fixed Tuesday: 2026-02-10
                const tuesday = new Date(Date.UTC(2026, 1, 10, 0, 0, 0, 0));
                expect(tuesday.getUTCDay()).toBe(2); // Sanity: Tuesday

                const task = await Task.create({
                    name: 'Tue/Thu Task',
                    recurrence_type: 'weekly',
                    recurrence_interval: 1,
                    recurrence_weekdays: [2, 4], // Tuesday, Thursday
                    due_date: tuesday,
                    user_id: user.id,
                    status: Task.STATUS.NOT_STARTED,
                });

                // Completing on Tuesday should advance to Thursday (2 days), not next Tuesday (7 days)
                const nextDate = calculateNextDueDate(task, tuesday);
                expect(nextDate.getUTCDay()).toBe(4); // Thursday
                expect(nextDate.toISOString().split('T')[0]).toBe('2026-02-12');
            });

            it('should wrap around to next week when completing on last selected weekday', async () => {
                // Use a fixed Thursday: 2026-02-12
                const thursday = new Date(Date.UTC(2026, 1, 12, 0, 0, 0, 0));
                expect(thursday.getUTCDay()).toBe(4); // Sanity: Thursday

                const task = await Task.create({
                    name: 'Tue/Thu Task',
                    recurrence_type: 'weekly',
                    recurrence_interval: 1,
                    recurrence_weekdays: [2, 4], // Tuesday, Thursday
                    due_date: thursday,
                    user_id: user.id,
                    status: Task.STATUS.NOT_STARTED,
                });

                // Completing on Thursday should wrap to next Tuesday (5 days)
                const nextDate = calculateNextDueDate(task, thursday);
                expect(nextDate.getUTCDay()).toBe(2); // Tuesday
                expect(nextDate.toISOString().split('T')[0]).toBe('2026-02-17');
            });

            it('should advance due date correctly on completion with multiple weekdays via API', async () => {
                // Use a fixed Tuesday: 2026-02-10
                const tuesday = new Date(Date.UTC(2026, 1, 10, 0, 0, 0, 0));

                const task = await Task.create({
                    name: 'Tue/Thu Via API',
                    recurrence_type: 'weekly',
                    recurrence_interval: 1,
                    recurrence_weekdays: [2, 4], // Tuesday, Thursday
                    due_date: tuesday,
                    user_id: user.id,
                    status: Task.STATUS.NOT_STARTED,
                });

                // Complete the task on Tuesday
                await agent
                    .patch(`/api/task/${task.uid}`)
                    .send({ status: Task.STATUS.DONE });
                await task.reload();

                // Should advance to Thursday, not next Tuesday
                expect(task.status).toBe(Task.STATUS.NOT_STARTED);
                const newDueDate = new Date(task.due_date);
                expect(newDueDate.getUTCDay()).toBe(4); // Thursday

                // Complete again on Thursday
                await agent
                    .patch(`/api/task/${task.uid}`)
                    .send({ status: Task.STATUS.DONE });
                await task.reload();

                // Should wrap to next Tuesday
                expect(task.status).toBe(Task.STATUS.NOT_STARTED);
                const nextDueDate = new Date(task.due_date);
                expect(nextDueDate.getUTCDay()).toBe(2); // Tuesday
            });

            it('should respect bi-weekly interval with multiple weekdays (Issue #844)', async () => {
                // Use a fixed Tuesday: 2026-02-10
                const tuesday = new Date(Date.UTC(2026, 1, 10, 0, 0, 0, 0));
                expect(tuesday.getUTCDay()).toBe(2); // Sanity: Tuesday

                const task = await Task.create({
                    name: 'Bi-weekly Tue/Thu Task',
                    recurrence_type: 'weekly',
                    recurrence_interval: 2,
                    recurrence_weekdays: [2, 4], // Tuesday, Thursday
                    due_date: tuesday,
                    user_id: user.id,
                    status: Task.STATUS.NOT_STARTED,
                });

                // Tuesday -> Thursday (same cycle week)
                const next1 = calculateNextDueDate(task, tuesday);
                expect(next1.getUTCDay()).toBe(4); // Thursday
                expect(next1.toISOString().split('T')[0]).toBe('2026-02-12');

                // Thursday -> next cycle Tuesday (skip 1 week, land on week 3's Tuesday)
                const next2 = calculateNextDueDate(task, next1);
                expect(next2.getUTCDay()).toBe(2); // Tuesday
                expect(next2.toISOString().split('T')[0]).toBe('2026-02-24');

                // Tuesday (week 3) -> Thursday (week 3, same cycle)
                const next3 = calculateNextDueDate(task, next2);
                expect(next3.getUTCDay()).toBe(4); // Thursday
                expect(next3.toISOString().split('T')[0]).toBe('2026-02-26');

                // Thursday (week 3) -> Tuesday (week 5)
                const next4 = calculateNextDueDate(task, next3);
                expect(next4.getUTCDay()).toBe(2); // Tuesday
                expect(next4.toISOString().split('T')[0]).toBe('2026-03-10');
            });

            it('should respect tri-weekly interval with single weekday via weekdays array (Issue #844)', async () => {
                // Use a fixed Monday: 2026-02-09
                const monday = new Date(Date.UTC(2026, 1, 9, 0, 0, 0, 0));
                expect(monday.getUTCDay()).toBe(1);

                const task = await Task.create({
                    name: 'Every 3 weeks on Monday',
                    recurrence_type: 'weekly',
                    recurrence_interval: 3,
                    recurrence_weekdays: [1], // Monday only
                    due_date: monday,
                    user_id: user.id,
                    status: Task.STATUS.NOT_STARTED,
                });

                // Monday Feb 9 -> Monday Mar 2 (3 weeks later)
                const next1 = calculateNextDueDate(task, monday);
                expect(next1.getUTCDay()).toBe(1);
                expect(next1.toISOString().split('T')[0]).toBe('2026-03-02');

                // Monday Mar 2 -> Monday Mar 23 (3 weeks later)
                const next2 = calculateNextDueDate(task, next1);
                expect(next2.getUTCDay()).toBe(1);
                expect(next2.toISOString().split('T')[0]).toBe('2026-03-23');
            });

            it('should handle three weekdays (Mon/Wed/Fri)', async () => {
                // Use a fixed Monday: 2026-02-09
                const monday = new Date(Date.UTC(2026, 1, 9, 0, 0, 0, 0));
                expect(monday.getUTCDay()).toBe(1); // Sanity: Monday

                const task = await Task.create({
                    name: 'MWF Task',
                    recurrence_type: 'weekly',
                    recurrence_interval: 1,
                    recurrence_weekdays: [1, 3, 5], // Mon, Wed, Fri
                    due_date: monday,
                    user_id: user.id,
                    status: Task.STATUS.NOT_STARTED,
                });

                // Monday -> Wednesday
                const next1 = calculateNextDueDate(task, monday);
                expect(next1.getUTCDay()).toBe(3);
                expect(next1.toISOString().split('T')[0]).toBe('2026-02-11');

                // Wednesday -> Friday
                const next2 = calculateNextDueDate(task, next1);
                expect(next2.getUTCDay()).toBe(5);
                expect(next2.toISOString().split('T')[0]).toBe('2026-02-13');

                // Friday -> next Monday
                const next3 = calculateNextDueDate(task, next2);
                expect(next3.getUTCDay()).toBe(1);
                expect(next3.toISOString().split('T')[0]).toBe('2026-02-16');
            });
        });

        describe('Monthly Recurrence', () => {
            it('should set correct due date for monthly recurring task', async () => {
                const today = new Date();
                today.setUTCHours(0, 0, 0, 0);
                const dayOfMonth = today.getUTCDate();

                const taskData = {
                    name: 'Monthly Report',
                    recurrence_type: 'monthly',
                    recurrence_interval: 1,
                    recurrence_month_day: dayOfMonth,
                    due_date: today.toISOString().split('T')[0],
                };

                const response = await agent.post('/api/task').send(taskData);

                expect(response.status).toBe(201);
                expect(response.body.recurrence_type).toBe('monthly');
                expect(response.body.recurrence_month_day).toBe(dayOfMonth);

                const createdTask = await Task.findByPk(response.body.id);
                expect(createdTask.due_date).toBeDefined();
            });

            it('should calculate next month for monthly recurrence', async () => {
                const today = new Date();
                today.setUTCHours(0, 0, 0, 0);
                const dayOfMonth = 15; // Use a safe day that exists in all months

                const taskData = {
                    name: 'Monthly Bill',
                    recurrence_type: 'monthly',
                    recurrence_interval: 1,
                    recurrence_month_day: dayOfMonth,
                    due_date: new Date(
                        Date.UTC(
                            today.getUTCFullYear(),
                            today.getUTCMonth(),
                            dayOfMonth
                        )
                    )
                        .toISOString()
                        .split('T')[0],
                };

                const response = await agent.post('/api/task').send(taskData);
                const task = await Task.findByPk(response.body.id);

                const nextDate = calculateNextDueDate(
                    task,
                    new Date(task.due_date)
                );
                const expectedMonth = (today.getUTCMonth() + 1) % 12;

                expect(nextDate.getUTCDate()).toBe(dayOfMonth);
                expect(nextDate.getUTCMonth()).toBe(expectedMonth);
            });

            it('should handle monthly recurrence on last day of month', async () => {
                const today = new Date();
                today.setUTCHours(0, 0, 0, 0);

                const taskData = {
                    name: 'End of Month Task',
                    recurrence_type: 'monthly_last_day',
                    recurrence_interval: 1,
                    due_date: today.toISOString().split('T')[0],
                };

                const response = await agent.post('/api/task').send(taskData);
                const task = await Task.findByPk(response.body.id);

                const nextDate = calculateNextDueDate(
                    task,
                    new Date(task.due_date)
                );

                // Should be last day of next month
                const expectedDate = new Date(
                    Date.UTC(
                        nextDate.getUTCFullYear(),
                        nextDate.getUTCMonth() + 1,
                        0
                    )
                );
                expect(nextDate.getUTCDate()).toBe(expectedDate.getUTCDate());
            });

            it('should not skip months for monthly_last_day when starting from 31st', async () => {
                // Bug fix: Jan 31 -> should go to Feb 28, not March 31
                const jan31 = new Date(Date.UTC(2025, 0, 31, 0, 0, 0, 0));

                const taskData = {
                    name: 'End of Month Task',
                    recurrence_type: 'monthly_last_day',
                    recurrence_interval: 1,
                    due_date: jan31.toISOString().split('T')[0],
                };

                const response = await agent.post('/api/task').send(taskData);
                const task = await Task.findByPk(response.body.id);

                // First occurrence: Jan 31 -> Feb 28
                const nextDate1 = calculateNextDueDate(task, jan31);
                expect(nextDate1.getUTCMonth()).toBe(1); // February
                expect(nextDate1.getUTCDate()).toBe(28);

                // Second occurrence: Feb 28 -> Mar 31
                const nextDate2 = calculateNextDueDate(task, nextDate1);
                expect(nextDate2.getUTCMonth()).toBe(2); // March
                expect(nextDate2.getUTCDate()).toBe(31);

                // Third occurrence: Mar 31 -> Apr 30
                const nextDate3 = calculateNextDueDate(task, nextDate2);
                expect(nextDate3.getUTCMonth()).toBe(3); // April
                expect(nextDate3.getUTCDate()).toBe(30);
            });

            it('should handle monthly recurrence when day does not exist in target month', async () => {
                // Create a task for Jan 31
                const jan31 = new Date(Date.UTC(2024, 0, 31, 0, 0, 0, 0));

                const taskData = {
                    name: 'Monthly on 31st',
                    recurrence_type: 'monthly',
                    recurrence_interval: 1,
                    recurrence_month_day: 31,
                    due_date: jan31.toISOString().split('T')[0],
                };

                const response = await agent.post('/api/task').send(taskData);
                const task = await Task.findByPk(response.body.id);

                // Next occurrence should be Feb 29 (if leap year) or Feb 28
                const nextDate = calculateNextDueDate(task, jan31);

                // February should cap at the last day of the month
                expect(nextDate.getUTCMonth()).toBe(1); // February
                expect(nextDate.getUTCDate()).toBeLessThanOrEqual(29);
            });
        });

        describe('Monthly Weekday Recurrence', () => {
            it('should handle first Monday of the month', async () => {
                const today = new Date();
                today.setUTCHours(0, 0, 0, 0);

                const taskData = {
                    name: 'First Monday Meeting',
                    recurrence_type: 'monthly_weekday',
                    recurrence_interval: 1,
                    recurrence_weekday: 1, // Monday
                    recurrence_week_of_month: 1, // First week
                    due_date: today.toISOString().split('T')[0],
                };

                const response = await agent.post('/api/task').send(taskData);
                expect(response.status).toBe(201);

                const task = await Task.findByPk(response.body.id);
                const nextDate = calculateNextDueDate(
                    task,
                    new Date(task.due_date)
                );

                // Should be a Monday
                expect(nextDate.getUTCDay()).toBe(1);
            });

            it('should handle third Friday of the month', async () => {
                const today = new Date();
                today.setUTCHours(0, 0, 0, 0);

                const taskData = {
                    name: 'Third Friday Task',
                    recurrence_type: 'monthly_weekday',
                    recurrence_interval: 1,
                    recurrence_weekday: 5, // Friday
                    recurrence_week_of_month: 3, // Third week
                    due_date: today.toISOString().split('T')[0],
                };

                const response = await agent.post('/api/task').send(taskData);
                const task = await Task.findByPk(response.body.id);
                const nextDate = calculateNextDueDate(
                    task,
                    new Date(task.due_date)
                );

                // Should be a Friday
                expect(nextDate.getUTCDay()).toBe(5);
            });
        });
    });

    describe('Due Date Refresh on Completion', () => {
        it('should advance due date when daily recurring task is completed', async () => {
            const today = new Date();
            today.setUTCHours(0, 0, 0, 0);

            // Create a daily recurring task
            const task = await Task.create({
                name: 'Daily Task',
                recurrence_type: 'daily',
                recurrence_interval: 1,
                due_date: today,
                user_id: user.id,
                status: Task.STATUS.NOT_STARTED,
            });

            const originalDueDate = new Date(task.due_date);

            // Mark the task as completed
            const response = await agent
                .patch(`/api/task/${task.uid}`)
                .send({ status: Task.STATUS.DONE });

            expect(response.status).toBe(200);

            // Reload the task
            await task.reload();

            // Task should be reset to NOT_STARTED
            expect(task.status).toBe(Task.STATUS.NOT_STARTED);
            expect(task.completed_at).toBeNull();

            // Due date should be advanced by 1 day
            const newDueDate = new Date(task.due_date);
            const expectedDate = new Date(originalDueDate);
            expectedDate.setUTCDate(expectedDate.getUTCDate() + 1);

            expect(newDueDate.toISOString().split('T')[0]).toBe(
                expectedDate.toISOString().split('T')[0]
            );

            // Verify RecurringCompletion was created
            const completions = await RecurringCompletion.findAll({
                where: { task_id: task.id },
            });
            expect(completions.length).toBe(1);
            expect(completions[0].original_due_date).toBeDefined();
        });

        it('should advance due date when weekly recurring task is completed', async () => {
            const today = new Date();
            today.setUTCHours(0, 0, 0, 0);
            const todayWeekday = today.getUTCDay();

            const task = await Task.create({
                name: 'Weekly Task',
                recurrence_type: 'weekly',
                recurrence_interval: 1,
                recurrence_weekday: todayWeekday,
                due_date: today,
                user_id: user.id,
                status: Task.STATUS.NOT_STARTED,
            });

            const originalDueDate = new Date(task.due_date);

            // Complete the task
            await agent
                .patch(`/api/task/${task.uid}`)
                .send({ status: Task.STATUS.DONE });

            await task.reload();

            // Due date should be advanced by 1 week
            const newDueDate = new Date(task.due_date);
            const expectedDate = new Date(originalDueDate);
            expectedDate.setUTCDate(expectedDate.getUTCDate() + 7);

            expect(newDueDate.toISOString().split('T')[0]).toBe(
                expectedDate.toISOString().split('T')[0]
            );
            expect(task.status).toBe(Task.STATUS.NOT_STARTED);
        });

        it('should advance due date when monthly recurring task is completed', async () => {
            const today = new Date();
            today.setUTCHours(0, 0, 0, 0);
            const dayOfMonth = 15; // Use a safe day

            const startDate = new Date(
                Date.UTC(
                    today.getUTCFullYear(),
                    today.getUTCMonth(),
                    dayOfMonth
                )
            );

            const task = await Task.create({
                name: 'Monthly Task',
                recurrence_type: 'monthly',
                recurrence_interval: 1,
                recurrence_month_day: dayOfMonth,
                due_date: startDate,
                user_id: user.id,
                status: Task.STATUS.NOT_STARTED,
            });

            const originalMonth = new Date(task.due_date).getUTCMonth();

            // Complete the task
            await agent
                .patch(`/api/task/${task.uid}`)
                .send({ status: Task.STATUS.DONE });

            await task.reload();

            // Due date should be in the next month
            const newDueDate = new Date(task.due_date);
            const expectedMonth = (originalMonth + 1) % 12;

            expect(newDueDate.getUTCMonth()).toBe(expectedMonth);
            expect(newDueDate.getUTCDate()).toBe(dayOfMonth);
            expect(task.status).toBe(Task.STATUS.NOT_STARTED);
        });

        it('should track multiple completions in RecurringCompletion table', async () => {
            const today = new Date();
            today.setUTCHours(0, 0, 0, 0);

            const task = await Task.create({
                name: 'Daily Task',
                recurrence_type: 'daily',
                recurrence_interval: 1,
                due_date: today,
                user_id: user.id,
                status: Task.STATUS.NOT_STARTED,
            });

            // Complete the task three times
            for (let i = 0; i < 3; i++) {
                await agent
                    .patch(`/api/task/${task.uid}`)
                    .send({ status: Task.STATUS.DONE });
                await task.reload();
            }

            // Verify three completions were recorded
            const completions = await RecurringCompletion.findAll({
                where: { task_id: task.id },
                order: [['completed_at', 'ASC']],
            });

            expect(completions.length).toBe(3);

            // Verify each completion has the correct original_due_date
            for (let i = 0; i < 3; i++) {
                expect(completions[i].original_due_date).toBeDefined();
                expect(completions[i].skipped).toBe(false);
            }
        });
    });

    describe('Completion-Based Recurrence', () => {
        it('should use completion date when completion_based is true', async () => {
            const threeDaysAgo = new Date();
            threeDaysAgo.setDate(threeDaysAgo.getDate() - 3);
            threeDaysAgo.setHours(0, 0, 0, 0);

            // Create a task due 3 days ago with completion-based recurrence
            const task = await Task.create({
                name: 'Completion Based Task',
                recurrence_type: 'daily',
                recurrence_interval: 1,
                due_date: threeDaysAgo,
                completion_based: true,
                user_id: user.id,
                status: Task.STATUS.NOT_STARTED,
            });

            // Complete it today
            const completionTime = new Date();
            await agent
                .patch(`/api/task/${task.uid}`)
                .send({ status: Task.STATUS.DONE });

            await task.reload();

            // Next due date should be tomorrow (completion date + interval)
            // not 2 days ago (original due date + interval)
            const newDueDate = new Date(task.due_date);
            const tomorrow = new Date(completionTime);
            tomorrow.setDate(tomorrow.getDate() + 1);
            tomorrow.setHours(0, 0, 0, 0);

            const dayDiff = Math.round(
                (newDueDate - completionTime) / (1000 * 60 * 60 * 24)
            );

            // Should be approximately 1 day from completion
            expect(dayDiff).toBeGreaterThanOrEqual(0);
            expect(dayDiff).toBeLessThanOrEqual(1);
        });

        it('should use original due date when completion_based is false', async () => {
            const threeDaysAgo = new Date();
            threeDaysAgo.setDate(threeDaysAgo.getDate() - 3);
            threeDaysAgo.setHours(0, 0, 0, 0);

            const task = await Task.create({
                name: 'Date Based Task',
                recurrence_type: 'daily',
                recurrence_interval: 1,
                due_date: threeDaysAgo,
                completion_based: false,
                user_id: user.id,
                status: Task.STATUS.NOT_STARTED,
            });

            const originalDueDate = new Date(task.due_date);

            // Complete it today
            await agent
                .patch(`/api/task/${task.uid}`)
                .send({ status: Task.STATUS.DONE });

            await task.reload();

            // Next due date should be 2 days ago (original due date + 1 day)
            const newDueDate = new Date(task.due_date);
            const expectedDate = new Date(originalDueDate);
            expectedDate.setUTCDate(expectedDate.getUTCDate() + 1);

            expect(newDueDate.toISOString().split('T')[0]).toBe(
                expectedDate.toISOString().split('T')[0]
            );
        });

        it('should use completion date for early completion when completion_based is true', async () => {
            const twoDaysFromNow = new Date();
            twoDaysFromNow.setDate(twoDaysFromNow.getDate() + 2);
            twoDaysFromNow.setHours(0, 0, 0, 0);

            const task = await Task.create({
                name: 'Early Completion Task',
                recurrence_type: 'daily',
                recurrence_interval: 1,
                due_date: twoDaysFromNow,
                completion_based: true,
                user_id: user.id,
                status: Task.STATUS.NOT_STARTED,
            });

            // Complete it today (2 days early)
            const completionTime = new Date();
            await agent
                .patch(`/api/task/${task.uid}`)
                .send({ status: Task.STATUS.DONE });

            await task.reload();

            // Next due date should be tomorrow (today + 1 day)
            // not 3 days from now (original due date + 1 day)
            const newDueDate = new Date(task.due_date);
            const dayDiff = Math.round(
                (newDueDate - completionTime) / (1000 * 60 * 60 * 24)
            );

            // Should be approximately 1 day from completion (today)
            expect(dayDiff).toBeGreaterThanOrEqual(0);
            expect(dayDiff).toBeLessThanOrEqual(1);

            // Verify it's NOT 3 days from now
            const threeDaysFromNow = new Date();
            threeDaysFromNow.setDate(threeDaysFromNow.getDate() + 3);
            expect(newDueDate.toISOString().split('T')[0]).not.toBe(
                threeDaysFromNow.toISOString().split('T')[0]
            );
        });

        it('should work with weekly completion_based recurrence', async () => {
            const lastWeek = new Date();
            lastWeek.setDate(lastWeek.getDate() - 7);
            lastWeek.setHours(0, 0, 0, 0);
            const lastWeekWeekday = lastWeek.getDay();

            const task = await Task.create({
                name: 'Weekly Completion Based',
                recurrence_type: 'weekly',
                recurrence_interval: 1,
                recurrence_weekday: lastWeekWeekday,
                due_date: lastWeek,
                completion_based: true,
                user_id: user.id,
                status: Task.STATUS.NOT_STARTED,
            });

            // Complete it today
            await agent
                .patch(`/api/task/${task.uid}`)
                .send({ status: Task.STATUS.DONE });

            await task.reload();

            // Next due date should be approximately 7 days from today
            // not 7 days from last week (which would be today)
            const newDueDate = new Date(task.due_date);
            const today = new Date();
            const dayDiff = Math.round(
                (newDueDate - today) / (1000 * 60 * 60 * 24)
            );

            // Should be around 6-7 days from now
            expect(dayDiff).toBeGreaterThanOrEqual(6);
            expect(dayDiff).toBeLessThanOrEqual(8);
        });

        it('should work with monthly completion_based recurrence', async () => {
            const lastMonth = new Date();
            lastMonth.setMonth(lastMonth.getMonth() - 1);
            lastMonth.setUTCHours(0, 0, 0, 0);
            const dayOfMonth = 15;

            const task = await Task.create({
                name: 'Monthly Completion Based',
                recurrence_type: 'monthly',
                recurrence_interval: 1,
                recurrence_month_day: dayOfMonth,
                due_date: new Date(
                    Date.UTC(
                        lastMonth.getUTCFullYear(),
                        lastMonth.getUTCMonth(),
                        dayOfMonth
                    )
                ),
                completion_based: true,
                user_id: user.id,
                status: Task.STATUS.NOT_STARTED,
            });

            // Complete it today
            const completionMonth = new Date().getUTCMonth();
            await agent
                .patch(`/api/task/${task.uid}`)
                .send({ status: Task.STATUS.DONE });

            await task.reload();

            // Next due date should be approximately 1 month from today
            // not 1 month from last month (which would be this month)
            const newDueDate = new Date(task.due_date);
            const expectedMonth = (completionMonth + 1) % 12;

            // Should be next month
            expect(newDueDate.getUTCMonth()).toBe(expectedMonth);
        });

        it('should handle interval > 1 with completion_based', async () => {
            const today = new Date();
            today.setUTCHours(0, 0, 0, 0);

            const task = await Task.create({
                name: 'Every 3 Days Completion Based',
                recurrence_type: 'daily',
                recurrence_interval: 3,
                due_date: today,
                completion_based: true,
                user_id: user.id,
                status: Task.STATUS.NOT_STARTED,
            });

            const completionTime = new Date();
            await agent
                .patch(`/api/task/${task.uid}`)
                .send({ status: Task.STATUS.DONE });

            await task.reload();

            // Next due date should be 3 days from completion
            const newDueDate = new Date(task.due_date);
            const dayDiff = Math.round(
                (newDueDate - completionTime) / (1000 * 60 * 60 * 24)
            );

            // Should be approximately 3 days from completion
            expect(dayDiff).toBeGreaterThanOrEqual(2);
            expect(dayDiff).toBeLessThanOrEqual(3);
        });

        it('should respect updated completion_based flag', async () => {
            const today = new Date();
            today.setUTCHours(0, 0, 0, 0);

            // Create task with completion_based = false
            const task = await Task.create({
                name: 'Toggle Task',
                recurrence_type: 'daily',
                recurrence_interval: 1,
                due_date: today,
                completion_based: false,
                user_id: user.id,
                status: Task.STATUS.NOT_STARTED,
            });

            // Update to completion_based = true
            await agent.patch(`/api/task/${task.uid}`).send({
                completion_based: true,
            });

            await task.reload();
            expect(task.completion_based).toBe(true);

            // Now complete it - should use completion-based logic
            const completionTime = new Date();
            await agent
                .patch(`/api/task/${task.uid}`)
                .send({ status: Task.STATUS.DONE });

            await task.reload();

            // Should advance from completion time, not original due date
            const newDueDate = new Date(task.due_date);
            const dayDiff = Math.round(
                (newDueDate - completionTime) / (1000 * 60 * 60 * 24)
            );

            expect(dayDiff).toBeGreaterThanOrEqual(0);
            expect(dayDiff).toBeLessThanOrEqual(1);
        });

        it('should handle multiple rapid completions with completion_based', async () => {
            const today = new Date();
            today.setUTCHours(0, 0, 0, 0);

            const task = await Task.create({
                name: 'Rapid Completions',
                recurrence_type: 'daily',
                recurrence_interval: 2,
                due_date: today,
                completion_based: true,
                user_id: user.id,
                status: Task.STATUS.NOT_STARTED,
            });

            // First completion
            const firstCompletionTime = new Date();
            await agent
                .patch(`/api/task/${task.uid}`)
                .send({ status: Task.STATUS.DONE });
            await task.reload();

            const firstNewDueDate = new Date(task.due_date);

            // Should be approximately 2 days from completion
            const firstDayDiff = Math.round(
                (firstNewDueDate - firstCompletionTime) / (1000 * 60 * 60 * 24)
            );
            expect(firstDayDiff).toBeGreaterThanOrEqual(1);
            expect(firstDayDiff).toBeLessThanOrEqual(2);

            // Second completion immediately after first
            const secondCompletionTime = new Date();
            await agent
                .patch(`/api/task/${task.uid}`)
                .send({ status: Task.STATUS.DONE });
            await task.reload();

            const secondNewDueDate = new Date(task.due_date);

            // Should be approximately 2 days from second completion
            // which means it should be later than the first new due date
            expect(secondNewDueDate.getTime()).toBeGreaterThan(
                firstNewDueDate.getTime()
            );

            const secondDayDiff = Math.round(
                (secondNewDueDate - secondCompletionTime) /
                    (1000 * 60 * 60 * 24)
            );
            expect(secondDayDiff).toBeGreaterThanOrEqual(1);
            expect(secondDayDiff).toBeLessThanOrEqual(2);

            // Verify both completions were recorded
            const completions = await RecurringCompletion.findAll({
                where: { task_id: task.id },
            });
            expect(completions.length).toBe(2);
        });
    });

    describe('Recurrence End Date', () => {
        it('should stop recurring when end date is reached', async () => {
            const today = new Date();
            today.setUTCHours(0, 0, 0, 0);
            const tomorrow = new Date(today);
            tomorrow.setDate(tomorrow.getDate() + 1);
            const dayAfterTomorrow = new Date(today);
            dayAfterTomorrow.setDate(dayAfterTomorrow.getDate() + 2);
            const threeDaysFromNow = new Date(today);
            threeDaysFromNow.setDate(threeDaysFromNow.getDate() + 3);

            // Create a daily task due today with end date 3 days from now
            // Note: end date is exclusive (uses <, not <=)
            // So this task can recur on: today (day 0), tomorrow (day 1), day 2
            // But NOT on day 3 (the end date itself)
            const task = await Task.create({
                name: 'Limited Recurring Task',
                recurrence_type: 'daily',
                recurrence_interval: 1,
                due_date: today,
                recurrence_end_date: threeDaysFromNow,
                user_id: user.id,
                status: Task.STATUS.NOT_STARTED,
            });

            // Complete the task (first completion) - should advance to tomorrow (day 1)
            await agent
                .patch(`/api/task/${task.uid}`)
                .send({ status: Task.STATUS.DONE });

            await task.reload();

            expect(task.status).toBe(Task.STATUS.NOT_STARTED);
            let newDueDate = new Date(task.due_date);
            expect(newDueDate.toISOString().split('T')[0]).toBe(
                tomorrow.toISOString().split('T')[0]
            );

            // Complete again - should advance to day after tomorrow (day 2, last valid occurrence)
            await agent
                .patch(`/api/task/${task.uid}`)
                .send({ status: Task.STATUS.DONE });
            await task.reload();

            expect(task.status).toBe(Task.STATUS.NOT_STARTED);
            newDueDate = new Date(task.due_date);
            expect(newDueDate.toISOString().split('T')[0]).toBe(
                dayAfterTomorrow.toISOString().split('T')[0]
            );

            // Complete one more time - this time it should stop recurring
            // because the next occurrence would be day 3 (threeDaysFromNow) which is >= end date
            await agent
                .patch(`/api/task/${task.uid}`)
                .send({ status: Task.STATUS.DONE });

            await task.reload();

            // Task should remain completed since next occurrence would be on or past end date
            expect(task.status).toBe(Task.STATUS.DONE);
            expect(task.completed_at).not.toBeNull();
        });

        it('should allow recurring tasks without end date to continue indefinitely', async () => {
            const today = new Date();
            today.setUTCHours(0, 0, 0, 0);

            const task = await Task.create({
                name: 'Infinite Task',
                recurrence_type: 'daily',
                recurrence_interval: 1,
                due_date: today,
                recurrence_end_date: null,
                user_id: user.id,
                status: Task.STATUS.NOT_STARTED,
            });

            // Complete multiple times
            for (let i = 0; i < 5; i++) {
                await agent
                    .patch(`/api/task/${task.uid}`)
                    .send({ status: Task.STATUS.DONE });
                await task.reload();
                expect(task.status).toBe(Task.STATUS.NOT_STARTED);
            }

            // Verify all completions were recorded
            const completions = await RecurringCompletion.findAll({
                where: { task_id: task.id },
            });
            expect(completions.length).toBe(5);
        });
    });

    describe('Edge Cases', () => {
        it('should handle completing a task multiple times in quick succession', async () => {
            const today = new Date();
            today.setUTCHours(0, 0, 0, 0);

            const task = await Task.create({
                name: 'Quick Complete Task',
                recurrence_type: 'daily',
                recurrence_interval: 1,
                due_date: today,
                user_id: user.id,
                status: Task.STATUS.NOT_STARTED,
            });

            // Try to complete it twice rapidly
            await agent
                .patch(`/api/task/${task.uid}`)
                .send({ status: Task.STATUS.DONE });
            await task.reload();

            const dueDateAfterFirst = new Date(task.due_date);

            await agent
                .patch(`/api/task/${task.uid}`)
                .send({ status: Task.STATUS.DONE });
            await task.reload();

            const dueDateAfterSecond = new Date(task.due_date);

            // Both completions should have been recorded
            const completions = await RecurringCompletion.findAll({
                where: { task_id: task.id },
            });
            expect(completions.length).toBe(2);

            // Second due date should be one day after first
            const expectedDate = new Date(dueDateAfterFirst);
            expectedDate.setUTCDate(expectedDate.getUTCDate() + 1);
            expect(dueDateAfterSecond.toISOString().split('T')[0]).toBe(
                expectedDate.toISOString().split('T')[0]
            );
        });

        it('should handle uncompleting a recurring task', async () => {
            const today = new Date();
            today.setUTCHours(0, 0, 0, 0);

            const task = await Task.create({
                name: 'Uncomplete Task',
                recurrence_type: 'daily',
                recurrence_interval: 1,
                due_date: today,
                user_id: user.id,
                status: Task.STATUS.NOT_STARTED,
            });

            const originalDueDate = new Date(task.due_date);

            // Complete the task
            await agent
                .patch(`/api/task/${task.uid}`)
                .send({ status: Task.STATUS.DONE });
            await task.reload();

            expect(task.status).toBe(Task.STATUS.NOT_STARTED);
            const advancedDueDate = new Date(task.due_date);

            // Due date should have advanced
            expect(advancedDueDate.getTime()).toBeGreaterThan(
                originalDueDate.getTime()
            );

            // Change status to in_progress (not completed)
            await agent
                .patch(`/api/task/${task.uid}`)
                .send({ status: Task.STATUS.IN_PROGRESS });
            await task.reload();

            // Due date should remain the same
            expect(new Date(task.due_date).getTime()).toBe(
                advancedDueDate.getTime()
            );
            expect(task.status).toBe(Task.STATUS.IN_PROGRESS);
        });

        it('should not create recurring completion for non-recurring tasks', async () => {
            const task = await Task.create({
                name: 'Regular Task',
                recurrence_type: 'none',
                user_id: user.id,
                status: Task.STATUS.NOT_STARTED,
            });

            // Complete the task
            await agent
                .patch(`/api/task/${task.uid}`)
                .send({ status: Task.STATUS.DONE });
            await task.reload();

            // Should stay completed
            expect(task.status).toBe(Task.STATUS.DONE);
            expect(task.completed_at).not.toBeNull();

            // No recurring completion should be created
            const completions = await RecurringCompletion.findAll({
                where: { task_id: task.id },
            });
            expect(completions.length).toBe(0);
        });

        it('should handle tasks without a due date', async () => {
            const task = await Task.create({
                name: 'No Due Date Recurring',
                recurrence_type: 'daily',
                recurrence_interval: 1,
                due_date: null,
                user_id: user.id,
                status: Task.STATUS.NOT_STARTED,
            });

            // Complete the task
            await agent
                .patch(`/api/task/${task.uid}`)
                .send({ status: Task.STATUS.DONE });
            await task.reload();

            // Should calculate next due date from completion time
            expect(task.status).toBe(Task.STATUS.NOT_STARTED);
            expect(task.due_date).not.toBeNull();

            const dueDate = new Date(task.due_date);
            const today = new Date();
            today.setUTCHours(0, 0, 0, 0);
            const tomorrow = new Date(today);
            tomorrow.setDate(tomorrow.getDate() + 1);

            // Due date should be approximately today or tomorrow
            expect(dueDate.getTime()).toBeGreaterThanOrEqual(today.getTime());
            expect(dueDate.getTime()).toBeLessThan(
                tomorrow.getTime() + 24 * 60 * 60 * 1000
            );
        });
    });

    describe('Service Function Tests', () => {
        it('calculateNextDueDate should return null for invalid inputs', () => {
            expect(calculateNextDueDate(null, new Date())).toBeNull();
            expect(
                calculateNextDueDate({ recurrence_type: null }, new Date())
            ).toBeNull();
            expect(
                calculateNextDueDate({ recurrence_type: 'daily' }, null)
            ).toBeNull();
            expect(
                calculateNextDueDate(
                    { recurrence_type: 'daily' },
                    new Date('invalid')
                )
            ).toBeNull();
        });

        it('calculateNextDueDate should handle unknown recurrence types', () => {
            const result = calculateNextDueDate(
                { recurrence_type: 'unknown_type' },
                new Date()
            );
            expect(result).toBeNull();
        });
    });

    describe('Recurring Task Defer Until Validation', () => {
        it('should allow defer_until after due_date for recurring instance with end date', async () => {
            // Create parent recurring task with end date
            const parentTask = await Task.create({
                name: 'Recurring Parent',
                recurrence_type: 'daily',
                recurrence_interval: 1,
                recurrence_end_date: new Date('2026-12-31'),
                due_date: new Date('2026-03-01'),
                user_id: user.id,
                status: 0,
            });

            // Create instance with recurring_parent_id
            const response = await agent.post('/api/task').send({
                name: 'Instance Task',
                due_date: '2026-03-10',
                defer_until: '2026-03-15', // After due_date but before end_date
                recurring_parent_id: parentTask.id,
            });

            expect(response.status).toBe(201);
            expect(response.body.defer_until).toBeTruthy();
        });

        it('should reject defer_until after recurrence_end_date', async () => {
            const parentTask = await Task.create({
                name: 'Recurring Parent',
                recurrence_type: 'daily',
                recurrence_interval: 1,
                recurrence_end_date: new Date('2026-03-20'),
                due_date: new Date('2026-03-01'),
                user_id: user.id,
                status: 0,
            });

            const response = await agent.post('/api/task').send({
                name: 'Instance Task',
                due_date: '2026-03-10',
                defer_until: '2026-03-25', // After end_date
                recurring_parent_id: parentTask.id,
            });

            expect(response.status).toBe(400);
            expect(response.body.error).toContain('recurring task end date');
        });

        it('should allow any defer_until for recurring instance with no end date', async () => {
            const parentTask = await Task.create({
                name: 'Recurring Parent',
                recurrence_type: 'daily',
                recurrence_interval: 1,
                recurrence_end_date: null, // No end date
                due_date: new Date('2026-03-01'),
                user_id: user.id,
                status: 0,
            });

            const response = await agent.post('/api/task').send({
                name: 'Instance Task',
                due_date: '2026-03-10',
                defer_until: '2027-12-31', // Far in future
                recurring_parent_id: parentTask.id,
            });

            expect(response.status).toBe(201);
            expect(response.body.defer_until).toBeTruthy();
        });

        it('should update defer_until on recurring instance via PATCH', async () => {
            const parentTask = await Task.create({
                name: 'Recurring Parent',
                recurrence_type: 'daily',
                recurrence_interval: 1,
                recurrence_end_date: new Date('2026-12-31'),
                due_date: new Date('2026-03-01'),
                user_id: user.id,
                status: 0,
            });

            const instanceTask = await Task.create({
                name: 'Instance Task',
                due_date: new Date('2026-03-10'),
                recurring_parent_id: parentTask.id,
                user_id: user.id,
                status: 0,
            });

            const response = await agent
                .patch(`/api/task/${instanceTask.uid}`)
                .send({
                    defer_until: '2026-03-15', // After due_date but before end_date
                });

            expect(response.status).toBe(200);
            expect(response.body.defer_until).toBeTruthy();
        });

        it('should still enforce strict validation for non-recurring tasks', async () => {
            const response = await agent.post('/api/task').send({
                name: 'Regular Task',
                due_date: '2026-03-10',
                defer_until: '2026-03-15', // After due_date
                recurrence_type: 'none',
            });

            expect(response.status).toBe(400);
            expect(response.body.error).toContain(
                'cannot be after the due date'
            );
        });

        it('should handle invalid recurring_parent_id gracefully', async () => {
            const response = await agent.post('/api/task').send({
                name: 'Instance Task',
                due_date: '2026-03-10',
                defer_until: '2026-03-15',
                recurring_parent_id: 999999, // Non-existent parent
            });

            // Should fall back to strict validation
            expect(response.status).toBe(400);
            expect(response.body.error).toContain(
                'cannot be after the due date'
            );
        });
    });
});
