import { getNextRepeatOccurrence } from './get-next-repeat-occurrence.util';
import { DEFAULT_TASK_REPEAT_CFG, TaskRepeatCfg } from '../task-repeat-cfg.model';
import { getDbDateStr } from '../../../util/get-db-date-str';
import { Log } from '../../../core/log';

const FAKE_MONDAY_THE_10TH = new Date(2022, 0, 10).getTime();

const DUMMY_REPEATABLE_TASK: TaskRepeatCfg = {
  ...DEFAULT_TASK_REPEAT_CFG,
  id: 'REPEATABLE_DEFAULT',
  title: 'REPEATABLE_DEFAULT',
  quickSetting: 'DAILY',
  lastTaskCreationDay: getDbDateStr(FAKE_MONDAY_THE_10TH),
  defaultEstimate: undefined,
  projectId: null,
  startTime: undefined,
  remindAt: undefined,
  isPaused: false,
  repeatCycle: 'WEEKLY',
  startDate: getDbDateStr(FAKE_MONDAY_THE_10TH),
  repeatEvery: 1,
  monday: false,
  tuesday: false,
  wednesday: false,
  thursday: false,
  friday: false,
  saturday: false,
  sunday: false,
  tagIds: [],
  order: 0,
};

const HOUR = 60 * 60 * 1000;
const DAY = 24 * HOUR;

const dummyRepeatable = (id: string, fields: Partial<TaskRepeatCfg>): TaskRepeatCfg => ({
  ...DUMMY_REPEATABLE_TASK,
  id,
  ...fields,
});

const testCase = (
  cfg: TaskRepeatCfg,
  fromDate: Date,
  startDate: Date | number,
  expected: Date | null,
): void => {
  if (expected) {
    expected.setHours(12, 0, 0, 0);
  }

  expect(
    getNextRepeatOccurrence(
      {
        ...cfg,
        startDate: getDbDateStr(startDate),
      },
      fromDate,
    ),
  ).toEqual(expected);
};

describe('getNextRepeatOccurrence()', () => {
  describe('Input validation', () => {
    it('should use fallback date (1970-01-01) when startDate is not defined', () => {
      const cfg = dummyRepeatable('ID1', {
        startDate: undefined as any,
        repeatCycle: 'DAILY',
        repeatEvery: 1,
        lastTaskCreationDay: '1970-01-01',
      });
      const today = new Date(2022, 0, 10);
      // Should not throw, and should return a valid date using the fallback
      const result = getNextRepeatOccurrence(cfg, today);
      expect(result).toBeTruthy();
    });

    it('should return null and warn if repeatEvery is not a positive integer', () => {
      spyOn(Log, 'warn');

      const cfg1 = dummyRepeatable('ID1', {
        repeatEvery: 0,
      });
      expect(getNextRepeatOccurrence(cfg1, new Date())).toBeNull();

      const cfg2 = dummyRepeatable('ID1', {
        repeatEvery: -1,
      });
      expect(getNextRepeatOccurrence(cfg2, new Date())).toBeNull();

      const cfg3 = dummyRepeatable('ID1', {
        repeatEvery: 1.5,
      });
      expect(getNextRepeatOccurrence(cfg3, new Date())).toBeNull();

      expect(Log.warn).toHaveBeenCalledTimes(3);
    });
  });

  describe('DAILY', () => {
    it('should return tomorrow for daily task created today', () => {
      const today = new Date(2022, 0, 10); // January 10, 2022
      const tomorrow = new Date(2022, 0, 11); // January 11, 2022
      const cfg = dummyRepeatable('ID1', {
        repeatCycle: 'DAILY',
        repeatEvery: 1,
        lastTaskCreationDay: getDbDateStr(today),
      });
      testCase(cfg, today, today, tomorrow);
    });

    it('should return correct date for every 2 days pattern', () => {
      const startDate = new Date(2022, 0, 10);
      const lastCreation = new Date(2022, 0, 10);
      const fromDate = new Date(2022, 0, 11);
      const expected = new Date(2022, 0, 12);
      const cfg = dummyRepeatable('ID1', {
        repeatCycle: 'DAILY',
        repeatEvery: 2,
        lastTaskCreationDay: getDbDateStr(lastCreation),
      });
      testCase(cfg, fromDate, startDate, expected);
    });

    it('should skip to next occurrence if last creation was in the past', () => {
      const startDate = new Date(2022, 0, 1);
      const lastCreation = new Date(2022, 0, 5);
      const fromDate = new Date(2022, 0, 10);
      const expected = new Date(2022, 0, 11); // Next daily occurrence after Jan 10
      const cfg = dummyRepeatable('ID1', {
        repeatCycle: 'DAILY',
        repeatEvery: 1,
        lastTaskCreationDay: getDbDateStr(lastCreation),
      });
      testCase(cfg, fromDate, startDate, expected);
    });

    it('should handle every 3 days pattern correctly', () => {
      const startDate = new Date(2022, 0, 1); // Saturday
      const lastCreation = new Date(2022, 0, 10); // Monday (day 9 from start)
      const fromDate = new Date(2022, 0, 11);
      const expected = new Date(2022, 0, 13); // Thursday (day 12 from start)
      const cfg = dummyRepeatable('ID1', {
        repeatCycle: 'DAILY',
        repeatEvery: 3,
        lastTaskCreationDay: getDbDateStr(lastCreation),
      });
      testCase(cfg, fromDate, startDate, expected);
    });
  });

  describe('WEEKLY', () => {
    it('should return next week for weekly task on same weekday', () => {
      const startDate = new Date(2022, 0, 10); // Monday
      const lastCreation = new Date(2022, 0, 10);
      const fromDate = new Date(2022, 0, 11);
      const expected = new Date(2022, 0, 17); // Next Monday
      const cfg = dummyRepeatable('ID1', {
        repeatCycle: 'WEEKLY',
        repeatEvery: 1,
        monday: true,
        lastTaskCreationDay: getDbDateStr(lastCreation),
      });
      testCase(cfg, fromDate, startDate, expected);
    });

    it('should return correct day for multiple weekdays pattern', () => {
      const startDate = new Date(2022, 0, 10); // Monday
      const lastCreation = new Date(2022, 0, 10);
      const fromDate = new Date(2022, 0, 11); // Tuesday
      const expected = new Date(2022, 0, 12); // Wednesday
      const cfg = dummyRepeatable('ID1', {
        repeatCycle: 'WEEKLY',
        repeatEvery: 1,
        monday: true,
        wednesday: true,
        friday: true,
        lastTaskCreationDay: getDbDateStr(lastCreation),
      });
      testCase(cfg, fromDate, startDate, expected);
    });

    it('should handle every 2 weeks pattern', () => {
      const startDate = new Date(2022, 0, 3); // Monday Jan 3
      const lastCreation = new Date(2022, 0, 17); // Monday Jan 17 (2 weeks later)
      const fromDate = new Date(2022, 0, 18);
      const expected = new Date(2022, 0, 31); // Monday Jan 31 (2 weeks after Jan 17)
      const cfg = dummyRepeatable('ID1', {
        repeatCycle: 'WEEKLY',
        repeatEvery: 2,
        monday: true,
        lastTaskCreationDay: getDbDateStr(lastCreation),
      });
      testCase(cfg, fromDate, startDate, expected);
    });

    it('should find next valid weekday in current week', () => {
      const startDate = new Date(2022, 0, 10); // Monday
      const lastCreation = new Date(2022, 0, 10);
      const fromDate = new Date(2022, 0, 10); // Still Monday
      const expected = new Date(2022, 0, 11); // Tuesday
      const cfg = dummyRepeatable('ID1', {
        repeatCycle: 'WEEKLY',
        repeatEvery: 1,
        tuesday: true,
        thursday: true,
        lastTaskCreationDay: getDbDateStr(lastCreation),
      });
      testCase(cfg, fromDate, startDate, expected);
    });
  });

  describe('MONTHLY', () => {
    it('should return same day next month for monthly task', () => {
      const startDate = new Date(2022, 0, 15);
      const lastCreation = new Date(2022, 0, 15);
      const fromDate = new Date(2022, 0, 16);
      const expected = new Date(2022, 1, 15);
      const cfg = dummyRepeatable('ID1', {
        repeatCycle: 'MONTHLY',
        repeatEvery: 1,
        lastTaskCreationDay: getDbDateStr(lastCreation),
      });
      testCase(cfg, fromDate, startDate, expected);
    });

    it('should handle end-of-month dates correctly', () => {
      const startDate = new Date(2022, 0, 31);
      const lastCreation = new Date(2022, 0, 31);
      const fromDate = new Date(2022, 1, 1);
      const expected = new Date(2022, 1, 28); // Feb doesn't have 31 days
      const cfg = dummyRepeatable('ID1', {
        repeatCycle: 'MONTHLY',
        repeatEvery: 1,
        lastTaskCreationDay: getDbDateStr(lastCreation),
      });
      testCase(cfg, fromDate, startDate, expected);
    });

    it('should handle every 2 months pattern', () => {
      const startDate = new Date(2022, 0, 15);
      const lastCreation = new Date(2022, 0, 15);
      const fromDate = new Date(2022, 0, 16);
      const expected = new Date(2022, 2, 15); // 2 months later
      const cfg = dummyRepeatable('ID1', {
        repeatCycle: 'MONTHLY',
        repeatEvery: 2,
        lastTaskCreationDay: getDbDateStr(lastCreation),
      });
      testCase(cfg, fromDate, startDate, expected);
    });

    it('should handle leap year correctly', () => {
      const startDate = new Date(2024, 0, 31);
      const lastCreation = new Date(2024, 0, 31);
      const fromDate = new Date(2024, 1, 1);
      const expected = new Date(2024, 1, 29); // Leap year
      const cfg = dummyRepeatable('ID1', {
        repeatCycle: 'MONTHLY',
        repeatEvery: 1,
        lastTaskCreationDay: getDbDateStr(lastCreation),
      });
      testCase(cfg, fromDate, startDate, expected);
    });
  });

  describe('MONTHLY monthlyLastDay (issue #7726)', () => {
    it('returns the last day of the next month', () => {
      const cfg = dummyRepeatable('ID1', {
        repeatCycle: 'MONTHLY',
        repeatEvery: 1,
        monthlyLastDay: true,
        lastTaskCreationDay: getDbDateStr(new Date(2026, 4, 31)),
      });
      testCase(cfg, new Date(2026, 5, 1), new Date(2026, 4, 31), new Date(2026, 5, 30));
    });

    it('clamps to month-end even when startDate day-of-month is 30', () => {
      // A recurrence set up in a 30-day month must still hit 31 in long
      // months — the anchor is decoupled from startDate's day.
      const cfg = dummyRepeatable('ID1', {
        repeatCycle: 'MONTHLY',
        repeatEvery: 1,
        monthlyLastDay: true,
        lastTaskCreationDay: getDbDateStr(new Date(2026, 5, 30)),
      });
      testCase(cfg, new Date(2026, 6, 1), new Date(2026, 5, 30), new Date(2026, 6, 31));
    });

    it('clamps to February month-end', () => {
      const cfg = dummyRepeatable('ID1', {
        repeatCycle: 'MONTHLY',
        repeatEvery: 1,
        monthlyLastDay: true,
        lastTaskCreationDay: getDbDateStr(new Date(2026, 0, 31)),
      });
      testCase(cfg, new Date(2026, 1, 1), new Date(2026, 0, 31), new Date(2026, 1, 28));
    });

    it('clamps to February 29 in a leap year', () => {
      const cfg = dummyRepeatable('ID1', {
        repeatCycle: 'MONTHLY',
        repeatEvery: 1,
        monthlyLastDay: true,
        lastTaskCreationDay: getDbDateStr(new Date(2024, 0, 31)),
      });
      testCase(cfg, new Date(2024, 1, 1), new Date(2024, 0, 31), new Date(2024, 1, 29));
    });
  });

  describe('MONTHLY Nth weekday (issue #6040)', () => {
    it('returns the first Thursday of next month', () => {
      const startDate = new Date(2026, 0, 1); // Jan 1, 2026 (Thursday)
      const lastCreation = new Date(2026, 0, 1); // first Thu of Jan 2026 was Jan 1
      const fromDate = new Date(2026, 0, 2);
      const expected = new Date(2026, 1, 5); // first Thu of Feb 2026
      const cfg = dummyRepeatable('ID-NTH-1', {
        repeatCycle: 'MONTHLY',
        repeatEvery: 1,
        monthlyWeekOfMonth: 1,
        monthlyWeekday: 4, // Thursday
        lastTaskCreationDay: getDbDateStr(lastCreation),
      });
      testCase(cfg, fromDate, startDate, expected);
    });

    it('returns the last Monday of next month', () => {
      const startDate = new Date(2026, 0, 26); // Jan 26, 2026 (last Mon)
      const lastCreation = new Date(2026, 0, 26);
      const fromDate = new Date(2026, 0, 27);
      const expected = new Date(2026, 1, 23); // last Mon of Feb 2026
      const cfg = dummyRepeatable('ID-NTH-2', {
        repeatCycle: 'MONTHLY',
        repeatEvery: 1,
        monthlyWeekOfMonth: -1,
        monthlyWeekday: 1, // Monday
        lastTaskCreationDay: getDbDateStr(lastCreation),
      });
      testCase(cfg, fromDate, startDate, expected);
    });

    it('handles a month with 5 occurrences of a weekday correctly when "last" is requested', () => {
      // March 2026 has 5 Tuesdays (3, 10, 17, 24, 31). Last = the 31st.
      const startDate = new Date(2026, 1, 24); // last Tue of Feb 2026
      const lastCreation = new Date(2026, 1, 24);
      const fromDate = new Date(2026, 1, 25);
      const expected = new Date(2026, 2, 31); // last Tue of Mar 2026
      const cfg = dummyRepeatable('ID-NTH-4', {
        repeatCycle: 'MONTHLY',
        repeatEvery: 1,
        monthlyWeekOfMonth: -1,
        monthlyWeekday: 2, // Tuesday
        lastTaskCreationDay: getDbDateStr(lastCreation),
      });
      testCase(cfg, fromDate, startDate, expected);
    });

    it('respects repeatEvery > 1 (every 2 months)', () => {
      const startDate = new Date(2026, 0, 1); // Jan 1, 2026 (1st Thu of Jan)
      const lastCreation = new Date(2026, 0, 1);
      const fromDate = new Date(2026, 0, 2);
      const expected = new Date(2026, 2, 5); // 1st Thu of Mar (skip Feb)
      const cfg = dummyRepeatable('ID-NTH-5', {
        repeatCycle: 'MONTHLY',
        repeatEvery: 2,
        monthlyWeekOfMonth: 1,
        monthlyWeekday: 4,
        lastTaskCreationDay: getDbDateStr(lastCreation),
      });
      testCase(cfg, fromDate, startDate, expected);
    });
  });

  describe('YEARLY', () => {
    it('should return same date next year for yearly task', () => {
      const startDate = new Date(2022, 2, 15);
      const lastCreation = new Date(2022, 2, 15);
      const fromDate = new Date(2022, 2, 16);
      const expected = new Date(2023, 2, 15);
      const cfg = dummyRepeatable('ID1', {
        repeatCycle: 'YEARLY',
        repeatEvery: 1,
        lastTaskCreationDay: getDbDateStr(lastCreation),
      });
      testCase(cfg, fromDate, startDate, expected);
    });

    it('should handle every 2 years pattern', () => {
      const startDate = new Date(2022, 2, 15);
      const lastCreation = new Date(2022, 2, 15);
      const fromDate = new Date(2022, 2, 16);
      const expected = new Date(2024, 2, 15); // 2 years later
      const cfg = dummyRepeatable('ID1', {
        repeatCycle: 'YEARLY',
        repeatEvery: 2,
        lastTaskCreationDay: getDbDateStr(lastCreation),
      });
      testCase(cfg, fromDate, startDate, expected);
    });

    it('should handle leap year date (Feb 29)', () => {
      const startDate = new Date(2024, 1, 29);
      const lastCreation = new Date(2024, 1, 29);
      const fromDate = new Date(2024, 2, 1);
      const expected = new Date(2025, 1, 28); // Non-leap year
      const cfg = dummyRepeatable('ID1', {
        repeatCycle: 'YEARLY',
        repeatEvery: 1,
        lastTaskCreationDay: getDbDateStr(lastCreation),
      });
      testCase(cfg, fromDate, startDate, expected);
    });

    it('should return this year if date hasnt passed yet', () => {
      const startDate = new Date(2021, 11, 25);
      const lastCreation = new Date(2021, 11, 25);
      const fromDate = new Date(2022, 0, 1);
      const expected = new Date(2022, 11, 25); // This year
      const cfg = dummyRepeatable('ID1', {
        repeatCycle: 'YEARLY',
        repeatEvery: 1,
        lastTaskCreationDay: getDbDateStr(lastCreation),
      });
      testCase(cfg, fromDate, startDate, expected);
    });
  });

  describe('Timezone Edge Cases', () => {
    describe('DST transitions', () => {
      it('should handle spring DST transition (clocks forward)', () => {
        // March 13, 2022 is when DST starts in US (2 AM becomes 3 AM)
        const cfg = dummyRepeatable('ID1', {
          repeatCycle: 'DAILY',
          repeatEvery: 1,
          lastTaskCreationDay: getDbDateStr(new Date(2022, 2, 12).getTime()),
        });
        const fromDate = new Date(2022, 2, 12);
        const startDate = new Date(2022, 2, 12);
        const expected = new Date(2022, 2, 13);
        expected.setHours(12, 0, 0, 0);

        const result = getNextRepeatOccurrence(
          { ...cfg, startDate: getDbDateStr(startDate) },
          fromDate,
        );
        expect(result).toEqual(expected);
      });

      it('should handle fall DST transition (clocks back)', () => {
        // November 6, 2022 is when DST ends in US (2 AM becomes 1 AM)
        const cfg = dummyRepeatable('ID1', {
          repeatCycle: 'DAILY',
          repeatEvery: 1,
          lastTaskCreationDay: getDbDateStr(new Date(2022, 10, 5).getTime()),
        });
        const fromDate = new Date(2022, 10, 5);
        const startDate = new Date(2022, 10, 5);
        const expected = new Date(2022, 10, 6);
        expected.setHours(12, 0, 0, 0);

        const result = getNextRepeatOccurrence(
          { ...cfg, startDate: getDbDateStr(startDate) },
          fromDate,
        );
        expect(result).toEqual(expected);
      });
    });

    describe('Year boundary crossing', () => {
      it('should handle daily repeat across year boundary', () => {
        const cfg = dummyRepeatable('ID1', {
          repeatCycle: 'DAILY',
          repeatEvery: 1,
          lastTaskCreationDay: getDbDateStr(new Date(2021, 11, 31).getTime()),
        });
        const fromDate = new Date(2021, 11, 31);
        const startDate = new Date(2021, 11, 30);
        const expected = new Date(2022, 0, 1);
        expected.setHours(12, 0, 0, 0);

        const result = getNextRepeatOccurrence(
          { ...cfg, startDate: getDbDateStr(startDate) },
          fromDate,
        );
        expect(result).toEqual(expected);
      });

      it('should handle weekly repeat across year boundary', () => {
        const cfg = dummyRepeatable('ID1', {
          repeatCycle: 'WEEKLY',
          repeatEvery: 1,
          lastTaskCreationDay: getDbDateStr(new Date(2021, 11, 27).getTime()), // Monday
          monday: true,
        });
        const fromDate = new Date(2021, 11, 28); // Tuesday
        const startDate = new Date(2021, 11, 27);
        const expected = new Date(2022, 0, 3); // Next Monday
        expected.setHours(12, 0, 0, 0);

        const result = getNextRepeatOccurrence(
          { ...cfg, startDate: getDbDateStr(startDate) },
          fromDate,
        );
        expect(result).toEqual(expected);
      });

      it('should handle monthly repeat across year boundary', () => {
        const cfg = dummyRepeatable('ID1', {
          repeatCycle: 'MONTHLY',
          repeatEvery: 1,
          lastTaskCreationDay: getDbDateStr(new Date(2021, 11, 15).getTime()),
        });
        const fromDate = new Date(2021, 11, 16);
        const startDate = new Date(2021, 10, 15);
        const expected = new Date(2022, 0, 15);
        expected.setHours(12, 0, 0, 0);

        const result = getNextRepeatOccurrence(
          { ...cfg, startDate: getDbDateStr(startDate) },
          fromDate,
        );
        expect(result).toEqual(expected);
      });

      it('should handle yearly repeat across decade boundary', () => {
        const cfg = dummyRepeatable('ID1', {
          repeatCycle: 'YEARLY',
          repeatEvery: 1,
          lastTaskCreationDay: getDbDateStr(new Date(2019, 5, 15).getTime()),
        });
        const fromDate = new Date(2019, 5, 16);
        const startDate = new Date(2019, 5, 15);
        const expected = new Date(2020, 5, 15);
        expected.setHours(12, 0, 0, 0);

        const result = getNextRepeatOccurrence(
          { ...cfg, startDate: getDbDateStr(startDate) },
          fromDate,
        );
        expect(result).toEqual(expected);
      });
    });

    describe('Leap year edge cases', () => {
      it('should handle February 29 in leap year for monthly repeat', () => {
        const cfg = dummyRepeatable('ID1', {
          repeatCycle: 'MONTHLY',
          repeatEvery: 1,
          lastTaskCreationDay: getDbDateStr(new Date(2024, 0, 29).getTime()),
        });
        const fromDate = new Date(2024, 0, 30);
        const startDate = new Date(2024, 0, 29);
        const expected = new Date(2024, 1, 29);
        expected.setHours(12, 0, 0, 0);

        const result = getNextRepeatOccurrence(
          { ...cfg, startDate: getDbDateStr(startDate) },
          fromDate,
        );
        expect(result).toEqual(expected);
      });

      it('should handle February 29 yearly repeat in non-leap year', () => {
        const cfg = dummyRepeatable('ID1', {
          repeatCycle: 'YEARLY',
          repeatEvery: 1,
          lastTaskCreationDay: getDbDateStr(new Date(2024, 1, 29).getTime()),
        });
        const fromDate = new Date(2024, 2, 1);
        const startDate = new Date(2024, 1, 29);
        const expected = new Date(2025, 1, 28); // Non-leap year
        expected.setHours(12, 0, 0, 0);

        const result = getNextRepeatOccurrence(
          { ...cfg, startDate: getDbDateStr(startDate) },
          fromDate,
        );
        expect(result).toEqual(expected);
      });

      it('should handle February 29 to February 28 transition for yearly repeat', () => {
        // Testing yearly repeat starting from Feb 29 in a leap year
        // When the next year is not a leap year, it should use Feb 28
        const cfg = dummyRepeatable('ID1', {
          repeatCycle: 'YEARLY',
          repeatEvery: 1,
          lastTaskCreationDay: getDbDateStr(new Date(2024, 1, 29).getTime()),
        });
        const fromDate = new Date(2024, 2, 1);
        const startDate = new Date(2024, 1, 29);
        // 2025 is not a leap year, so Feb 29 doesn't exist - should be Feb 28
        const expected = new Date(2025, 1, 28);
        expected.setHours(12, 0, 0, 0);

        const result = getNextRepeatOccurrence(
          { ...cfg, startDate: getDbDateStr(startDate) },
          fromDate,
        );
        expect(result).toEqual(expected);
      });

      it('should handle century leap year edge case', () => {
        // 2100 is not a leap year (divisible by 100 but not 400)
        const cfg = dummyRepeatable('ID1', {
          repeatCycle: 'YEARLY',
          repeatEvery: 1,
          lastTaskCreationDay: getDbDateStr(new Date(2099, 1, 28).getTime()),
        });
        const fromDate = new Date(2099, 2, 1);
        const startDate = new Date(2099, 1, 28);
        const expected = new Date(2100, 1, 28); // Not a leap year
        expected.setHours(12, 0, 0, 0);

        const result = getNextRepeatOccurrence(
          { ...cfg, startDate: getDbDateStr(startDate) },
          fromDate,
        );
        expect(result).toEqual(expected);
      });
    });

    describe('Midnight and near-midnight times', () => {
      it('should handle task created at 23:59:59', () => {
        const lastCreation = new Date(2022, 0, 10, 23, 59, 59, 999);
        const cfg = dummyRepeatable('ID1', {
          repeatCycle: 'DAILY',
          repeatEvery: 1,
          lastTaskCreationDay: getDbDateStr(lastCreation.getTime()),
        });
        const fromDate = new Date(2022, 0, 10, 23, 59, 59, 999);
        const startDate = new Date(2022, 0, 10);
        // Since lastCreation is Jan 10 23:59:59 and fromDate is also Jan 10 23:59:59,
        // the function will start checking from the day after lastTaskCreation
        // which would be Jan 11
        const expected = new Date(2022, 0, 11, 12, 0, 0, 0);

        const result = getNextRepeatOccurrence(
          { ...cfg, startDate: getDbDateStr(startDate) },
          fromDate,
        );
        expect(result).toEqual(expected);
      });

      it('should handle task created at 00:00:01', () => {
        const lastCreation = new Date(2022, 0, 11, 0, 0, 1, 0);
        const cfg = dummyRepeatable('ID1', {
          repeatCycle: 'DAILY',
          repeatEvery: 1,
          lastTaskCreationDay: getDbDateStr(lastCreation.getTime()),
        });
        const fromDate = new Date(2022, 0, 11, 0, 0, 1, 0);
        const startDate = new Date(2022, 0, 10);
        const expected = new Date(2022, 0, 12, 12, 0, 0, 0);

        const result = getNextRepeatOccurrence(
          { ...cfg, startDate: getDbDateStr(startDate) },
          fromDate,
        );
        expect(result).toEqual(expected);
      });

      it('should handle weekly repeat with midnight crossing', () => {
        const lastCreation = new Date(2022, 0, 9, 23, 59, 59, 999); // Sunday night
        const cfg = dummyRepeatable('ID1', {
          repeatCycle: 'WEEKLY',
          repeatEvery: 1,
          lastTaskCreationDay: getDbDateStr(lastCreation.getTime()),
          monday: true,
        });
        const fromDate = new Date(2022, 0, 10, 0, 0, 1, 0); // Monday morning
        const startDate = new Date(2022, 0, 3); // Previous Monday
        const expected = new Date(2022, 0, 17); // Next Monday
        expected.setHours(12, 0, 0, 0);

        const result = getNextRepeatOccurrence(
          { ...cfg, startDate: getDbDateStr(startDate) },
          fromDate,
        );
        expect(result).toEqual(expected);
      });
    });

    describe('Month-end date handling', () => {
      it('should handle 31st to 30-day month transition', () => {
        const cfg = dummyRepeatable('ID1', {
          repeatCycle: 'MONTHLY',
          repeatEvery: 1,
          lastTaskCreationDay: getDbDateStr(new Date(2022, 2, 31).getTime()),
        });
        const fromDate = new Date(2022, 3, 1);
        const startDate = new Date(2022, 0, 31);
        const expected = new Date(2022, 3, 30); // April has 30 days
        expected.setHours(12, 0, 0, 0);

        const result = getNextRepeatOccurrence(
          { ...cfg, startDate: getDbDateStr(startDate) },
          fromDate,
        );
        expect(result).toEqual(expected);
      });

      it('should handle 30th to February transition', () => {
        const cfg = dummyRepeatable('ID1', {
          repeatCycle: 'MONTHLY',
          repeatEvery: 1,
          lastTaskCreationDay: getDbDateStr(new Date(2022, 0, 30).getTime()),
        });
        const fromDate = new Date(2022, 0, 31);
        const startDate = new Date(2021, 10, 30);
        const expected = new Date(2022, 1, 28); // February in non-leap year
        expected.setHours(12, 0, 0, 0);

        const result = getNextRepeatOccurrence(
          { ...cfg, startDate: getDbDateStr(startDate) },
          fromDate,
        );
        expect(result).toEqual(expected);
      });

      it('should handle all months correctly for 31st start date', () => {
        const startDate = new Date(2022, 0, 31);
        const testCases = [
          { month: '2022-02', expectedDay: 28 }, // Feb non-leap
          { month: '2022-04', expectedDay: 30 }, // Apr
          { month: '2022-06', expectedDay: 30 }, // Jun
          { month: '2022-09', expectedDay: 30 }, // Sep
          { month: '2022-11', expectedDay: 30 }, // Nov
        ];

        testCases.forEach(({ month, expectedDay }) => {
          const [year, monthNum] = month.split('-').map(Number);
          const lastCreation = new Date(year, monthNum - 1, 1);
          lastCreation.setDate(lastCreation.getDate() - 1); // Previous month

          const cfg = dummyRepeatable('ID1', {
            repeatCycle: 'MONTHLY',
            repeatEvery: 1,
            lastTaskCreationDay: getDbDateStr(lastCreation.getTime()),
          });

          const fromDate = new Date(year, monthNum - 1, 1);
          const expected = new Date(year, monthNum - 1, expectedDay);
          expected.setHours(12, 0, 0, 0);

          const result = getNextRepeatOccurrence(
            { ...cfg, startDate: getDbDateStr(startDate) },
            fromDate,
          );

          expect(result?.getDate()).toBe(expectedDay);
        });
      });
    });

    describe('Multi-timezone scenario simulations', () => {
      it('should handle task created in one timezone and checked in another', () => {
        // Task created at 11 PM LA time on Jan 10
        const lastCreationLA = new Date(2022, 0, 10, 23, 0, 0);
        const cfg = dummyRepeatable('ID1', {
          repeatCycle: 'DAILY',
          repeatEvery: 1,
          lastTaskCreationDay: getDbDateStr(lastCreationLA.getTime()),
        });

        // Check on Jan 11
        const fromDateBerlin = new Date(2022, 0, 11, 9, 0, 0);
        const startDate = new Date(2022, 0, 10);
        const expected = new Date(2022, 0, 12, 12, 0, 0);
        expected.setHours(12, 0, 0, 0);

        const result = getNextRepeatOccurrence(
          { ...cfg, startDate: getDbDateStr(startDate) },
          fromDateBerlin,
        );
        expect(result).toEqual(expected);
      });

      it('should handle weekly repeat with timezone differences', () => {
        // Task repeats every Monday, created Sunday night
        const lastCreation = new Date(2022, 0, 9, 23, 0, 0); // Sunday 11 PM
        const cfg = dummyRepeatable('ID1', {
          repeatCycle: 'WEEKLY',
          repeatEvery: 1,
          lastTaskCreationDay: getDbDateStr(lastCreation.getTime()),
          monday: true,
        });

        // Check on Monday
        const fromDate = new Date(2022, 0, 10, 10, 0, 0); // Monday 10 AM
        const startDate = new Date(2022, 0, 3); // Previous Monday
        const expected = new Date(2022, 0, 17, 12, 0, 0); // Next Monday
        expected.setHours(12, 0, 0, 0);

        const result = getNextRepeatOccurrence(
          { ...cfg, startDate: getDbDateStr(startDate) },
          fromDate,
        );
        expect(result).toEqual(expected);
      });

      it('should handle International Date Line crossing', () => {
        // Task created on Jan 10
        const lastCreation = new Date(2022, 0, 10, 23, 0, 0);
        const cfg = dummyRepeatable('ID1', {
          repeatCycle: 'DAILY',
          repeatEvery: 1,
          lastTaskCreationDay: getDbDateStr(lastCreation.getTime()),
        });

        // Check on Jan 12
        const fromDate = new Date(2022, 0, 12, 1, 0, 0);
        const startDate = new Date(2022, 0, 10, 12, 0, 0);
        const expected = new Date(2022, 0, 12, 1, 0, 0);
        expected.setDate(expected.getDate() + 1);
        expected.setHours(12, 0, 0, 0);

        const result = getNextRepeatOccurrence(
          { ...cfg, startDate: getDbDateStr(startDate) },
          fromDate,
        );
        expect(result).toBeTruthy();
      });
    });
  });

  describe('Biweekly with weekdays (#6298)', () => {
    // Issue #6298: "Every 2 weeks Mon-Fri" starting Monday Feb 2, 2026
    // Friday was incorrectly shifted to the next week due to Math.round in getDiffInWeeks
    const START_DATE = new Date(2026, 1, 2); // Monday Feb 2, 2026

    const biweeklyCfg = (lastCreationDay: Date): TaskRepeatCfg =>
      dummyRepeatable('BIWEEKLY', {
        repeatCycle: 'WEEKLY',
        repeatEvery: 2,
        monday: true,
        tuesday: true,
        wednesday: true,
        thursday: true,
        friday: true,
        lastTaskCreationDay: getDbDateStr(lastCreationDay),
      });

    it('should include Friday of week 0 (same week as start)', () => {
      // After Thursday Feb 5, next should be Friday Feb 6 (still week 0)
      const cfg = biweeklyCfg(new Date(2026, 1, 5)); // Thu Feb 5
      testCase(cfg, new Date(2026, 1, 5), START_DATE, new Date(2026, 1, 6)); // Fri Feb 6
    });

    it('should NOT include any day in week 1 (off-week)', () => {
      // After Friday Feb 6 (end of week 0), next should jump to Monday Feb 16 (week 2)
      const cfg = biweeklyCfg(new Date(2026, 1, 6)); // Fri Feb 6
      testCase(cfg, new Date(2026, 1, 6), START_DATE, new Date(2026, 1, 16)); // Mon Feb 16
    });

    it('should produce correct 4-week schedule from Monday start', () => {
      // Week 0 (active): Mon Feb 2 - Fri Feb 6
      // Week 1 (off): no tasks
      // Week 2 (active): Mon Feb 16 - Fri Feb 20
      // Week 3 (off): no tasks

      // Starting from the start date, walk through the schedule
      let cfg = biweeklyCfg(new Date(2026, 0, 1)); // before start
      testCase(cfg, new Date(2026, 1, 1), START_DATE, new Date(2026, 1, 2)); // Mon Feb 2

      cfg = biweeklyCfg(new Date(2026, 1, 2));
      testCase(cfg, new Date(2026, 1, 2), START_DATE, new Date(2026, 1, 3)); // Tue Feb 3

      cfg = biweeklyCfg(new Date(2026, 1, 3));
      testCase(cfg, new Date(2026, 1, 3), START_DATE, new Date(2026, 1, 4)); // Wed Feb 4

      cfg = biweeklyCfg(new Date(2026, 1, 4));
      testCase(cfg, new Date(2026, 1, 4), START_DATE, new Date(2026, 1, 5)); // Thu Feb 5

      cfg = biweeklyCfg(new Date(2026, 1, 5));
      testCase(cfg, new Date(2026, 1, 5), START_DATE, new Date(2026, 1, 6)); // Fri Feb 6

      // Skip week 1 entirely, jump to week 2
      cfg = biweeklyCfg(new Date(2026, 1, 6));
      testCase(cfg, new Date(2026, 1, 6), START_DATE, new Date(2026, 1, 16)); // Mon Feb 16

      cfg = biweeklyCfg(new Date(2026, 1, 16));
      testCase(cfg, new Date(2026, 1, 16), START_DATE, new Date(2026, 1, 17)); // Tue Feb 17

      cfg = biweeklyCfg(new Date(2026, 1, 20));
      testCase(cfg, new Date(2026, 1, 20), START_DATE, new Date(2026, 2, 2)); // Mon Mar 2 (week 4)
    });
  });

  describe('Edge cases', () => {
    it('should return null for unknown repeat cycle', () => {
      const cfg = dummyRepeatable('ID1', {
        repeatCycle: 'UNKNOWN' as any,
        lastTaskCreationDay: getDbDateStr(FAKE_MONDAY_THE_10TH),
      });
      expect(getNextRepeatOccurrence(cfg, new Date())).toBeNull();
    });

    it('should handle when fromDate is before lastTaskCreation', () => {
      const startDate = new Date(2022, 0, 1);
      const lastCreation = new Date(2022, 0, 15);
      const fromDate = new Date(2022, 0, 10); // Before last creation
      const expected = new Date(2022, 0, 16); // Day after last creation
      const cfg = dummyRepeatable('ID1', {
        repeatCycle: 'DAILY',
        repeatEvery: 1,
        lastTaskCreationDay: getDbDateStr(lastCreation),
      });
      testCase(cfg, fromDate, startDate, expected);
    });

    it('should use default fromDate (today) when not provided', () => {
      const today = new Date();
      today.setHours(12, 0, 0, 0);
      const tomorrow = new Date(today);
      tomorrow.setDate(tomorrow.getDate() + 1);

      const cfg = dummyRepeatable('ID1', {
        repeatCycle: 'DAILY',
        repeatEvery: 1,
        lastTaskCreationDay: getDbDateStr(today.getTime() - DAY), // Yesterday
        startDate: getDbDateStr(today.getTime() - DAY),
      });

      const result = getNextRepeatOccurrence(cfg);
      expect(result).toBeTruthy();
      // Should be today or later
      expect(result!.getTime()).toBeGreaterThanOrEqual(today.getTime());
    });
  });

  // Issue #7951: the inclusive variant is used when relocating an existing live
  // instance on a schedule edit — `fromDate` (today) must be considered instead
  // of being skipped to the day after the last creation.
  describe('inclusive option (#7951)', () => {
    const noon = (d: Date): Date => {
      const r = new Date(d);
      r.setHours(12, 0, 0, 0);
      return r;
    };

    // Enable exactly one weekday (0 = Sunday … 6 = Saturday).
    const weekdayFlags = (enabledDay: number): Partial<TaskRepeatCfg> => ({
      sunday: enabledDay === 0,
      monday: enabledDay === 1,
      tuesday: enabledDay === 2,
      wednesday: enabledDay === 3,
      thursday: enabledDay === 4,
      friday: enabledDay === 5,
      saturday: enabledDay === 6,
    });

    it('returns today for a DAILY repeat created today (vs tomorrow when exclusive)', () => {
      const today = noon(new Date());
      const cfg = dummyRepeatable('ID1', {
        repeatCycle: 'DAILY',
        repeatEvery: 1,
        startDate: getDbDateStr(today),
        lastTaskCreationDay: getDbDateStr(today),
      });

      const tomorrow = noon(new Date(today.getTime() + DAY));
      expect(getNextRepeatOccurrence(cfg, today)).toEqual(tomorrow);
      expect(getNextRepeatOccurrence(cfg, today, { inclusive: true })).toEqual(today);
    });

    it('returns today for a WEEKLY repeat when today matches the enabled weekday', () => {
      const today = noon(new Date());
      const cfg = dummyRepeatable('ID1', {
        repeatCycle: 'WEEKLY',
        repeatEvery: 1,
        startDate: getDbDateStr(today),
        lastTaskCreationDay: getDbDateStr(today),
        ...weekdayFlags(today.getDay()),
      });

      expect(getNextRepeatOccurrence(cfg, today, { inclusive: true })).toEqual(today);
    });

    it('still skips today when today is not a valid occurrence', () => {
      const today = noon(new Date());
      const tomorrow = noon(new Date(today.getTime() + DAY));
      const cfg = dummyRepeatable('ID1', {
        repeatCycle: 'WEEKLY',
        repeatEvery: 1,
        startDate: getDbDateStr(today),
        lastTaskCreationDay: getDbDateStr(today),
        // only tomorrow's weekday is enabled
        ...weekdayFlags(tomorrow.getDay()),
      });

      expect(getNextRepeatOccurrence(cfg, today, { inclusive: true })).toEqual(tomorrow);
    });

    it('returns today for a MONTHLY repeat whose day-of-month is today', () => {
      const today = noon(new Date());
      const cfg = dummyRepeatable('ID1', {
        repeatCycle: 'MONTHLY',
        repeatEvery: 1,
        startDate: getDbDateStr(today),
        lastTaskCreationDay: getDbDateStr(today),
      });

      expect(getNextRepeatOccurrence(cfg, today, { inclusive: true })).toEqual(today);
    });

    it('does not resolve to a past MONTHLY occurrence when this month already passed', () => {
      // fromDate is the 16th; the monthly anchor (the 10th) already passed this
      // month, so the soonest inclusive occurrence is next month's 10th — never
      // the past June 10th. Exercises the floor guard, not just the
      // epoch-neutralised gating.
      const fromDate = noon(new Date(2026, 5, 16)); // Tue Jun 16 2026
      const cfg = dummyRepeatable('ID1', {
        repeatCycle: 'MONTHLY',
        repeatEvery: 1,
        startDate: '2026-01-10',
        lastTaskCreationDay: '2026-06-10',
      });

      expect(getNextRepeatOccurrence(cfg, fromDate, { inclusive: true })).toEqual(
        noon(new Date(2026, 6, 10)), // Jul 10 2026
      );
    });

    it('returns today for a YEARLY repeat whose month and day are today', () => {
      const today = noon(new Date());
      const cfg = dummyRepeatable('ID1', {
        repeatCycle: 'YEARLY',
        repeatEvery: 1,
        startDate: getDbDateStr(today),
        lastTaskCreationDay: getDbDateStr(today),
      });

      expect(getNextRepeatOccurrence(cfg, today, { inclusive: true })).toEqual(today);
    });

    it('does not resolve to a past YEARLY occurrence when this year already passed', () => {
      // Yearly anchor is March 10; fromDate is mid-June, so this year's
      // occurrence already passed → next year's March 10, never the past one.
      const fromDate = noon(new Date(2026, 5, 16)); // Tue Jun 16 2026
      const cfg = dummyRepeatable('ID1', {
        repeatCycle: 'YEARLY',
        repeatEvery: 1,
        startDate: '2020-03-10',
        lastTaskCreationDay: '2026-03-10',
      });

      expect(getNextRepeatOccurrence(cfg, fromDate, { inclusive: true })).toEqual(
        noon(new Date(2027, 2, 10)), // Mar 10 2027
      );
    });

    it('keeps the repeatFromCompletionDate anchor (lastTaskCreationDay), not startDate', () => {
      // repeatFromCompletionDate cfgs anchor the pattern on lastTaskCreationDay
      // (via getEffectiveRepeatStartDate). The inclusive path must preserve that
      // anchor — it only neutralises the prior-creation *gating*, computed after
      // the start anchor is resolved. With lastTaskCreationDay = today the
      // soonest inclusive daily occurrence is today (exclusive would be tomorrow).
      const today = noon(new Date());
      const cfg = dummyRepeatable('ID1', {
        repeatCycle: 'DAILY',
        repeatEvery: 1,
        repeatFromCompletionDate: true,
        // startDate is intentionally far in the past; for repeatFromCompletionDate
        // the pattern anchors on lastTaskCreationDay, not startDate.
        startDate: '2020-01-01',
        lastTaskCreationDay: getDbDateStr(today),
      });

      expect(getNextRepeatOccurrence(cfg, today, { inclusive: true })).toEqual(today);
    });

    it('respects repeatEvery when today is off-cycle (returns the next on-cycle day)', () => {
      // Every-2-days anchored on yesterday → today is off-cycle (diff 1, odd),
      // so inclusive must NOT force today; the next on-cycle day is tomorrow.
      const today = noon(new Date());
      const tomorrow = noon(new Date(today.getTime() + DAY));
      const cfg = dummyRepeatable('ID1', {
        repeatCycle: 'DAILY',
        repeatEvery: 2,
        startDate: getDbDateStr(new Date(today.getTime() - DAY)),
        lastTaskCreationDay: getDbDateStr(today),
      });

      expect(getNextRepeatOccurrence(cfg, today, { inclusive: true })).toEqual(tomorrow);
    });
  });
});
