import { TestBed } from '@angular/core/testing';
import { provideMockActions } from '@ngrx/effects/testing';
import { Observable, of, throwError } from 'rxjs';
import { Action } from '@ngrx/store';
import { TaskRepeatCfgEffects } from './task-repeat-cfg.effects';
import { TaskService } from '../../tasks/task.service';
import { TaskRepeatCfgService } from '../task-repeat-cfg.service';
import { MatDialog } from '@angular/material/dialog';
import { TaskArchiveService } from '../../archive/task-archive.service';
import { AddTasksForTomorrowService } from '../../add-tasks-for-tomorrow/add-tasks-for-tomorrow.service';
import { addTaskRepeatCfgToTask, updateTaskRepeatCfg } from './task-repeat-cfg.actions';
import {
  DEFAULT_TASK,
  Task,
  TaskWithSubTasks,
  TaskReminderOptionId,
} from '../../tasks/task.model';
import { DEFAULT_TASK_REPEAT_CFG, TaskRepeatCfgCopy } from '../task-repeat-cfg.model';
import { addSubTask } from '../../tasks/store/task.actions';
import { TestScheduler } from 'rxjs/testing';
import { TaskSharedActions } from '../../../root-store/meta/task-shared.actions';
import { PlannerActions } from '../../planner/store/planner.actions';
import { dateStrToUtcDate } from '../../../util/date-str-to-utc-date';
import { getDbDateStr } from '../../../util/get-db-date-str';
import { getDateTimeFromClockString } from '../../../util/get-date-time-from-clock-string';
import { remindOptionToMilliseconds } from '../../tasks/util/remind-option-to-milliseconds';
import { isToday } from '../../../util/is-today.util';
import { getFirstRepeatOccurrence } from './get-first-repeat-occurrence.util';
import { getNextRepeatOccurrence } from './get-next-repeat-occurrence.util';

describe('TaskRepeatCfgEffects - Repeatable Subtasks', () => {
  let actions$: Observable<Action>;
  let effects: TaskRepeatCfgEffects;
  let taskService: jasmine.SpyObj<TaskService>;
  let taskRepeatCfgService: jasmine.SpyObj<TaskRepeatCfgService>;
  let taskArchiveService: jasmine.SpyObj<TaskArchiveService>;
  let addTasksForTomorrowService: jasmine.SpyObj<AddTasksForTomorrowService>;
  let testScheduler: TestScheduler;

  const mockTask: Task = {
    ...DEFAULT_TASK,
    id: 'parent-task-id',
    title: 'Parent Task',
    projectId: 'test-project',
    repeatCfgId: 'repeat-cfg-id',
    subTaskIds: ['sub1', 'sub2'],
    created: Date.now(),
  };

  const mockSubTask1: Task = {
    ...DEFAULT_TASK,
    id: 'sub1',
    title: 'SubTask 1',
    projectId: 'test-project',
    notes: 'Notes 1',
    timeEstimate: 3600000,
    parentId: 'parent-task-id',
  };

  const mockSubTask2: Task = {
    ...DEFAULT_TASK,
    id: 'sub2',
    title: 'SubTask 2',
    projectId: 'test-project',
    notes: 'Notes 2',
    timeEstimate: 7200000,
    parentId: 'parent-task-id',
  };

  const mockTaskWithSubTasks: TaskWithSubTasks = {
    ...mockTask,
    subTasks: [mockSubTask1, mockSubTask2],
  };

  const mockRepeatCfg: TaskRepeatCfgCopy = {
    ...DEFAULT_TASK_REPEAT_CFG,
    id: 'repeat-cfg-id',
    lastTaskCreationDay: getDbDateStr(),
    shouldInheritSubtasks: true,
    disableAutoUpdateSubtasks: false,
    subTaskTemplates: [],
  };

  beforeEach(() => {
    const taskServiceSpy = jasmine.createSpyObj('TaskService', [
      'getByIdWithSubTaskData$',
      'getByIdOnce$',
      'getTasksByRepeatCfgId$',
      'getByIdsLive$',
      'getArchiveTasksForRepeatCfgId',
      'update',
    ]);

    const taskRepeatCfgServiceSpy = jasmine.createSpyObj('TaskRepeatCfgService', [
      'updateTaskRepeatCfg',
      'getTaskRepeatCfgById$',
      'getTaskRepeatCfgByIdAllowUndefined$',
      '_getActionsForTaskRepeatCfg',
    ]);
    // Default: config exists. Individual tests override; the "deleted config"
    // tests set this to of(undefined) to exercise the #8715 crash-guard.
    taskRepeatCfgServiceSpy.getTaskRepeatCfgByIdAllowUndefined$.and.returnValue(
      of(undefined),
    );

    const matDialogSpy = jasmine.createSpyObj('MatDialog', ['open']);
    const taskArchiveServiceSpy = jasmine.createSpyObj('TaskArchiveService', ['load']);
    const addTasksForTomorrowServiceSpy = jasmine.createSpyObj(
      'AddTasksForTomorrowService',
      ['addAllDueToday'],
    );
    addTasksForTomorrowServiceSpy.addAllDueToday.and.returnValue(Promise.resolve());
    TestBed.configureTestingModule({
      providers: [
        TaskRepeatCfgEffects,
        provideMockActions(() => actions$),
        { provide: TaskService, useValue: taskServiceSpy },
        { provide: TaskRepeatCfgService, useValue: taskRepeatCfgServiceSpy },
        { provide: MatDialog, useValue: matDialogSpy },
        { provide: TaskArchiveService, useValue: taskArchiveServiceSpy },
        {
          provide: AddTasksForTomorrowService,
          useValue: addTasksForTomorrowServiceSpy,
        },
      ],
    });

    effects = TestBed.inject(TaskRepeatCfgEffects);
    taskService = TestBed.inject(TaskService) as jasmine.SpyObj<TaskService>;
    taskRepeatCfgService = TestBed.inject(
      TaskRepeatCfgService,
    ) as jasmine.SpyObj<TaskRepeatCfgService>;
    taskArchiveService = TestBed.inject(
      TaskArchiveService,
    ) as jasmine.SpyObj<TaskArchiveService>;
    addTasksForTomorrowService = TestBed.inject(
      AddTasksForTomorrowService,
    ) as jasmine.SpyObj<AddTasksForTomorrowService>;

    testScheduler = new TestScheduler((actual, expected) => {
      expect(actual).toEqual(expected);
    });
  });

  describe('Helper Methods', () => {
    it('should convert tasks to subtask templates', () => {
      const subs = [mockSubTask1, mockSubTask2];
      const templates = (effects as any)._toSubTaskTemplates(subs);

      expect(templates).toEqual([
        {
          title: 'SubTask 1',
          notes: 'Notes 1',
          timeEstimate: 3600000,
        },
        {
          title: 'SubTask 2',
          notes: 'Notes 2',
          timeEstimate: 7200000,
        },
      ]);
    });

    it('should correctly compare template arrays for equality', () => {
      const templates1 = [
        { title: 'Task 1', notes: 'Notes 1', timeEstimate: 3600000 },
        { title: 'Task 2', notes: '', timeEstimate: 0 },
      ];

      const templates2 = [
        { title: 'Task 1', notes: 'Notes 1', timeEstimate: 3600000 },
        { title: 'Task 2', notes: '', timeEstimate: 0 },
      ];

      const templates3 = [
        { title: 'Task 1', notes: 'Notes 1', timeEstimate: 3600000 },
        { title: 'Task 2 Modified', notes: '', timeEstimate: 0 },
      ];

      expect((effects as any)._templatesEqual(templates1, templates2)).toBe(true);
      expect((effects as any)._templatesEqual(templates1, templates3)).toBe(false);
      expect((effects as any)._templatesEqual(undefined, [])).toBe(true);
      expect((effects as any)._templatesEqual([], templates1)).toBe(false);
    });

    it('should handle missing notes and timeEstimate in template comparison', () => {
      const templates1 = [{ title: 'Task 1' }];
      const templates2 = [{ title: 'Task 1', notes: '', timeEstimate: 0 }];

      expect((effects as any)._templatesEqual(templates1, templates2)).toBe(true);
    });
  });

  describe('updateTaskAfterMakingItRepeatable$', () => {
    it('should capture subtasks as templates and set lastTaskCreationDay when adding repeat config', () => {
      const action = addTaskRepeatCfgToTask({
        taskRepeatCfg: mockRepeatCfg,
        taskId: 'parent-task-id',
      });

      actions$ = of(action);
      taskService.getByIdWithSubTaskData$.and.returnValue(of(mockTaskWithSubTasks));

      // Since this is a side effect, we verify the service methods are called
      spyOn(effects as any, '_updateRegularTaskInstance');

      effects.updateTaskAfterMakingItRepeatable$.subscribe().unsubscribe();

      // Should update config with subtask templates AND lastTaskCreationDay (#5594)
      expect(taskRepeatCfgService.updateTaskRepeatCfg).toHaveBeenCalledWith(
        'repeat-cfg-id',
        jasmine.objectContaining({
          subTaskTemplates: [
            { title: 'SubTask 1', notes: 'Notes 1', timeEstimate: 3600000 },
            { title: 'SubTask 2', notes: 'Notes 2', timeEstimate: 7200000 },
          ],
          lastTaskCreationDay: jasmine.any(String),
          lastTaskCreation: jasmine.any(Number),
        }),
      );
      expect((effects as any)._updateRegularTaskInstance).toHaveBeenCalled();
    });

    it('should handle task with no subtasks and still set lastTaskCreationDay', () => {
      const taskWithoutSubs: TaskWithSubTasks = {
        ...mockTask,
        subTasks: [],
      };

      const action = addTaskRepeatCfgToTask({
        taskRepeatCfg: mockRepeatCfg,
        taskId: 'parent-task-id',
      });

      actions$ = of(action);
      taskService.getByIdWithSubTaskData$.and.returnValue(of(taskWithoutSubs));

      spyOn(effects as any, '_updateRegularTaskInstance');

      effects.updateTaskAfterMakingItRepeatable$.subscribe().unsubscribe();

      // Should update config with empty subtasks AND lastTaskCreationDay (#5594)
      expect(taskRepeatCfgService.updateTaskRepeatCfg).toHaveBeenCalledWith(
        'repeat-cfg-id',
        jasmine.objectContaining({
          subTaskTemplates: [],
          lastTaskCreationDay: jasmine.any(String),
          lastTaskCreation: jasmine.any(Number),
        }),
      );
    });

    it('should update task created and dispatch planTaskForDay when first occurrence is in the future (#6433)', (done) => {
      // Scenario: Task is created today, but repeat config only matches future days
      // Use a day that is 3 days from today (guaranteed to not be today)
      const today = new Date();
      const todayStr = getDbDateStr(today);
      const todayDayOfWeek = today.getDay(); // 0=Sun, 1=Mon, ..., 6=Sat

      // Pick a weekday that is 3 days from now (guaranteed to not be today)
      const targetDayOfWeek = (todayDayOfWeek + 3) % 7;

      const taskCreatedToday: TaskWithSubTasks = {
        ...mockTask,
        subTasks: [],
        dueDay: todayStr,
        created: today.getTime(),
      };

      // Create weekday booleans with only the target day set to true
      const weeklyRepeatCfg: TaskRepeatCfgCopy = {
        ...mockRepeatCfg,
        repeatCycle: 'WEEKLY',
        repeatEvery: 1,
        startDate: todayStr,
        sunday: targetDayOfWeek === 0,
        monday: targetDayOfWeek === 1,
        tuesday: targetDayOfWeek === 2,
        wednesday: targetDayOfWeek === 3,
        thursday: targetDayOfWeek === 4,
        friday: targetDayOfWeek === 5,
        saturday: targetDayOfWeek === 6,
      };

      const firstOccurrence = getFirstRepeatOccurrence(weeklyRepeatCfg as any)!;
      const firstOccurrenceStr = getDbDateStr(firstOccurrence);

      const action = addTaskRepeatCfgToTask({
        taskRepeatCfg: weeklyRepeatCfg,
        taskId: 'parent-task-id',
      });

      actions$ = of(action);
      taskService.getByIdWithSubTaskData$.and.returnValue(of(taskCreatedToday));

      spyOn(effects as any, '_updateRegularTaskInstance');

      effects.updateTaskAfterMakingItRepeatable$.subscribe((result) => {
        // Should dispatch planTaskForDay for the future occurrence
        // Note: effect strips subTasks from the task object
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        const { subTasks: _ignored, ...expectedTask } = taskCreatedToday;
        expect(result).toEqual(
          PlannerActions.planTaskForDay({
            task: expectedTask as any,
            day: firstOccurrenceStr,
          }),
        );

        // Should update created timestamp AND dueDay for proper scheduling (#6856)
        expect(taskService.update).toHaveBeenCalledWith('parent-task-id', {
          created: firstOccurrence.getTime(),
          dueDay: firstOccurrenceStr,
        });
        done();
      });
    });

    it('should NOT update task dueDay when first occurrence matches current', () => {
      // Scenario: Task dueDay already matches first occurrence
      // Use daily repeat starting today - first occurrence is today
      const today = new Date();
      const todayStr = getDbDateStr(today);

      const taskCreatedToday: TaskWithSubTasks = {
        ...mockTask,
        subTasks: [],
        dueDay: todayStr, // Matches first occurrence
        created: today.getTime(),
      };

      const dailyRepeatCfg: TaskRepeatCfgCopy = {
        ...mockRepeatCfg,
        repeatCycle: 'DAILY',
        repeatEvery: 1,
        startDate: todayStr,
      };

      const action = addTaskRepeatCfgToTask({
        taskRepeatCfg: dailyRepeatCfg,
        taskId: 'parent-task-id',
      });

      actions$ = of(action);
      taskService.getByIdWithSubTaskData$.and.returnValue(of(taskCreatedToday));

      spyOn(effects as any, '_updateRegularTaskInstance');

      effects.updateTaskAfterMakingItRepeatable$.subscribe().unsubscribe();

      // Verify that update was NOT called (same date)
      expect(taskService.update).not.toHaveBeenCalled();
    });

    it('should set dueDay to today when an Inbox task (no dueDay) is made repeatable starting today (#7725)', (done) => {
      // Scenario from issue #7725: a task added to the Inbox (no dueDay) is
      // converted to a recurring task via the dialog with start date = today.
      // It must be scheduled for today instead of staying unscheduled.
      const today = new Date();
      const todayStr = getDbDateStr(today);

      const inboxTask: TaskWithSubTasks = {
        ...mockTask,
        subTasks: [],
        dueDay: undefined, // Inbox task — not scheduled
        dueWithTime: undefined,
        created: today.getTime(),
      };

      const dailyRepeatCfg: TaskRepeatCfgCopy = {
        ...mockRepeatCfg,
        repeatCycle: 'DAILY',
        repeatEvery: 1,
        startDate: todayStr,
      };

      const action = addTaskRepeatCfgToTask({
        taskRepeatCfg: dailyRepeatCfg,
        taskId: 'parent-task-id',
      });

      actions$ = of(action);
      taskService.getByIdWithSubTaskData$.and.returnValue(of(inboxTask));

      spyOn(effects as any, '_updateRegularTaskInstance');

      let emitted = false;
      effects.updateTaskAfterMakingItRepeatable$.subscribe(() => {
        emitted = true;
      });

      setTimeout(() => {
        expect(emitted).toBe(false); // today occurrence = no planTaskForDay
        expect(taskService.update).toHaveBeenCalledWith('parent-task-id', {
          dueDay: todayStr,
        });
        done();
      }, 0);
    });

    it('should NOT set dueDay for a non-timed config when the task already has dueWithTime', (done) => {
      // A task scheduled with a time (dueWithTime, no dueDay) is made
      // repeatable, but the user cleared the start-time field so the config is
      // non-timed. dueDay must NOT be set: dueDay/dueWithTime are mutually
      // exclusive and a plain update would not clear dueWithTime.
      const today = new Date();
      const todayStr = getDbDateStr(today);
      const todayNoon = new Date(today);
      todayNoon.setHours(12, 0, 0, 0);

      const timedTask: TaskWithSubTasks = {
        ...mockTask,
        subTasks: [],
        dueDay: undefined,
        dueWithTime: todayNoon.getTime(),
        created: today.getTime(),
      };

      const dailyRepeatCfg: TaskRepeatCfgCopy = {
        ...mockRepeatCfg,
        repeatCycle: 'DAILY',
        repeatEvery: 1,
        startDate: todayStr,
      };

      const action = addTaskRepeatCfgToTask({
        taskRepeatCfg: dailyRepeatCfg,
        taskId: 'parent-task-id',
      });

      actions$ = of(action);
      taskService.getByIdWithSubTaskData$.and.returnValue(of(timedTask));

      spyOn(effects as any, '_updateRegularTaskInstance');

      effects.updateTaskAfterMakingItRepeatable$.subscribe();

      setTimeout(() => {
        const dueDayUpdate = taskService.update.calls
          .allArgs()
          .find(([, changes]) => 'dueDay' in (changes as object));
        expect(dueDayUpdate).toBeUndefined();
        done();
      }, 0);
    });

    it('should update task created when first occurrence is today but task was created earlier', () => {
      const today = new Date();
      const todayStr = getDbDateStr(today);
      const yesterday = new Date(today);
      yesterday.setDate(yesterday.getDate() - 1);

      const taskCreatedYesterday: TaskWithSubTasks = {
        ...mockTask,
        subTasks: [],
        dueDay: todayStr,
        created: yesterday.getTime(),
      };

      const dailyRepeatCfg: TaskRepeatCfgCopy = {
        ...mockRepeatCfg,
        repeatCycle: 'DAILY',
        repeatEvery: 1,
        startDate: todayStr,
      };

      const firstOccurrence = getFirstRepeatOccurrence(dailyRepeatCfg as any)!;
      const action = addTaskRepeatCfgToTask({
        taskRepeatCfg: dailyRepeatCfg,
        taskId: 'parent-task-id',
      });

      actions$ = of(action);
      taskService.getByIdWithSubTaskData$.and.returnValue(of(taskCreatedYesterday));

      spyOn(effects as any, '_updateRegularTaskInstance');

      effects.updateTaskAfterMakingItRepeatable$.subscribe().unsubscribe();

      expect(taskService.update).toHaveBeenCalledWith('parent-task-id', {
        created: firstOccurrence.getTime(),
      });
    });

    it('should dispatch planTaskForDay and update created for daily repeat with future start date (#6433)', (done) => {
      // Scenario: Task created today, but start date is 7 days in the future
      const today = new Date();
      const todayStr = getDbDateStr(today);

      const dailyRepeatCfg: TaskRepeatCfgCopy = {
        ...mockRepeatCfg,
        repeatCycle: 'DAILY',
        repeatEvery: 1,
        startDate: getDbDateStr(
          new Date(today.getFullYear(), today.getMonth(), today.getDate() + 7),
        ),
      };

      const firstOccurrence = getFirstRepeatOccurrence(dailyRepeatCfg as any)!;
      const firstOccurrenceStr = getDbDateStr(firstOccurrence);

      const taskCreatedToday: TaskWithSubTasks = {
        ...mockTask,
        subTasks: [],
        dueDay: todayStr,
        created: today.getTime(),
      };

      const action = addTaskRepeatCfgToTask({
        taskRepeatCfg: dailyRepeatCfg,
        taskId: 'parent-task-id',
      });

      actions$ = of(action);
      taskService.getByIdWithSubTaskData$.and.returnValue(of(taskCreatedToday));

      spyOn(effects as any, '_updateRegularTaskInstance');

      effects.updateTaskAfterMakingItRepeatable$.subscribe((result) => {
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        const { subTasks: _ignored, ...expectedTask } = taskCreatedToday;
        expect(result).toEqual(
          PlannerActions.planTaskForDay({
            task: expectedTask as any,
            day: firstOccurrenceStr,
          }),
        );
        expect(taskService.update).toHaveBeenCalledWith('parent-task-id', {
          created: firstOccurrence.getTime(),
          dueDay: firstOccurrenceStr,
        });
        done();
      });
    });

    it('should dispatch planTaskForDay for monthly repeat when today is after repeat day (#6433)', (done) => {
      // Scenario: Monthly repeat on the 1st, but we're past the 1st
      const today = new Date();
      const todayStr = getDbDateStr(today);

      // Start date is the 1st of current month (already passed)
      const startDate = new Date(today.getFullYear(), today.getMonth(), 1);
      const startDateStr = getDbDateStr(startDate);

      const monthlyRepeatCfg: TaskRepeatCfgCopy = {
        ...mockRepeatCfg,
        repeatCycle: 'MONTHLY',
        repeatEvery: 1,
        startDate: startDateStr, // 1st of month
      };

      const firstOccurrence = getFirstRepeatOccurrence(monthlyRepeatCfg as any)!;
      const isFirstToday = isToday(firstOccurrence);

      // Skip this test on the 1st of the month (first occurrence is today, no planTaskForDay)
      if (isFirstToday) {
        done();
        return;
      }

      const firstOccurrenceStr = getDbDateStr(firstOccurrence);

      const taskCreatedToday: TaskWithSubTasks = {
        ...mockTask,
        subTasks: [],
        dueDay: todayStr,
        created: today.getTime(),
      };

      const action = addTaskRepeatCfgToTask({
        taskRepeatCfg: monthlyRepeatCfg,
        taskId: 'parent-task-id',
      });

      actions$ = of(action);
      taskService.getByIdWithSubTaskData$.and.returnValue(of(taskCreatedToday));

      spyOn(effects as any, '_updateRegularTaskInstance');

      effects.updateTaskAfterMakingItRepeatable$.subscribe((result) => {
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        const { subTasks: _ignored, ...expectedTask } = taskCreatedToday;
        expect(result).toEqual(
          PlannerActions.planTaskForDay({
            task: expectedTask as any,
            day: firstOccurrenceStr,
          }),
        );
        expect(taskService.update).toHaveBeenCalledWith('parent-task-id', {
          created: firstOccurrence.getTime(),
          dueDay: firstOccurrenceStr,
        });
        done();
      });
    });

    it('should dispatch planTaskForDay when dueDay is missing and first occurrence is future (#6433)', (done) => {
      // Scenario: Task has no dueDay, first occurrence is in the future
      const today = new Date();
      const todayStr = getDbDateStr(today);
      const todayDayOfWeek = today.getDay();

      // Pick a weekday that is 3 days from now (guaranteed to not be today)
      const targetDayOfWeek = (todayDayOfWeek + 3) % 7;

      const taskWithoutDueDay: TaskWithSubTasks = {
        ...mockTask,
        subTasks: [],
        dueDay: undefined,
        created: today.getTime(),
      };

      const weeklyRepeatCfg: TaskRepeatCfgCopy = {
        ...mockRepeatCfg,
        repeatCycle: 'WEEKLY',
        repeatEvery: 1,
        startDate: todayStr,
        sunday: targetDayOfWeek === 0,
        monday: targetDayOfWeek === 1,
        tuesday: targetDayOfWeek === 2,
        wednesday: targetDayOfWeek === 3,
        thursday: targetDayOfWeek === 4,
        friday: targetDayOfWeek === 5,
        saturday: targetDayOfWeek === 6,
      };

      const firstOccurrence = getFirstRepeatOccurrence(weeklyRepeatCfg as any)!;
      const firstOccurrenceStr = getDbDateStr(firstOccurrence);

      const action = addTaskRepeatCfgToTask({
        taskRepeatCfg: weeklyRepeatCfg,
        taskId: 'parent-task-id',
      });

      actions$ = of(action);
      taskService.getByIdWithSubTaskData$.and.returnValue(of(taskWithoutDueDay));

      spyOn(effects as any, '_updateRegularTaskInstance');

      effects.updateTaskAfterMakingItRepeatable$.subscribe((result) => {
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        const { subTasks: _ignored, ...expectedTask } = taskWithoutDueDay;
        expect(result).toEqual(
          PlannerActions.planTaskForDay({
            task: expectedTask as any,
            day: firstOccurrenceStr,
          }),
        );
        expect(taskService.update).toHaveBeenCalledWith('parent-task-id', {
          created: firstOccurrence.getTime(),
          dueDay: firstOccurrenceStr,
        });
        done();
      });
    });

    it('should NOT dispatch planTaskForDay for timed tasks with future first occurrence (handled by addRepeatCfgToTaskUpdateTask$)', (done) => {
      // When startTime && remindAt are set, addRepeatCfgToTaskUpdateTask$ handles
      // scheduling via scheduleTaskWithTime. planTaskForDay would overwrite dueWithTime.
      const today = new Date();
      const todayStr = getDbDateStr(today);
      const todayDayOfWeek = today.getDay();

      // Pick a weekday that is 3 days from now (guaranteed future)
      const targetDayOfWeek = (todayDayOfWeek + 3) % 7;

      const taskCreatedToday: TaskWithSubTasks = {
        ...mockTask,
        subTasks: [],
        dueDay: todayStr,
        created: today.getTime(),
      };

      const weeklyRepeatCfg: TaskRepeatCfgCopy = {
        ...mockRepeatCfg,
        repeatCycle: 'WEEKLY',
        repeatEvery: 1,
        startDate: todayStr,
        sunday: targetDayOfWeek === 0,
        monday: targetDayOfWeek === 1,
        tuesday: targetDayOfWeek === 2,
        wednesday: targetDayOfWeek === 3,
        thursday: targetDayOfWeek === 4,
        friday: targetDayOfWeek === 5,
        saturday: targetDayOfWeek === 6,
      };

      const firstOccurrence = getFirstRepeatOccurrence(weeklyRepeatCfg as any)!;

      const action = addTaskRepeatCfgToTask({
        taskRepeatCfg: weeklyRepeatCfg,
        taskId: 'parent-task-id',
        startTime: '10:00',
        remindAt: TaskReminderOptionId.AtStart,
      });

      actions$ = of(action);
      taskService.getByIdWithSubTaskData$.and.returnValue(of(taskCreatedToday));

      spyOn(effects as any, '_updateRegularTaskInstance');

      // Effect should NOT emit planTaskForDay for timed tasks
      let emitted = false;
      effects.updateTaskAfterMakingItRepeatable$.subscribe(() => {
        emitted = true;
      });

      setTimeout(() => {
        expect(emitted).toBe(false);

        // Should still update created (duplicate prevention)
        const firstOccurrenceStr = getDbDateStr(firstOccurrence);
        expect(taskService.update).toHaveBeenCalledWith('parent-task-id', {
          created: firstOccurrence.getTime(),
          dueDay: firstOccurrenceStr,
        });
        // Should still update lastTaskCreationDay to the future date
        expect(taskRepeatCfgService.updateTaskRepeatCfg).toHaveBeenCalledWith(
          'repeat-cfg-id',
          jasmine.objectContaining({
            lastTaskCreationDay: firstOccurrenceStr,
          }),
        );
        // Should still call _updateRegularTaskInstance
        expect((effects as any)._updateRegularTaskInstance).toHaveBeenCalled();
        done();
      }, 0);
    });

    it('should NOT dispatch planTaskForDay for timed tasks with today first occurrence', (done) => {
      const today = new Date();
      const todayStr = getDbDateStr(today);

      const taskCreatedToday: TaskWithSubTasks = {
        ...mockTask,
        subTasks: [],
        dueDay: todayStr,
        created: today.getTime(),
      };

      // DAILY starting today = first occurrence is today
      const dailyRepeatCfg: TaskRepeatCfgCopy = {
        ...mockRepeatCfg,
        repeatCycle: 'DAILY',
        repeatEvery: 1,
        startDate: todayStr,
      };

      const action = addTaskRepeatCfgToTask({
        taskRepeatCfg: dailyRepeatCfg,
        taskId: 'parent-task-id',
        startTime: '10:00',
        remindAt: TaskReminderOptionId.AtStart,
      });

      actions$ = of(action);
      taskService.getByIdWithSubTaskData$.and.returnValue(of(taskCreatedToday));

      spyOn(effects as any, '_updateRegularTaskInstance');

      let emitted = false;
      effects.updateTaskAfterMakingItRepeatable$.subscribe(() => {
        emitted = true;
      });

      setTimeout(() => {
        expect(emitted).toBe(false);
        // Today branch: dueDay already matches, so no update call
        expect(taskService.update).not.toHaveBeenCalled();
        // Config should still be updated with lastTaskCreationDay
        expect(taskRepeatCfgService.updateTaskRepeatCfg).toHaveBeenCalledWith(
          'repeat-cfg-id',
          jasmine.objectContaining({
            lastTaskCreationDay: todayStr,
          }),
        );
        expect((effects as any)._updateRegularTaskInstance).toHaveBeenCalled();
        done();
      }, 0);
    });

    it('should update task dueDay when first occurrence is today but dueDay differs', (done) => {
      const today = new Date();
      const todayStr = getDbDateStr(today);

      // Task with a past dueDay
      const taskWithOldDueDay: TaskWithSubTasks = {
        ...mockTask,
        subTasks: [],
        dueDay: '2024-01-01', // old date, differs from today
        created: today.getTime(),
      };

      const dailyRepeatCfg: TaskRepeatCfgCopy = {
        ...mockRepeatCfg,
        repeatCycle: 'DAILY',
        repeatEvery: 1,
        startDate: todayStr,
      };

      const action = addTaskRepeatCfgToTask({
        taskRepeatCfg: dailyRepeatCfg,
        taskId: 'parent-task-id',
      });

      actions$ = of(action);
      taskService.getByIdWithSubTaskData$.and.returnValue(of(taskWithOldDueDay));

      spyOn(effects as any, '_updateRegularTaskInstance');

      let emitted = false;
      effects.updateTaskAfterMakingItRepeatable$.subscribe(() => {
        emitted = true;
      });

      setTimeout(() => {
        expect(emitted).toBe(false); // today occurrence = no planTaskForDay
        expect(taskService.update).toHaveBeenCalledWith('parent-task-id', {
          dueDay: todayStr,
        });
        done();
      }, 0);
    });

    it('should fall back to today when getFirstRepeatOccurrence returns null (repeatEvery=0)', (done) => {
      const today = new Date();
      const todayStr = getDbDateStr(today);

      const taskCreatedToday: TaskWithSubTasks = {
        ...mockTask,
        subTasks: [],
        dueDay: todayStr,
        created: today.getTime(),
      };

      const invalidRepeatCfg: TaskRepeatCfgCopy = {
        ...mockRepeatCfg,
        repeatCycle: 'DAILY',
        repeatEvery: 0, // causes getFirstRepeatOccurrence to return null
        startDate: todayStr,
      };

      const action = addTaskRepeatCfgToTask({
        taskRepeatCfg: invalidRepeatCfg,
        taskId: 'parent-task-id',
      });

      actions$ = of(action);
      taskService.getByIdWithSubTaskData$.and.returnValue(of(taskCreatedToday));

      spyOn(effects as any, '_updateRegularTaskInstance');

      let emitted = false;
      effects.updateTaskAfterMakingItRepeatable$.subscribe(() => {
        emitted = true;
      });

      setTimeout(() => {
        // Should treat as today (null fallback), so no planTaskForDay emitted
        expect(emitted).toBe(false);
        // lastTaskCreationDay should be today (fallback)
        expect(taskRepeatCfgService.updateTaskRepeatCfg).toHaveBeenCalledWith(
          'repeat-cfg-id',
          jasmine.objectContaining({
            lastTaskCreationDay: todayStr,
          }),
        );
        done();
      }, 0);
    });

    it('should handle Mon/Wed/Fri pattern correctly for both today and future (#6433)', (done) => {
      const today = new Date();
      const todayStr = getDbDateStr(today);
      const todayDayOfWeek = today.getDay();

      // Mon=1, Wed=3, Fri=5
      const isMonWedFri =
        todayDayOfWeek === 1 || todayDayOfWeek === 3 || todayDayOfWeek === 5;

      const taskCreatedToday: TaskWithSubTasks = {
        ...mockTask,
        subTasks: [],
        dueDay: todayStr,
        created: today.getTime(),
      };

      const monWedFriRepeatCfg: TaskRepeatCfgCopy = {
        ...mockRepeatCfg,
        repeatCycle: 'WEEKLY',
        repeatEvery: 1,
        startDate: todayStr,
        monday: true,
        tuesday: false,
        wednesday: true,
        thursday: false,
        friday: true,
        saturday: false,
        sunday: false,
      };

      const firstOccurrence = getFirstRepeatOccurrence(monWedFriRepeatCfg as any)!;

      const action = addTaskRepeatCfgToTask({
        taskRepeatCfg: monWedFriRepeatCfg,
        taskId: 'parent-task-id',
      });

      actions$ = of(action);
      taskService.getByIdWithSubTaskData$.and.returnValue(of(taskCreatedToday));

      spyOn(effects as any, '_updateRegularTaskInstance');

      if (isMonWedFri) {
        // Today case: effect is filtered out (no emission)
        let emitted = false;
        effects.updateTaskAfterMakingItRepeatable$.subscribe(() => {
          emitted = true;
        });
        // Give it a tick
        setTimeout(() => {
          expect(emitted).toBe(false);
          expect(taskService.update).not.toHaveBeenCalled();
          done();
        }, 0);
      } else {
        // Future case: should dispatch planTaskForDay
        const firstOccurrenceStr = getDbDateStr(firstOccurrence);
        effects.updateTaskAfterMakingItRepeatable$.subscribe((result) => {
          // eslint-disable-next-line @typescript-eslint/no-unused-vars
          const { subTasks: _ignored, ...expectedTask } = taskCreatedToday;
          expect(result).toEqual(
            PlannerActions.planTaskForDay({
              task: expectedTask as any,
              day: firstOccurrenceStr,
            }),
          );
          expect(taskService.update).toHaveBeenCalledWith('parent-task-id', {
            created: firstOccurrence.getTime(),
            dueDay: firstOccurrenceStr,
          });
          done();
        });
      }
    });

    it('should preserve dueDay when converting a task whose dueDay matches startDate to YEARLY recurring (#7344)', (done) => {
      // Scenario from issue #7344: a task is planned for a past date; user converts
      // it to a yearly recurrence. The dialog defaults startDate = task.dueDay.
      // Expected: the task stays planned for its original dueDay, not auto-advanced
      // to next year's occurrence.
      const today = new Date();
      const pastDueDay = new Date(today);
      pastDueDay.setDate(today.getDate() - 22);
      const pastDueDayStr = getDbDateStr(pastDueDay);
      const shiftedByOneYearStr = getDbDateStr(
        new Date(
          pastDueDay.getFullYear() + 1,
          pastDueDay.getMonth(),
          pastDueDay.getDate(),
        ),
      );

      const taskWithPastDueDay: TaskWithSubTasks = {
        ...mockTask,
        subTasks: [],
        dueDay: pastDueDayStr,
        created: pastDueDay.getTime(),
      };

      const yearlyRepeatCfg: TaskRepeatCfgCopy = {
        ...mockRepeatCfg,
        repeatCycle: 'YEARLY',
        repeatEvery: 4,
        startDate: pastDueDayStr,
      };

      const action = addTaskRepeatCfgToTask({
        taskRepeatCfg: yearlyRepeatCfg,
        taskId: 'parent-task-id',
      });

      actions$ = of(action);
      taskService.getByIdWithSubTaskData$.and.returnValue(of(taskWithPastDueDay));
      spyOn(effects as any, '_updateRegularTaskInstance');

      effects.updateTaskAfterMakingItRepeatable$.subscribe((result) => {
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        const { subTasks: _ignored, ...expectedTask } = taskWithPastDueDay;
        expect(result).toEqual(
          PlannerActions.planTaskForDay({
            task: expectedTask as any,
            day: pastDueDayStr,
          }),
        );

        // Repeat cfg must record pastDueDayStr as the last creation day (so the
        // next occurrence is computed from the preserved date, not a jumped one).
        expect(taskRepeatCfgService.updateTaskRepeatCfg).toHaveBeenCalledWith(
          'repeat-cfg-id',
          jasmine.objectContaining({
            lastTaskCreationDay: pastDueDayStr,
          }),
        );

        // No update may shift dueDay to next year.
        taskService.update.calls.allArgs().forEach(([, changes]) => {
          const c = changes as { dueDay?: string };
          if ('dueDay' in c) {
            expect(c.dueDay).not.toBe(shiftedByOneYearStr);
            expect(c.dueDay).toBe(pastDueDayStr);
          }
        });
        done();
      });
    });
  });

  describe('updateStartDateOnComplete$', () => {
    it('should update startDate and lastTaskCreationDay when completing a repeat-on-complete task', () => {
      testScheduler.run(({ hot, expectObservable }) => {
        const action = TaskSharedActions.updateTask({
          task: { id: 'parent-task-id', changes: { isDone: true } },
        });

        actions$ = hot('-a', { a: action });

        // full task with repeatCfgId
        taskService.getByIdOnce$.and.returnValue(of(mockTask));

        // repeat cfg with repeatOnComplete true
        taskRepeatCfgService.getTaskRepeatCfgByIdAllowUndefined$.and.returnValue(
          of({ ...mockRepeatCfg, repeatFromCompletionDate: true }),
        );

        const today = getDbDateStr();
        const expectedAction = updateTaskRepeatCfg({
          taskRepeatCfg: {
            id: 'repeat-cfg-id',
            changes: { startDate: today, lastTaskCreationDay: today },
          },
        });

        expectObservable(effects.updateStartDateOnComplete$).toBe('-a', {
          a: expectedAction,
        });
      });
    });

    it('should not emit when task has no repeatCfgId', () => {
      testScheduler.run(({ hot, expectObservable }) => {
        const action = TaskSharedActions.updateTask({
          task: { id: 'no-repeat-task-id', changes: { isDone: true } },
        });

        const taskWithoutRepeat: Task = {
          ...DEFAULT_TASK,
          id: 'no-repeat-task-id',
          title: 'No Repeat',
        } as Task;

        actions$ = hot('-a', { a: action });

        taskService.getByIdOnce$.and.returnValue(of(taskWithoutRepeat));

        expectObservable(effects.updateStartDateOnComplete$).toBe('--');
      });
    });

    it('should not emit when repeatOnComplete is false', () => {
      testScheduler.run(({ hot, expectObservable }) => {
        const action = TaskSharedActions.updateTask({
          task: { id: 'parent-task-id', changes: { isDone: true } },
        });

        actions$ = hot('-a', { a: action });

        taskService.getByIdOnce$.and.returnValue(of(mockTask));
        taskRepeatCfgService.getTaskRepeatCfgByIdAllowUndefined$.and.returnValue(
          of({ ...mockRepeatCfg, repeatFromCompletionDate: false }),
        );

        expectObservable(effects.updateStartDateOnComplete$).toBe('--');
      });
    });

    it('should not emit when completing a non-latest instance', () => {
      testScheduler.run(({ hot, expectObservable }) => {
        const action = TaskSharedActions.updateTask({
          task: { id: 'parent-task-id', changes: { isDone: true } },
        });

        const oldTask: Task = {
          ...mockTask,
          created: new Date(2020, 0, 1).getTime(),
          dueDay: '2020-01-01',
        };

        actions$ = hot('-a', { a: action });

        taskService.getByIdOnce$.and.returnValue(of(oldTask));
        taskRepeatCfgService.getTaskRepeatCfgByIdAllowUndefined$.and.returnValue(
          of({
            ...mockRepeatCfg,
            repeatFromCompletionDate: true,
            lastTaskCreationDay: '2020-01-02',
          }),
        );

        expectObservable(effects.updateStartDateOnComplete$).toBe('--');
      });
    });

    it('should support legacy latest instance identified by dueDay when created predates cursor', () => {
      testScheduler.run(({ hot, expectObservable }) => {
        const today = getDbDateStr();
        const action = TaskSharedActions.updateTask({
          task: { id: 'parent-task-id', changes: { isDone: true } },
        });
        const legacyTask: Task = {
          ...mockTask,
          created: dateStrToUtcDate('2020-01-01').getTime(),
          dueDay: today,
        };

        actions$ = hot('-a', { a: action });
        taskService.getByIdOnce$.and.returnValue(of(legacyTask));
        taskRepeatCfgService.getTaskRepeatCfgByIdAllowUndefined$.and.returnValue(
          of({
            ...mockRepeatCfg,
            repeatFromCompletionDate: true,
            lastTaskCreationDay: today,
          }),
        );

        const expectedAction = updateTaskRepeatCfg({
          taskRepeatCfg: {
            id: 'repeat-cfg-id',
            changes: { startDate: today, lastTaskCreationDay: today },
          },
        });

        expectObservable(effects.updateStartDateOnComplete$).toBe('-a', {
          a: expectedAction,
        });
      });
    });

    it('should trigger waitForCompletion creation path for archived latest instance', (done) => {
      const today = getDbDateStr();
      const action = TaskSharedActions.updateTask({
        task: { id: 'parent-task-id', changes: { isDone: true } },
      });
      const archivedTask: Task = {
        ...mockTask,
        created: dateStrToUtcDate(today).getTime(),
        dueDay: today,
      };
      const repeatCfg: TaskRepeatCfgCopy = {
        ...mockRepeatCfg,
        repeatFromCompletionDate: false,
        waitForCompletion: true,
        lastTaskCreationDay: today,
      };
      const expectedAction = updateTaskRepeatCfg({
        taskRepeatCfg: {
          id: 'repeat-cfg-id',
          changes: { lastTaskCreationDay: today },
        },
      });

      actions$ = of(action);
      taskService.getByIdOnce$.and.returnValue(of(undefined as unknown as Task));
      taskArchiveService.load.and.returnValue(
        Promise.resolve({
          ids: ['parent-task-id'],
          entities: {
            [archivedTask.id]: archivedTask,
          },
        } as any),
      );
      taskRepeatCfgService.getTaskRepeatCfgByIdAllowUndefined$.and.returnValue(
        of(repeatCfg),
      );
      taskRepeatCfgService._getActionsForTaskRepeatCfg.and.returnValue(
        Promise.resolve([expectedAction]),
      );

      effects.updateStartDateOnComplete$.subscribe({
        next: (result) => {
          expect(result).toEqual(expectedAction);
          expect(taskRepeatCfgService._getActionsForTaskRepeatCfg).toHaveBeenCalledWith(
            repeatCfg,
            jasmine.any(Number),
          );
          done();
        },
        error: done.fail,
      });
    });

    it('should probe waitForCompletion creation path when completing a non-latest blocker', (done) => {
      const today = getDbDateStr();
      const action = TaskSharedActions.updateTask({
        task: { id: 'parent-task-id', changes: { isDone: true } },
      });
      const oldTask: Task = {
        ...mockTask,
        created: dateStrToUtcDate('2020-01-01').getTime(),
        dueDay: '2020-01-01',
      };
      const repeatCfg: TaskRepeatCfgCopy = {
        ...mockRepeatCfg,
        repeatFromCompletionDate: false,
        waitForCompletion: true,
        lastTaskCreationDay: today,
      };
      const expectedAction = updateTaskRepeatCfg({
        taskRepeatCfg: {
          id: 'repeat-cfg-id',
          changes: { lastTaskCreationDay: today },
        },
      });

      actions$ = of(action);
      taskService.getByIdOnce$.and.returnValue(of(oldTask));
      taskRepeatCfgService.getTaskRepeatCfgByIdAllowUndefined$.and.returnValue(
        of(repeatCfg),
      );
      taskRepeatCfgService._getActionsForTaskRepeatCfg.and.returnValue(
        Promise.resolve([expectedAction]),
      );

      effects.updateStartDateOnComplete$.subscribe({
        next: (result) => {
          expect(result).toEqual(expectedAction);
          expect(taskRepeatCfgService._getActionsForTaskRepeatCfg).toHaveBeenCalledWith(
            repeatCfg,
            jasmine.any(Number),
          );
          done();
        },
        error: done.fail,
      });
    });

    // #8715: a completed instance can reference a repeat config that was
    // already deleted (e.g. via cross-client sync). The lookup must not throw
    // ('Missing taskRepeatCfg') and crash the whole app — the effect just skips.
    it('should not throw when the referenced repeat config was deleted (#8715)', () => {
      testScheduler.run(({ hot, expectObservable }) => {
        const action = TaskSharedActions.updateTask({
          task: { id: 'parent-task-id', changes: { isDone: true } },
        });

        actions$ = hot('-a', { a: action });

        // task still references the (now missing) config
        taskService.getByIdOnce$.and.returnValue(of(mockTask));
        // config no longer exists -> non-throwing selector yields undefined
        taskRepeatCfgService.getTaskRepeatCfgByIdAllowUndefined$.and.returnValue(
          of(undefined),
        );
        // guard against regressing to the throwing selector (the #8715 root cause)
        taskRepeatCfgService.getTaskRepeatCfgById$.and.returnValue(
          throwError(() => new Error('Missing taskRepeatCfg')),
        );

        expectObservable(effects.updateStartDateOnComplete$).toBe('--');
      });
    });
  });

  describe('autoSyncSubtaskTemplatesFromNewest$', () => {
    it('should sync templates when subtask is added to repeatable task', () => {
      testScheduler.run(({ hot, expectObservable }) => {
        const action = addSubTask({
          task: mockSubTask1,
          parentId: 'parent-task-id',
        });

        actions$ = hot('-a', { a: action });

        taskService.getByIdOnce$.and.returnValue(of(mockTask));
        taskRepeatCfgService.getTaskRepeatCfgByIdAllowUndefined$.and.returnValue(
          of({
            ...mockRepeatCfg,
            shouldInheritSubtasks: true,
            disableAutoUpdateSubtasks: false,
          }),
        );
        taskService.getTasksByRepeatCfgId$.and.returnValue(of([mockTask]));
        taskService.getByIdsLive$.and.returnValue(of([mockSubTask1, mockSubTask2]));

        const expectedAction = updateTaskRepeatCfg({
          taskRepeatCfg: {
            id: 'repeat-cfg-id',
            changes: {
              subTaskTemplates: [
                { title: 'SubTask 1', notes: 'Notes 1', timeEstimate: 3600000 },
                { title: 'SubTask 2', notes: 'Notes 2', timeEstimate: 7200000 },
              ],
            },
          },
          isAskToUpdateAllTaskInstances: false,
        });

        expectObservable(effects.autoSyncSubtaskTemplatesFromNewest$).toBe('-a', {
          a: expectedAction,
        });
      });
    });

    it('should not sync when inherit is disabled', () => {
      testScheduler.run(({ hot, expectObservable }) => {
        const action = addSubTask({
          task: mockSubTask1,
          parentId: 'parent-task-id',
        });

        actions$ = hot('-a', { a: action });

        taskService.getByIdOnce$.and.returnValue(of(mockTask));
        taskRepeatCfgService.getTaskRepeatCfgByIdAllowUndefined$.and.returnValue(
          of({ ...mockRepeatCfg, shouldInheritSubtasks: false }),
        );

        expectObservable(effects.autoSyncSubtaskTemplatesFromNewest$).toBe('--');
      });
    });

    it('should not sync when auto-update is disabled', () => {
      testScheduler.run(({ hot, expectObservable }) => {
        const action = addSubTask({
          task: mockSubTask1,
          parentId: 'parent-task-id',
        });

        actions$ = hot('-a', { a: action });

        taskService.getByIdOnce$.and.returnValue(of(mockTask));
        taskRepeatCfgService.getTaskRepeatCfgByIdAllowUndefined$.and.returnValue(
          of({
            ...mockRepeatCfg,
            shouldInheritSubtasks: true,
            disableAutoUpdateSubtasks: true,
          }),
        );

        expectObservable(effects.autoSyncSubtaskTemplatesFromNewest$).toBe('--');
      });
    });

    it('should not sync when templates are already equal', () => {
      testScheduler.run(({ hot, expectObservable }) => {
        const action = addSubTask({
          task: mockSubTask1,
          parentId: 'parent-task-id',
        });

        actions$ = hot('-a', { a: action });

        const existingTemplates = [
          { title: 'SubTask 1', notes: 'Notes 1', timeEstimate: 3600000 },
          { title: 'SubTask 2', notes: 'Notes 2', timeEstimate: 7200000 },
        ];

        taskService.getByIdOnce$.and.returnValue(of(mockTask));
        taskRepeatCfgService.getTaskRepeatCfgByIdAllowUndefined$.and.returnValue(
          of({
            ...mockRepeatCfg,
            shouldInheritSubtasks: true,
            subTaskTemplates: existingTemplates,
          }),
        );
        taskService.getTasksByRepeatCfgId$.and.returnValue(of([mockTask]));
        taskService.getByIdsLive$.and.returnValue(of([mockSubTask1, mockSubTask2]));

        expectObservable(effects.autoSyncSubtaskTemplatesFromNewest$).toBe('--');
      });
    });

    // #8715: the parent task can reference a repeat config that was already
    // deleted (e.g. via cross-client sync). The lookup must not throw and
    // crash the whole app — the effect should simply skip.
    it('should not throw when the referenced repeat config was deleted (#8715)', () => {
      testScheduler.run(({ hot, expectObservable }) => {
        const action = addSubTask({
          task: mockSubTask1,
          parentId: 'parent-task-id',
        });

        actions$ = hot('-a', { a: action });

        taskService.getByIdOnce$.and.returnValue(of(mockTask));
        // config no longer exists -> non-throwing selector yields undefined
        taskRepeatCfgService.getTaskRepeatCfgByIdAllowUndefined$.and.returnValue(
          of(undefined),
        );
        // guard against regressing to the throwing selector (the #8715 root cause)
        taskRepeatCfgService.getTaskRepeatCfgById$.and.returnValue(
          throwError(() => new Error('Missing taskRepeatCfg')),
        );

        expectObservable(effects.autoSyncSubtaskTemplatesFromNewest$).toBe('--');
      });
    });
  });

  describe('enableAutoUpdateOrInheritSnapshot$', () => {
    it('should snapshot subtasks when enabling inherit', () => {
      testScheduler.run(({ hot, expectObservable }) => {
        const action = updateTaskRepeatCfg({
          taskRepeatCfg: {
            id: 'repeat-cfg-id',
            changes: { shouldInheritSubtasks: true },
          },
          isAskToUpdateAllTaskInstances: false,
        });

        actions$ = hot('-a', { a: action });

        taskRepeatCfgService.getTaskRepeatCfgById$.and.returnValue(of(mockRepeatCfg));
        taskService.getTasksByRepeatCfgId$.and.returnValue(of([mockTask]));
        taskService.getByIdsLive$.and.returnValue(of([mockSubTask1, mockSubTask2]));

        const expectedAction = updateTaskRepeatCfg({
          taskRepeatCfg: {
            id: 'repeat-cfg-id',
            changes: {
              subTaskTemplates: [
                { title: 'SubTask 1', notes: 'Notes 1', timeEstimate: 3600000 },
                { title: 'SubTask 2', notes: 'Notes 2', timeEstimate: 7200000 },
              ],
            },
          },
          isAskToUpdateAllTaskInstances: false,
        });

        expectObservable(effects.enableAutoUpdateOrInheritSnapshot$).toBe('-a', {
          a: expectedAction,
        });
      });
    });

    it('should fallback to archive when no live instances exist', (done) => {
      const action = updateTaskRepeatCfg({
        taskRepeatCfg: {
          id: 'repeat-cfg-id',
          changes: { shouldInheritSubtasks: true },
        },
        isAskToUpdateAllTaskInstances: false,
      });

      actions$ = of(action);

      const archiveTask = { ...mockTask, created: Date.now() - 86400000 }; // Yesterday
      const archiveState = {
        ids: ['sub1', 'sub2'],
        entities: {
          sub1: mockSubTask1,
          sub2: mockSubTask2,
        },
      };

      taskRepeatCfgService.getTaskRepeatCfgById$.and.returnValue(of(mockRepeatCfg));
      taskService.getTasksByRepeatCfgId$.and.returnValue(of([])); // No live instances
      taskService.getArchiveTasksForRepeatCfgId.and.returnValue(
        Promise.resolve([archiveTask]),
      );
      taskArchiveService.load.and.returnValue(Promise.resolve(archiveState));

      effects.enableAutoUpdateOrInheritSnapshot$.subscribe((result) => {
        expect(result.taskRepeatCfg.changes).toEqual({
          subTaskTemplates: [
            { title: 'SubTask 1', notes: 'Notes 1', timeEstimate: 3600000 },
            { title: 'SubTask 2', notes: 'Notes 2', timeEstimate: 7200000 },
          ],
        });
        done();
      });
    });

    it('should not snapshot when inherit is not being enabled', () => {
      testScheduler.run(({ hot, expectObservable }) => {
        const action = updateTaskRepeatCfg({
          taskRepeatCfg: {
            id: 'repeat-cfg-id',
            changes: { title: 'New Title' }, // Not enabling inherit
          },
          isAskToUpdateAllTaskInstances: false,
        });

        actions$ = hot('-a', { a: action });

        expectObservable(effects.enableAutoUpdateOrInheritSnapshot$).toBe('--');
      });
    });
  });

  describe('addRepeatCfgToTaskUpdateTask$', () => {
    it('should schedule task for today when DAILY repeat pattern matches (issue #5594)', () => {
      testScheduler.run(({ hot, expectObservable }) => {
        const todayStr = getDbDateStr();
        const startTimeStr = '10:00';
        const action = addTaskRepeatCfgToTask({
          taskRepeatCfg: {
            ...mockRepeatCfg,
            startDate: todayStr,
            repeatCycle: 'DAILY',
            repeatEvery: 1,
          },
          taskId: 'parent-task-id',
          startTime: startTimeStr,
          remindAt: TaskReminderOptionId.AtStart,
        });

        actions$ = hot('-a', { a: action });
        taskService.getByIdOnce$.and.returnValue(of(mockTask));

        // For DAILY repeat, getFirstRepeatOccurrence returns today
        const expectedDateTime = getDateTimeFromClockString(
          startTimeStr,
          dateStrToUtcDate(todayStr).getTime(),
        );

        const expectedAction = TaskSharedActions.scheduleTaskWithTime({
          task: mockTask,
          dueWithTime: expectedDateTime,
          remindAt: remindOptionToMilliseconds(
            expectedDateTime,
            TaskReminderOptionId.AtStart,
          ),
          isMoveToBacklog: false,
          isSkipAutoRemoveFromToday: isToday(expectedDateTime),
        });

        expectObservable(effects.addRepeatCfgToTaskUpdateTask$).toBe('-a', {
          a: expectedAction,
        });
      });
    });

    it('should schedule task for correct weekday when WEEKLY pattern is used (issue #5594)', () => {
      testScheduler.run(({ hot, expectObservable }) => {
        const todayStr = getDbDateStr();
        const today = new Date();
        const todayDayOfWeek = today.getDay(); // 0 = Sunday, 1 = Monday, etc.
        const startTimeStr = '10:00';

        // Create a repeat config that repeats on today's weekday
        const weekdayKeys = [
          'sunday',
          'monday',
          'tuesday',
          'wednesday',
          'thursday',
          'friday',
          'saturday',
        ] as const;
        const todayWeekdayKey = weekdayKeys[todayDayOfWeek];

        const action = addTaskRepeatCfgToTask({
          taskRepeatCfg: {
            ...mockRepeatCfg,
            startDate: todayStr,
            repeatCycle: 'WEEKLY',
            repeatEvery: 1,
            monday: todayWeekdayKey === 'monday',
            tuesday: todayWeekdayKey === 'tuesday',
            wednesday: todayWeekdayKey === 'wednesday',
            thursday: todayWeekdayKey === 'thursday',
            friday: todayWeekdayKey === 'friday',
            saturday: todayWeekdayKey === 'saturday',
            sunday: todayWeekdayKey === 'sunday',
          },
          taskId: 'parent-task-id',
          startTime: startTimeStr,
          remindAt: TaskReminderOptionId.AtStart,
        });

        actions$ = hot('-a', { a: action });
        taskService.getByIdOnce$.and.returnValue(of(mockTask));

        // For WEEKLY repeat on today's weekday, should return today
        const expectedDateTime = getDateTimeFromClockString(
          startTimeStr,
          dateStrToUtcDate(todayStr).getTime(),
        );

        const expectedAction = TaskSharedActions.scheduleTaskWithTime({
          task: mockTask,
          dueWithTime: expectedDateTime,
          remindAt: remindOptionToMilliseconds(
            expectedDateTime,
            TaskReminderOptionId.AtStart,
          ),
          isMoveToBacklog: false,
          isSkipAutoRemoveFromToday: isToday(expectedDateTime),
        });

        expectObservable(effects.addRepeatCfgToTaskUpdateTask$).toBe('-a', {
          a: expectedAction,
        });
      });
    });

    it('should calculate next occurrence when startDate is undefined (issue #5594)', () => {
      testScheduler.run(({ hot, expectObservable }) => {
        const startTimeStr = '10:00';
        const today = new Date();
        const todayDayOfWeek = today.getDay();

        // Create a repeat config without startDate - getFirstRepeatOccurrence
        // will find the next matching weekday from today
        // Set the repeat to today's weekday so we get today's date
        const weekdayKeys = [
          'sunday',
          'monday',
          'tuesday',
          'wednesday',
          'thursday',
          'friday',
          'saturday',
        ] as const;
        const todayWeekdayKey = weekdayKeys[todayDayOfWeek];

        const action = addTaskRepeatCfgToTask({
          taskRepeatCfg: {
            ...mockRepeatCfg,
            startDate: undefined,
            repeatCycle: 'WEEKLY',
            repeatEvery: 1,
            monday: todayWeekdayKey === 'monday',
            tuesday: todayWeekdayKey === 'tuesday',
            wednesday: todayWeekdayKey === 'wednesday',
            thursday: todayWeekdayKey === 'thursday',
            friday: todayWeekdayKey === 'friday',
            saturday: todayWeekdayKey === 'saturday',
            sunday: todayWeekdayKey === 'sunday',
          },
          taskId: 'parent-task-id',
          startTime: startTimeStr,
          remindAt: TaskReminderOptionId.AtStart,
        });

        actions$ = hot('-a', { a: action });
        taskService.getByIdOnce$.and.returnValue(of(mockTask));

        // When startDate is undefined, getFirstRepeatOccurrence still calculates
        // the next valid occurrence. For WEEKLY on today's day, it returns today.
        const todayStr = getDbDateStr();
        const expectedDateTime = getDateTimeFromClockString(
          startTimeStr,
          dateStrToUtcDate(todayStr).getTime(),
        );

        const expectedAction = TaskSharedActions.scheduleTaskWithTime({
          task: mockTask,
          dueWithTime: expectedDateTime,
          remindAt: remindOptionToMilliseconds(
            expectedDateTime,
            TaskReminderOptionId.AtStart,
          ),
          isMoveToBacklog: false,
          isSkipAutoRemoveFromToday: isToday(expectedDateTime),
        });

        expectObservable(effects.addRepeatCfgToTaskUpdateTask$).toBe('-a', {
          a: expectedAction,
        });
      });
    });

    it('should calculate today for DAILY when startDate is undefined', () => {
      testScheduler.run(({ hot, expectObservable }) => {
        const startTimeStr = '10:00';

        const action = addTaskRepeatCfgToTask({
          taskRepeatCfg: {
            ...mockRepeatCfg,
            startDate: undefined,
            repeatCycle: 'DAILY',
            repeatEvery: 1,
          },
          taskId: 'parent-task-id',
          startTime: startTimeStr,
          remindAt: TaskReminderOptionId.AtStart,
        });

        actions$ = hot('-a', { a: action });
        taskService.getByIdOnce$.and.returnValue(of(mockTask));

        // For DAILY with undefined startDate, getFirstRepeatOccurrence returns today
        const todayStr = getDbDateStr();
        const expectedDateTime = getDateTimeFromClockString(
          startTimeStr,
          dateStrToUtcDate(todayStr).getTime(),
        );

        const expectedAction = TaskSharedActions.scheduleTaskWithTime({
          task: mockTask,
          dueWithTime: expectedDateTime,
          remindAt: remindOptionToMilliseconds(
            expectedDateTime,
            TaskReminderOptionId.AtStart,
          ),
          isMoveToBacklog: false,
          isSkipAutoRemoveFromToday: isToday(expectedDateTime),
        });

        expectObservable(effects.addRepeatCfgToTaskUpdateTask$).toBe('-a', {
          a: expectedAction,
        });
      });
    });

    it('should fallback to Date.now() when no dates available', () => {
      testScheduler.run(({ hot, expectObservable }) => {
        const startTimeStr = '10:00';
        const now = Date.now();

        // Mock Date.now for predictable results
        spyOn(Date, 'now').and.returnValue(now);

        const action = addTaskRepeatCfgToTask({
          taskRepeatCfg: {
            ...mockRepeatCfg,
            startDate: undefined,
            repeatCycle: 'DAILY',
            repeatEvery: 1,
          },
          taskId: 'parent-task-id',
          startTime: startTimeStr,
          remindAt: TaskReminderOptionId.AtStart,
        });

        actions$ = hot('-a', { a: action });

        const taskWithNoDates: Task = {
          ...mockTask,
          dueDay: undefined,
          dueWithTime: undefined,
        };

        taskService.getByIdOnce$.and.returnValue(of(taskWithNoDates));

        const expectedDateTime = getDateTimeFromClockString(startTimeStr, now);

        const expectedAction = TaskSharedActions.scheduleTaskWithTime({
          task: taskWithNoDates,
          dueWithTime: expectedDateTime,
          remindAt: remindOptionToMilliseconds(
            expectedDateTime,
            TaskReminderOptionId.AtStart,
          ),
          isMoveToBacklog: false,
          isSkipAutoRemoveFromToday: isToday(expectedDateTime),
        });

        expectObservable(effects.addRepeatCfgToTaskUpdateTask$).toBe('-a', {
          a: expectedAction,
        });
      });
    });

    it('should handle MONTHLY repeat pattern correctly (issue #5594)', () => {
      testScheduler.run(({ hot, expectObservable }) => {
        const todayStr = getDbDateStr();
        const startTimeStr = '10:00';

        // Set startDate to today so MONTHLY pattern matches today
        const action = addTaskRepeatCfgToTask({
          taskRepeatCfg: {
            ...mockRepeatCfg,
            startDate: todayStr,
            repeatCycle: 'MONTHLY',
            repeatEvery: 1,
          },
          taskId: 'parent-task-id',
          startTime: startTimeStr,
          remindAt: TaskReminderOptionId.AtStart,
        });

        actions$ = hot('-a', { a: action });
        taskService.getByIdOnce$.and.returnValue(of(mockTask));

        // For MONTHLY on the same day of month as startDate, should return today
        const expectedDateTime = getDateTimeFromClockString(
          startTimeStr,
          dateStrToUtcDate(todayStr).getTime(),
        );

        const expectedAction = TaskSharedActions.scheduleTaskWithTime({
          task: mockTask,
          dueWithTime: expectedDateTime,
          remindAt: remindOptionToMilliseconds(
            expectedDateTime,
            TaskReminderOptionId.AtStart,
          ),
          isMoveToBacklog: false,
          isSkipAutoRemoveFromToday: isToday(expectedDateTime),
        });

        expectObservable(effects.addRepeatCfgToTaskUpdateTask$).toBe('-a', {
          a: expectedAction,
        });
      });
    });

    it('should schedule for next matching weekday when WEEKLY pattern does not match today (issue #5594)', () => {
      testScheduler.run(({ hot, expectObservable }) => {
        const todayStr = getDbDateStr();
        const startTimeStr = '10:00';
        const today = new Date();
        const todayDayOfWeek = today.getDay();

        // Create a repeat config for a day that is 3 days from today
        const targetDayOfWeek = (todayDayOfWeek + 3) % 7;
        const weekdayKeys = [
          'sunday',
          'monday',
          'tuesday',
          'wednesday',
          'thursday',
          'friday',
          'saturday',
        ] as const;
        const targetWeekdayKey = weekdayKeys[targetDayOfWeek];

        const action = addTaskRepeatCfgToTask({
          taskRepeatCfg: {
            ...mockRepeatCfg,
            startDate: todayStr,
            repeatCycle: 'WEEKLY',
            repeatEvery: 1,
            monday: targetWeekdayKey === 'monday',
            tuesday: targetWeekdayKey === 'tuesday',
            wednesday: targetWeekdayKey === 'wednesday',
            thursday: targetWeekdayKey === 'thursday',
            friday: targetWeekdayKey === 'friday',
            saturday: targetWeekdayKey === 'saturday',
            sunday: targetWeekdayKey === 'sunday',
          },
          taskId: 'parent-task-id',
          startTime: startTimeStr,
          remindAt: TaskReminderOptionId.AtStart,
        });

        actions$ = hot('-a', { a: action });
        taskService.getByIdOnce$.and.returnValue(of(mockTask));

        // When WEEKLY pattern doesn't match today, getFirstRepeatOccurrence
        // returns the NEXT matching weekday (3 days from today)
        const nextMatchingDate = new Date(today);
        nextMatchingDate.setDate(nextMatchingDate.getDate() + 3);
        nextMatchingDate.setHours(12, 0, 0, 0);
        const nextMatchingDateStr = getDbDateStr(nextMatchingDate);

        const expectedDateTime = getDateTimeFromClockString(
          startTimeStr,
          dateStrToUtcDate(nextMatchingDateStr).getTime(),
        );

        const expectedAction = TaskSharedActions.scheduleTaskWithTime({
          task: mockTask,
          dueWithTime: expectedDateTime,
          remindAt: remindOptionToMilliseconds(
            expectedDateTime,
            TaskReminderOptionId.AtStart,
          ),
          isMoveToBacklog: false,
          isSkipAutoRemoveFromToday: isToday(expectedDateTime),
        });

        expectObservable(effects.addRepeatCfgToTaskUpdateTask$).toBe('-a', {
          a: expectedAction,
        });
      });
    });

    it('should fallback gracefully when repeatEvery is invalid (issue #5594)', () => {
      testScheduler.run(({ hot, expectObservable }) => {
        const todayStr = getDbDateStr();
        const dueDayStr = '2025-03-15';
        const startTimeStr = '10:00';

        // Create a repeat config with invalid repeatEvery (0)
        const action = addTaskRepeatCfgToTask({
          taskRepeatCfg: {
            ...mockRepeatCfg,
            startDate: todayStr,
            repeatCycle: 'DAILY',
            repeatEvery: 0, // Invalid!
          },
          taskId: 'parent-task-id',
          startTime: startTimeStr,
          remindAt: TaskReminderOptionId.AtStart,
        });

        actions$ = hot('-a', { a: action });

        const taskWithDueDay: Task = {
          ...mockTask,
          dueDay: dueDayStr,
        };

        taskService.getByIdOnce$.and.returnValue(of(taskWithDueDay));

        // Should handle gracefully and fall back to task.dueDay
        const expectedDateTime = getDateTimeFromClockString(
          startTimeStr,
          dateStrToUtcDate(dueDayStr).getTime(),
        );

        const expectedAction = TaskSharedActions.scheduleTaskWithTime({
          task: taskWithDueDay,
          dueWithTime: expectedDateTime,
          remindAt: remindOptionToMilliseconds(
            expectedDateTime,
            TaskReminderOptionId.AtStart,
          ),
          isMoveToBacklog: false,
          isSkipAutoRemoveFromToday: isToday(expectedDateTime),
        });

        expectObservable(effects.addRepeatCfgToTaskUpdateTask$).toBe('-a', {
          a: expectedAction,
        });
      });
    });

    it('should handle YEARLY repeat pattern correctly (issue #5594)', () => {
      testScheduler.run(({ hot, expectObservable }) => {
        const todayStr = getDbDateStr();
        const startTimeStr = '10:00';

        // Set startDate to today so YEARLY pattern matches today
        const action = addTaskRepeatCfgToTask({
          taskRepeatCfg: {
            ...mockRepeatCfg,
            startDate: todayStr,
            repeatCycle: 'YEARLY',
            repeatEvery: 1,
          },
          taskId: 'parent-task-id',
          startTime: startTimeStr,
          remindAt: TaskReminderOptionId.AtStart,
        });

        actions$ = hot('-a', { a: action });
        taskService.getByIdOnce$.and.returnValue(of(mockTask));

        // For YEARLY on the same day/month as startDate, should return today
        const expectedDateTime = getDateTimeFromClockString(
          startTimeStr,
          dateStrToUtcDate(todayStr).getTime(),
        );

        const expectedAction = TaskSharedActions.scheduleTaskWithTime({
          task: mockTask,
          dueWithTime: expectedDateTime,
          remindAt: remindOptionToMilliseconds(
            expectedDateTime,
            TaskReminderOptionId.AtStart,
          ),
          isMoveToBacklog: false,
          isSkipAutoRemoveFromToday: isToday(expectedDateTime),
        });

        expectObservable(effects.addRepeatCfgToTaskUpdateTask$).toBe('-a', {
          a: expectedAction,
        });
      });
    });

    it('should use dateStrToUtcDate to avoid UTC parsing issues (issue #5515)', () => {
      // This test verifies that date string parsing uses local midnight, not UTC midnight
      // In far-west timezones (e.g., Hawaii UTC-10), new Date('2025-01-15') would give
      // Jan 14th local date, but dateStrToUtcDate correctly gives Jan 15th local date
      testScheduler.run(({ hot, expectObservable }) => {
        const dateStr = getDbDateStr(); // Use today for predictable results
        const startTimeStr = '14:45'; // 2:45 PM - user's reported time
        const action = addTaskRepeatCfgToTask({
          taskRepeatCfg: {
            ...mockRepeatCfg,
            startDate: dateStr,
            repeatCycle: 'DAILY',
            repeatEvery: 1,
          },
          taskId: 'parent-task-id',
          startTime: startTimeStr,
          remindAt: TaskReminderOptionId.AtStart,
        });

        actions$ = hot('-a', { a: action });
        taskService.getByIdOnce$.and.returnValue(of(mockTask));

        // Verify dateStrToUtcDate gives correct local date
        const localDate = dateStrToUtcDate(dateStr);
        const today = new Date();
        expect(localDate.getDate()).toBe(today.getDate());
        expect(localDate.getMonth()).toBe(today.getMonth());
        expect(localDate.getFullYear()).toBe(today.getFullYear());

        // The scheduled time should be 14:45 on today local time
        const expectedDateTime = getDateTimeFromClockString(
          startTimeStr,
          localDate.getTime(),
        );
        const scheduledDate = new Date(expectedDateTime);
        expect(scheduledDate.getHours()).toBe(14);
        expect(scheduledDate.getMinutes()).toBe(45);

        const expectedAction = TaskSharedActions.scheduleTaskWithTime({
          task: mockTask,
          dueWithTime: expectedDateTime,
          remindAt: remindOptionToMilliseconds(
            expectedDateTime,
            TaskReminderOptionId.AtStart,
          ),
          isMoveToBacklog: false,
          isSkipAutoRemoveFromToday: isToday(expectedDateTime),
        });

        expectObservable(effects.addRepeatCfgToTaskUpdateTask$).toBe('-a', {
          a: expectedAction,
        });
      });
    });

    it('should NOT schedule task when remindAt is undefined', () => {
      testScheduler.run(({ hot, expectObservable }) => {
        const todayStr = getDbDateStr();
        const startTimeStr = '10:00';
        const action = addTaskRepeatCfgToTask({
          taskRepeatCfg: {
            ...mockRepeatCfg,
            startDate: todayStr,
            repeatCycle: 'DAILY',
            repeatEvery: 1,
          },
          taskId: 'parent-task-id',
          startTime: startTimeStr,
          remindAt: undefined, // No remindAt - should filter out
        });

        actions$ = hot('-a', { a: action });

        // Effect should NOT emit because remindAt is undefined
        expectObservable(effects.addRepeatCfgToTaskUpdateTask$).toBe('--');
      });
    });

    it('should NOT schedule task when startTime is undefined', () => {
      testScheduler.run(({ hot, expectObservable }) => {
        const todayStr = getDbDateStr();
        const action = addTaskRepeatCfgToTask({
          taskRepeatCfg: {
            ...mockRepeatCfg,
            startDate: todayStr,
            repeatCycle: 'DAILY',
            repeatEvery: 1,
          },
          taskId: 'parent-task-id',
          startTime: undefined, // No startTime - should filter out
          remindAt: TaskReminderOptionId.AtStart,
        });

        actions$ = hot('-a', { a: action });

        // Effect should NOT emit because startTime is undefined
        expectObservable(effects.addRepeatCfgToTaskUpdateTask$).toBe('--');
      });
    });

    it('should NOT schedule task when both startTime and remindAt are undefined', () => {
      testScheduler.run(({ hot, expectObservable }) => {
        const todayStr = getDbDateStr();
        const action = addTaskRepeatCfgToTask({
          taskRepeatCfg: {
            ...mockRepeatCfg,
            startDate: todayStr,
            repeatCycle: 'DAILY',
            repeatEvery: 1,
          },
          taskId: 'parent-task-id',
          startTime: undefined,
          remindAt: undefined,
        });

        actions$ = hot('-a', { a: action });

        // Effect should NOT emit because both are undefined
        expectObservable(effects.addRepeatCfgToTaskUpdateTask$).toBe('--');
      });
    });

    it('should NOT schedule task when startTime is an invalid clock string (bug #7067)', () => {
      testScheduler.run(({ hot, expectObservable }) => {
        const todayStr = getDbDateStr();
        const action = addTaskRepeatCfgToTask({
          taskRepeatCfg: {
            ...mockRepeatCfg,
            startDate: todayStr,
            repeatCycle: 'DAILY',
            repeatEvery: 1,
          },
          taskId: 'parent-task-id',
          startTime: 'INVALID_CLOCK_STRING',
          remindAt: TaskReminderOptionId.AtStart,
        });

        actions$ = hot('-a', { a: action });

        // Effect should NOT emit because startTime fails isValidSplitTime
        expectObservable(effects.addRepeatCfgToTaskUpdateTask$).toBe('--');
      });
    });
  });

  describe('rescheduleTaskOnRepeatCfgUpdate$', () => {
    it('should dispatch planTaskForDay when schedule-affecting field changes and first occurrence is future', (done) => {
      const today = new Date();
      const todayStr = getDbDateStr(today);
      const todayDayOfWeek = today.getDay();

      // Pick a weekday that is 3 days from now (guaranteed future)
      const targetDayOfWeek = (todayDayOfWeek + 3) % 7;

      const liveTask: Task = {
        ...mockTask,
        isDone: false,
        dueDay: todayStr,
        created: today.getTime(),
      };

      const updatedCfg: TaskRepeatCfgCopy = {
        ...mockRepeatCfg,
        repeatCycle: 'WEEKLY',
        repeatEvery: 1,
        startDate: todayStr,
        sunday: targetDayOfWeek === 0,
        monday: targetDayOfWeek === 1,
        tuesday: targetDayOfWeek === 2,
        wednesday: targetDayOfWeek === 3,
        thursday: targetDayOfWeek === 4,
        friday: targetDayOfWeek === 5,
        saturday: targetDayOfWeek === 6,
      };

      const firstOccurrence = getNextRepeatOccurrence(updatedCfg as any, new Date())!;
      const firstOccurrenceStr = getDbDateStr(firstOccurrence);

      const action = updateTaskRepeatCfg({
        taskRepeatCfg: {
          id: 'repeat-cfg-id',
          changes: { startDate: todayStr },
        },
      });

      actions$ = of(action);
      taskRepeatCfgService.getTaskRepeatCfgById$.and.returnValue(of(updatedCfg));
      taskService.getTasksByRepeatCfgId$.and.returnValue(of([liveTask]));

      effects.rescheduleTaskOnRepeatCfgUpdate$.subscribe((result) => {
        expect(result).toEqual(
          PlannerActions.planTaskForDay({
            task: liveTask as any,
            day: firstOccurrenceStr,
          }),
        );
        expect(taskRepeatCfgService.updateTaskRepeatCfg).toHaveBeenCalledWith(
          'repeat-cfg-id',
          jasmine.objectContaining({
            lastTaskCreationDay: firstOccurrenceStr,
          }),
        );
        done();
      });
    });

    // Issue #7951: editing a schedule-affecting field of a DAILY repeat whose
    // current day is still a valid occurrence must NOT strand today's live
    // instance by moving it to tomorrow. Before the fix the effect used the
    // strictly-future getNextRepeatOccurrence(), so today's task disappeared
    // from Today and lastTaskCreationDay jumped past today — permanently
    // skipping today's instance.
    it('should keep the live instance on today when a DAILY repeat still includes today (#7951)', (done) => {
      const today = new Date();
      const todayStr = getDbDateStr(today);

      const liveTask: Task = {
        ...mockTask,
        isDone: false,
        dueDay: todayStr,
        created: today.getTime(),
      };

      const updatedCfg: TaskRepeatCfgCopy = {
        ...mockRepeatCfg,
        repeatCycle: 'DAILY',
        repeatEvery: 1,
        startDate: todayStr,
        lastTaskCreationDay: todayStr,
      };

      const action = updateTaskRepeatCfg({
        taskRepeatCfg: {
          id: 'repeat-cfg-id',
          changes: { repeatEvery: 1 },
        },
      });

      actions$ = of(action);
      taskRepeatCfgService.getTaskRepeatCfgById$.and.returnValue(of(updatedCfg));
      taskService.getTasksByRepeatCfgId$.and.returnValue(of([liveTask]));

      effects.rescheduleTaskOnRepeatCfgUpdate$.subscribe((result) => {
        expect(result).toEqual(
          PlannerActions.planTaskForDay({ task: liveTask as any, day: todayStr }),
        );
        expect(taskRepeatCfgService.updateTaskRepeatCfg).toHaveBeenCalledWith(
          'repeat-cfg-id',
          jasmine.objectContaining({ lastTaskCreationDay: todayStr }),
        );
        done();
      });
    });

    // #7951 follow-up: an OVERDUE undone instance (dueDay in the past) must also
    // be relocated to today on a schedule edit, not pushed to tomorrow (which
    // would skip both its overdue day and today).
    it('relocates an overdue live instance to today for a DAILY repeat (#7951)', (done) => {
      const THREE_DAYS = 3 * 24 * 60 * 60 * 1000;
      const today = new Date();
      const todayStr = getDbDateStr(today);
      const threeDaysAgo = today.getTime() - THREE_DAYS;
      const threeDaysAgoStr = getDbDateStr(new Date(threeDaysAgo));

      const overdueTask: Task = {
        ...mockTask,
        isDone: false,
        dueDay: threeDaysAgoStr,
        created: threeDaysAgo,
      };

      const updatedCfg: TaskRepeatCfgCopy = {
        ...mockRepeatCfg,
        repeatCycle: 'DAILY',
        repeatEvery: 1,
        startDate: threeDaysAgoStr,
        lastTaskCreationDay: threeDaysAgoStr,
      };

      const action = updateTaskRepeatCfg({
        taskRepeatCfg: {
          id: 'repeat-cfg-id',
          changes: { repeatEvery: 1 },
        },
      });

      actions$ = of(action);
      taskRepeatCfgService.getTaskRepeatCfgById$.and.returnValue(of(updatedCfg));
      taskService.getTasksByRepeatCfgId$.and.returnValue(of([overdueTask]));

      effects.rescheduleTaskOnRepeatCfgUpdate$.subscribe((result) => {
        expect(result).toEqual(
          PlannerActions.planTaskForDay({ task: overdueTask as any, day: todayStr }),
        );
        expect(taskRepeatCfgService.updateTaskRepeatCfg).toHaveBeenCalledWith(
          'repeat-cfg-id',
          jasmine.objectContaining({ lastTaskCreationDay: todayStr }),
        );
        done();
      });
    });

    it('should dispatch scheduleTaskWithTime when schedule-affecting field changes and task is timed', (done) => {
      const today = new Date();
      const todayStr = getDbDateStr(today);

      const liveTask: Task = {
        ...mockTask,
        isDone: false,
        dueDay: todayStr,
        created: today.getTime(),
      };

      const updatedCfg: TaskRepeatCfgCopy = {
        ...mockRepeatCfg,
        repeatCycle: 'DAILY',
        repeatEvery: 1,
        startDate: todayStr,
        startTime: '10:00',
        remindAt: TaskReminderOptionId.AtStart,
      };

      const action = updateTaskRepeatCfg({
        taskRepeatCfg: {
          id: 'repeat-cfg-id',
          changes: { startDate: todayStr },
        },
      });

      actions$ = of(action);
      taskRepeatCfgService.getTaskRepeatCfgById$.and.returnValue(of(updatedCfg));
      taskService.getTasksByRepeatCfgId$.and.returnValue(of([liveTask]));

      effects.rescheduleTaskOnRepeatCfgUpdate$.subscribe((result) => {
        expect(result.type).toBe(TaskSharedActions.scheduleTaskWithTime.type);
        done();
      });
    });

    // #7951 review (W1): a timed task whose start time has already passed today
    // must NOT be scheduled at that past time — that would set a past remindAt
    // and fire an immediate "missed reminder" on save (#7354). It advances to
    // the next future slot instead.
    it('does not schedule a past remindAt for a timed task whose start time passed today (#7951)', (done) => {
      const now = Date.now();
      const today = new Date();
      const todayStr = getDbDateStr(today);

      const liveTask: Task = {
        ...mockTask,
        isDone: false,
        dueDay: todayStr,
        created: today.getTime(),
      };

      const updatedCfg: TaskRepeatCfgCopy = {
        ...mockRepeatCfg,
        repeatCycle: 'DAILY',
        repeatEvery: 1,
        startDate: todayStr,
        lastTaskCreationDay: todayStr,
        // start of today — always in the past relative to "now"
        startTime: '00:00',
        remindAt: TaskReminderOptionId.AtStart,
      };

      const action = updateTaskRepeatCfg({
        taskRepeatCfg: {
          id: 'repeat-cfg-id',
          changes: { repeatEvery: 1 },
        },
      });

      actions$ = of(action);
      taskRepeatCfgService.getTaskRepeatCfgById$.and.returnValue(of(updatedCfg));
      taskService.getTasksByRepeatCfgId$.and.returnValue(of([liveTask]));

      effects.rescheduleTaskOnRepeatCfgUpdate$.subscribe((result) => {
        const scheduled = result as ReturnType<
          typeof TaskSharedActions.scheduleTaskWithTime
        >;
        expect(scheduled.type).toBe(TaskSharedActions.scheduleTaskWithTime.type);
        // The scheduled time and reminder must be in the future, not this morning.
        expect(scheduled.dueWithTime).toBeGreaterThan(now);
        expect(scheduled.remindAt).toBeGreaterThanOrEqual(now);
        done();
      });
    });

    it('should NOT dispatch when only non-schedule fields change', (done) => {
      const action = updateTaskRepeatCfg({
        taskRepeatCfg: {
          id: 'repeat-cfg-id',
          changes: { title: 'New Title', notes: 'New Notes' },
        },
      });

      actions$ = of(action);

      let emitted = false;
      effects.rescheduleTaskOnRepeatCfgUpdate$.subscribe(() => {
        emitted = true;
      });

      setTimeout(() => {
        expect(emitted).toBe(false);
        done();
      }, 0);
    });

    it('should NOT dispatch when no live task instance exists', (done) => {
      const today = new Date();
      const todayStr = getDbDateStr(today);

      const updatedCfg: TaskRepeatCfgCopy = {
        ...mockRepeatCfg,
        repeatCycle: 'DAILY',
        repeatEvery: 1,
        startDate: todayStr,
      };

      const action = updateTaskRepeatCfg({
        taskRepeatCfg: {
          id: 'repeat-cfg-id',
          changes: { startDate: todayStr },
        },
      });

      actions$ = of(action);
      taskRepeatCfgService.getTaskRepeatCfgById$.and.returnValue(of(updatedCfg));
      taskService.getTasksByRepeatCfgId$.and.returnValue(of([]));

      let emitted = false;
      effects.rescheduleTaskOnRepeatCfgUpdate$.subscribe(() => {
        emitted = true;
      });

      setTimeout(() => {
        expect(emitted).toBe(false);
        // #7724 guard: startDate present in changes but not moved earlier
        // (equal to lastTaskCreationDay) must not re-anchor the config.
        expect(taskRepeatCfgService.updateTaskRepeatCfg).not.toHaveBeenCalled();
        done();
      }, 0);
    });

    it('should NOT dispatch when task is already done', (done) => {
      const today = new Date();
      const todayStr = getDbDateStr(today);

      const doneTask: Task = {
        ...mockTask,
        isDone: true,
        dueDay: todayStr,
        created: today.getTime(),
      };

      const updatedCfg: TaskRepeatCfgCopy = {
        ...mockRepeatCfg,
        repeatCycle: 'DAILY',
        repeatEvery: 1,
        startDate: todayStr,
      };

      const action = updateTaskRepeatCfg({
        taskRepeatCfg: {
          id: 'repeat-cfg-id',
          changes: { startDate: todayStr },
        },
      });

      actions$ = of(action);
      taskRepeatCfgService.getTaskRepeatCfgById$.and.returnValue(of(updatedCfg));
      taskService.getTasksByRepeatCfgId$.and.returnValue(of([doneTask]));

      let emitted = false;
      effects.rescheduleTaskOnRepeatCfgUpdate$.subscribe(() => {
        emitted = true;
      });

      setTimeout(() => {
        expect(emitted).toBe(false);
        done();
      }, 0);
    });

    it('should NOT dispatch when config is paused', (done) => {
      const today = new Date();
      const todayStr = getDbDateStr(today);

      const pausedCfg: TaskRepeatCfgCopy = {
        ...mockRepeatCfg,
        isPaused: true,
        repeatCycle: 'DAILY',
        repeatEvery: 1,
        startDate: todayStr,
      };

      const action = updateTaskRepeatCfg({
        taskRepeatCfg: {
          id: 'repeat-cfg-id',
          changes: { isPaused: true },
        },
      });

      actions$ = of(action);
      taskRepeatCfgService.getTaskRepeatCfgById$.and.returnValue(of(pausedCfg));

      let emitted = false;
      effects.rescheduleTaskOnRepeatCfgUpdate$.subscribe(() => {
        emitted = true;
      });

      setTimeout(() => {
        expect(emitted).toBe(false);
        done();
      }, 0);
    });

    it('should update lastTaskCreationDay on the repeat config', (done) => {
      const today = new Date();
      const todayStr = getDbDateStr(today);
      const todayDayOfWeek = today.getDay();
      const targetDayOfWeek = (todayDayOfWeek + 3) % 7;

      const liveTask: Task = {
        ...mockTask,
        isDone: false,
        dueDay: todayStr,
        created: today.getTime(),
      };

      const updatedCfg: TaskRepeatCfgCopy = {
        ...mockRepeatCfg,
        repeatCycle: 'WEEKLY',
        repeatEvery: 1,
        startDate: todayStr,
        sunday: targetDayOfWeek === 0,
        monday: targetDayOfWeek === 1,
        tuesday: targetDayOfWeek === 2,
        wednesday: targetDayOfWeek === 3,
        thursday: targetDayOfWeek === 4,
        friday: targetDayOfWeek === 5,
        saturday: targetDayOfWeek === 6,
      };

      const firstOccurrence = getNextRepeatOccurrence(updatedCfg as any, new Date())!;
      const firstOccurrenceStr = getDbDateStr(firstOccurrence);

      const action = updateTaskRepeatCfg({
        taskRepeatCfg: {
          id: 'repeat-cfg-id',
          changes: { repeatCycle: 'WEEKLY' },
        },
      });

      actions$ = of(action);
      taskRepeatCfgService.getTaskRepeatCfgById$.and.returnValue(of(updatedCfg));
      taskService.getTasksByRepeatCfgId$.and.returnValue(of([liveTask]));

      effects.rescheduleTaskOnRepeatCfgUpdate$.subscribe(() => {
        expect(taskRepeatCfgService.updateTaskRepeatCfg).toHaveBeenCalledWith(
          'repeat-cfg-id',
          jasmine.objectContaining({
            lastTaskCreationDay: firstOccurrenceStr,
            lastTaskCreation: jasmine.any(Number),
          }),
        );
        done();
      });
    });

    const addDays = (base: Date, days: number): Date => {
      const d = new Date(base);
      d.setDate(d.getDate() + days);
      return d;
    };

    // Issue #7423: moving startDate earlier than the existing lastTaskCreationDay
    // must re-anchor on the new startDate. Previously, the stale lastTaskCreationDay
    // bumped checkDate inside getNextRepeatOccurrence, so the task landed one day
    // after the OLD startDate instead of on the NEW startDate.
    it('should plan task for the new startDate when moving startDate earlier than lastTaskCreationDay (#7423)', (done) => {
      const today = new Date();
      const oldStartDateStr = getDbDateStr(addDays(today, 29));
      const newStartDateStr = getDbDateStr(addDays(today, 9));

      const liveTask: Task = {
        ...mockTask,
        isDone: false,
        dueDay: oldStartDateStr,
        created: today.getTime(),
      };

      // Config as it would look right after the user saves the new startDate:
      // startDate is the new value but lastTaskCreationDay still points at the
      // old startDate (set by updateTaskAfterMakingItRepeatable$ on initial save).
      const updatedCfg: TaskRepeatCfgCopy = {
        ...mockRepeatCfg,
        repeatCycle: 'DAILY',
        repeatEvery: 1,
        startDate: newStartDateStr,
        lastTaskCreationDay: oldStartDateStr,
      };

      const action = updateTaskRepeatCfg({
        taskRepeatCfg: {
          id: 'repeat-cfg-id',
          changes: { startDate: newStartDateStr },
        },
      });

      actions$ = of(action);
      taskRepeatCfgService.getTaskRepeatCfgById$.and.returnValue(of(updatedCfg));
      taskService.getTasksByRepeatCfgId$.and.returnValue(of([liveTask]));

      effects.rescheduleTaskOnRepeatCfgUpdate$.subscribe((result) => {
        expect(result).toEqual(
          PlannerActions.planTaskForDay({
            task: liveTask as any,
            day: newStartDateStr,
          }),
        );
        expect(taskRepeatCfgService.updateTaskRepeatCfg).toHaveBeenCalledWith(
          'repeat-cfg-id',
          jasmine.objectContaining({
            lastTaskCreationDay: newStartDateStr,
          }),
        );
        done();
      });
    });

    // Issue #7423: same earlier-startDate scenario for WEEKLY repeat.
    it('should plan task for the new startDate for WEEKLY when moved earlier (#7423)', (done) => {
      const today = new Date();
      const todayDayOfWeek = today.getDay();

      // Both old and new startDate land on the same weekday, 28 days and 7 days out.
      const oldStartDateStr = getDbDateStr(addDays(today, 28));
      const newStartDateStr = getDbDateStr(addDays(today, 7));

      const liveTask: Task = {
        ...mockTask,
        isDone: false,
        dueDay: oldStartDateStr,
        created: today.getTime(),
      };

      const weekdayKeys = [
        'sunday',
        'monday',
        'tuesday',
        'wednesday',
        'thursday',
        'friday',
        'saturday',
      ] as const;
      const todayWeekdayKey = weekdayKeys[todayDayOfWeek];

      const updatedCfg: TaskRepeatCfgCopy = {
        ...mockRepeatCfg,
        repeatCycle: 'WEEKLY',
        repeatEvery: 1,
        startDate: newStartDateStr,
        lastTaskCreationDay: oldStartDateStr,
        sunday: todayWeekdayKey === 'sunday',
        monday: todayWeekdayKey === 'monday',
        tuesday: todayWeekdayKey === 'tuesday',
        wednesday: todayWeekdayKey === 'wednesday',
        thursday: todayWeekdayKey === 'thursday',
        friday: todayWeekdayKey === 'friday',
        saturday: todayWeekdayKey === 'saturday',
      };

      const action = updateTaskRepeatCfg({
        taskRepeatCfg: {
          id: 'repeat-cfg-id',
          changes: { startDate: newStartDateStr },
        },
      });

      actions$ = of(action);
      taskRepeatCfgService.getTaskRepeatCfgById$.and.returnValue(of(updatedCfg));
      taskService.getTasksByRepeatCfgId$.and.returnValue(of([liveTask]));

      effects.rescheduleTaskOnRepeatCfgUpdate$.subscribe((result) => {
        expect(result).toEqual(
          PlannerActions.planTaskForDay({
            task: liveTask as any,
            day: newStartDateStr,
          }),
        );
        done();
      });
    });

    // Issue #7423: same earlier-startDate scenario for MONTHLY repeat.
    it('should plan task for the new startDate for MONTHLY when moved earlier (#7423)', (done) => {
      const today = new Date();
      const oldStartDateStr = getDbDateStr(addDays(today, 29));
      const newStartDateStr = getDbDateStr(addDays(today, 9));

      const liveTask: Task = {
        ...mockTask,
        isDone: false,
        dueDay: oldStartDateStr,
        created: today.getTime(),
      };

      const updatedCfg: TaskRepeatCfgCopy = {
        ...mockRepeatCfg,
        repeatCycle: 'MONTHLY',
        repeatEvery: 1,
        startDate: newStartDateStr,
        lastTaskCreationDay: oldStartDateStr,
      };

      const action = updateTaskRepeatCfg({
        taskRepeatCfg: {
          id: 'repeat-cfg-id',
          changes: { startDate: newStartDateStr },
        },
      });

      actions$ = of(action);
      taskRepeatCfgService.getTaskRepeatCfgById$.and.returnValue(of(updatedCfg));
      taskService.getTasksByRepeatCfgId$.and.returnValue(of([liveTask]));

      effects.rescheduleTaskOnRepeatCfgUpdate$.subscribe((result) => {
        expect(result).toEqual(
          PlannerActions.planTaskForDay({
            task: liveTask as any,
            day: newStartDateStr,
          }),
        );
        done();
      });
    });

    // Issue #7423 guard: for repeatFromCompletionDate=true, startDate is
    // decoupled from scheduling (getEffectiveRepeatStartDate returns
    // lastTaskCreationDay). Editing startDate must NOT take the re-anchor
    // branch — doing so would wipe completion history and re-schedule from
    // an arbitrary past anchor.
    it('should NOT re-anchor on startDate edit when repeatFromCompletionDate is true (#7423)', (done) => {
      const today = new Date();

      // lastTaskCreationDay (April 15) > new startDate (Feb 1) — the same
      // shape as the bug, but on a repeatFromCompletionDate config.
      const lastCompletionStr = getDbDateStr(addDays(today, -16));
      const newStartDateStr = getDbDateStr(addDays(today, -89));

      const liveTask: Task = {
        ...mockTask,
        isDone: false,
        dueDay: lastCompletionStr,
        created: today.getTime(),
      };

      const updatedCfg: TaskRepeatCfgCopy = {
        ...mockRepeatCfg,
        repeatCycle: 'DAILY',
        repeatEvery: 1,
        startDate: newStartDateStr,
        lastTaskCreationDay: lastCompletionStr,
        repeatFromCompletionDate: true,
      };

      const action = updateTaskRepeatCfg({
        taskRepeatCfg: {
          id: 'repeat-cfg-id',
          changes: { startDate: newStartDateStr },
        },
      });

      actions$ = of(action);
      taskRepeatCfgService.getTaskRepeatCfgById$.and.returnValue(of(updatedCfg));
      taskService.getTasksByRepeatCfgId$.and.returnValue(of([liveTask]));

      // The re-anchor branch (which would call getFirstRepeatOccurrence and
      // schedule for newStartDateStr) must NOT fire. Capture the planned day
      // and assert it never equals the past startDate.
      let emittedDay: string | undefined;
      effects.rescheduleTaskOnRepeatCfgUpdate$.subscribe((result) => {
        if (result.type === PlannerActions.planTaskForDay.type) {
          emittedDay = result.day;
        }
      });
      setTimeout(() => {
        expect(emittedDay).not.toBe(newStartDateStr);
        done();
      }, 0);
    });

    // Issue #7423: edge case where the new startDate lies in the past relative
    // to today. The re-anchor branch should still fire (newStartDate <
    // lastTaskCreationDay holds), and the task is planned for that past date —
    // mirroring how updateTaskAfterMakingItRepeatable$ schedules past first
    // occurrences.
    it('should plan task for the new startDate even when it is in the past (#7423)', (done) => {
      const today = new Date();
      // lastTaskCreationDay 5 days out (set when old startDate was T+5).
      const oldStartDateStr = getDbDateStr(addDays(today, 5));
      // New startDate is 3 days in the past.
      const newStartDateStr = getDbDateStr(addDays(today, -3));

      const liveTask: Task = {
        ...mockTask,
        isDone: false,
        dueDay: oldStartDateStr,
        created: today.getTime(),
      };

      const updatedCfg: TaskRepeatCfgCopy = {
        ...mockRepeatCfg,
        repeatCycle: 'DAILY',
        repeatEvery: 1,
        startDate: newStartDateStr,
        lastTaskCreationDay: oldStartDateStr,
      };

      const action = updateTaskRepeatCfg({
        taskRepeatCfg: {
          id: 'repeat-cfg-id',
          changes: { startDate: newStartDateStr },
        },
      });

      actions$ = of(action);
      taskRepeatCfgService.getTaskRepeatCfgById$.and.returnValue(of(updatedCfg));
      taskService.getTasksByRepeatCfgId$.and.returnValue(of([liveTask]));

      effects.rescheduleTaskOnRepeatCfgUpdate$.subscribe((result) => {
        expect(result).toEqual(
          PlannerActions.planTaskForDay({
            task: liveTask as any,
            day: newStartDateStr,
          }),
        );
        done();
      });
    });

    // Issue #7724: moving startDate earlier must re-anchor lastTaskCreationDay
    // even when no live task instance exists (e.g. the user deleted it). The
    // stale anchor would otherwise suppress every projected/created instance
    // between the new startDate and the old anchor.
    it('should re-anchor lastTaskCreationDay when startDate moved earlier and no live instance exists (#7724)', (done) => {
      const today = new Date();
      // lastTaskCreationDay set when the (now deleted) instance was created.
      const oldStartDateStr = getDbDateStr(addDays(today, 8));
      const newStartDateStr = getDbDateStr(addDays(today, 3));
      // The fix anchors to the day before the new first occurrence so the new
      // startDate itself is created/projected fresh.
      const expectedAnchorStr = getDbDateStr(addDays(today, 2));

      const updatedCfg: TaskRepeatCfgCopy = {
        ...mockRepeatCfg,
        repeatCycle: 'DAILY',
        repeatEvery: 1,
        startDate: newStartDateStr,
        lastTaskCreationDay: oldStartDateStr,
      };

      const action = updateTaskRepeatCfg({
        taskRepeatCfg: {
          id: 'repeat-cfg-id',
          changes: { startDate: newStartDateStr },
        },
      });

      actions$ = of(action);
      taskRepeatCfgService.getTaskRepeatCfgById$.and.returnValue(of(updatedCfg));
      // No live instance — it was deleted.
      taskService.getTasksByRepeatCfgId$.and.returnValue(of([]));

      let emitted = false;
      effects.rescheduleTaskOnRepeatCfgUpdate$.subscribe(() => {
        emitted = true;
      });

      setTimeout(() => {
        // No live task to reschedule, so no action is dispatched...
        expect(emitted).toBe(false);
        // ...but the stale anchor is still corrected.
        expect(taskRepeatCfgService.updateTaskRepeatCfg).toHaveBeenCalledWith(
          'repeat-cfg-id',
          jasmine.objectContaining({
            lastTaskCreationDay: expectedAnchorStr,
          }),
        );
        done();
      }, 0);
    });

    // Issue #7768 Bug 1: moving startDate to today must move the existing
    // live instance to today, not leave it stranded on its previous due day.
    // The pre-fix effect skipped planTaskForDay when first occurrence was
    // today (the `!isFirstOccurrenceToday_` guard), so the task's dueDay
    // stayed on the old startDate.
    it('should plan task for today when startDate is moved to today and a live instance exists (#7768)', (done) => {
      const today = new Date();
      const todayStr = getDbDateStr(today);
      const oldStartDateStr = getDbDateStr(addDays(today, 5));

      const liveTask: Task = {
        ...mockTask,
        isDone: false,
        dueDay: oldStartDateStr,
        created: today.getTime(),
      };

      const updatedCfg: TaskRepeatCfgCopy = {
        ...mockRepeatCfg,
        repeatCycle: 'DAILY',
        repeatEvery: 1,
        startDate: todayStr,
        lastTaskCreationDay: oldStartDateStr,
      };

      const action = updateTaskRepeatCfg({
        taskRepeatCfg: {
          id: 'repeat-cfg-id',
          changes: { startDate: todayStr },
        },
      });

      actions$ = of(action);
      taskRepeatCfgService.getTaskRepeatCfgById$.and.returnValue(of(updatedCfg));
      taskService.getTasksByRepeatCfgId$.and.returnValue(of([liveTask]));

      effects.rescheduleTaskOnRepeatCfgUpdate$.subscribe((result) => {
        expect(result).toEqual(
          PlannerActions.planTaskForDay({
            task: liveTask as any,
            day: todayStr,
          }),
        );
        done();
      });
    });

    // Issue #7768 Bug 2: moving startDate to today when no live instance
    // exists must create one for today. The re-anchor alone is not enough —
    // addAllDueToday() is only triggered on date change, so without an
    // explicit call here the live instance won't appear until app restart
    // or the next midnight.
    it('should trigger addAllDueToday when startDate is moved to today and no live instance exists (#7768)', (done) => {
      const today = new Date();
      const todayStr = getDbDateStr(today);
      const oldStartDateStr = getDbDateStr(addDays(today, 5));
      const expectedAnchorStr = getDbDateStr(addDays(today, -1));

      const updatedCfg: TaskRepeatCfgCopy = {
        ...mockRepeatCfg,
        repeatCycle: 'DAILY',
        repeatEvery: 1,
        startDate: todayStr,
        lastTaskCreationDay: oldStartDateStr,
      };

      const action = updateTaskRepeatCfg({
        taskRepeatCfg: {
          id: 'repeat-cfg-id',
          changes: { startDate: todayStr },
        },
      });

      actions$ = of(action);
      taskRepeatCfgService.getTaskRepeatCfgById$.and.returnValue(of(updatedCfg));
      taskService.getTasksByRepeatCfgId$.and.returnValue(of([]));

      let emitted = false;
      effects.rescheduleTaskOnRepeatCfgUpdate$.subscribe(() => {
        emitted = true;
      });

      setTimeout(() => {
        expect(emitted).toBe(false);
        // Anchor is re-set to the day before today so addAllDueToday picks
        // today up.
        expect(taskRepeatCfgService.updateTaskRepeatCfg).toHaveBeenCalledWith(
          'repeat-cfg-id',
          jasmine.objectContaining({
            lastTaskCreationDay: expectedAnchorStr,
          }),
        );
        // The live instance for today must actually be created — not only
        // unblocked for the next date change.
        expect(addTasksForTomorrowService.addAllDueToday).toHaveBeenCalled();
        done();
      }, 0);
    });
  });
});

/**
 * Deterministic tests using jasmine.clock() to mock specific dates.
 * These tests ensure reliable, reproducible results regardless of when they run.
 */
describe('TaskRepeatCfgEffects - Deterministic Date Scenarios', () => {
  let actions$: Observable<Action>;
  let effects: TaskRepeatCfgEffects;
  let taskService: jasmine.SpyObj<TaskService>;
  let taskRepeatCfgService: jasmine.SpyObj<TaskRepeatCfgService>;
  let testScheduler: TestScheduler;

  // Fixed reference date: Wednesday, January 15, 2025
  const FIXED_WEDNESDAY = new Date(2025, 0, 15, 12, 0, 0);

  const baseTask: Task = {
    ...DEFAULT_TASK,
    id: 'test-task-id',
    title: 'Test Task',
    projectId: 'test-project',
    repeatCfgId: 'repeat-cfg-id',
    subTaskIds: [],
    created: FIXED_WEDNESDAY.getTime(),
  };

  const baseRepeatCfg: TaskRepeatCfgCopy = {
    ...DEFAULT_TASK_REPEAT_CFG,
    id: 'repeat-cfg-id',
    title: 'Test Repeat',
    lastTaskCreationDay: '1970-01-01', // No previous creation
    shouldInheritSubtasks: false,
    disableAutoUpdateSubtasks: false,
    subTaskTemplates: [],
  };

  beforeEach(() => {
    // Install jasmine clock BEFORE TestBed setup
    jasmine.clock().install();
    jasmine.clock().mockDate(FIXED_WEDNESDAY);

    const taskServiceSpy = jasmine.createSpyObj('TaskService', [
      'getByIdWithSubTaskData$',
      'getByIdOnce$',
      'getTasksByRepeatCfgId$',
      'getByIdsLive$',
      'getArchiveTasksForRepeatCfgId',
      'update',
      'scheduleTask',
      'reScheduleTask',
      'updateTags',
    ]);

    const taskRepeatCfgServiceSpy = jasmine.createSpyObj('TaskRepeatCfgService', [
      'updateTaskRepeatCfg',
      'getTaskRepeatCfgById$',
    ]);

    const matDialogSpy = jasmine.createSpyObj('MatDialog', ['open']);
    const taskArchiveServiceSpy = jasmine.createSpyObj('TaskArchiveService', ['load']);
    const addTasksForTomorrowServiceSpy = jasmine.createSpyObj(
      'AddTasksForTomorrowService',
      ['addAllDueToday'],
    );
    addTasksForTomorrowServiceSpy.addAllDueToday.and.returnValue(Promise.resolve());
    TestBed.configureTestingModule({
      providers: [
        TaskRepeatCfgEffects,
        provideMockActions(() => actions$),
        { provide: TaskService, useValue: taskServiceSpy },
        { provide: TaskRepeatCfgService, useValue: taskRepeatCfgServiceSpy },
        { provide: MatDialog, useValue: matDialogSpy },
        { provide: TaskArchiveService, useValue: taskArchiveServiceSpy },
        {
          provide: AddTasksForTomorrowService,
          useValue: addTasksForTomorrowServiceSpy,
        },
      ],
    });

    effects = TestBed.inject(TaskRepeatCfgEffects);
    taskService = TestBed.inject(TaskService) as jasmine.SpyObj<TaskService>;
    taskRepeatCfgService = TestBed.inject(
      TaskRepeatCfgService,
    ) as jasmine.SpyObj<TaskRepeatCfgService>;

    testScheduler = new TestScheduler((actual, expected) => {
      expect(actual).toEqual(expected);
    });
  });

  afterEach(() => {
    jasmine.clock().uninstall();
  });

  describe('Scenario: Wednesday creating task for different weekdays', () => {
    // Today is Wednesday (Jan 15, 2025)
    // getFirstRepeatOccurrence finds the NEXT matching weekday from today
    const scenarios = [
      {
        name: 'WEEKLY on Wednesday (today) - should schedule for today',
        weekday: 'wednesday',
        expectedDateStr: '2025-01-15', // Today
        shouldMatchToday: true,
      },
      {
        // Friday is 2 days ahead - getFirstRepeatOccurrence finds next Friday
        name: 'WEEKLY on Friday - schedules for next Friday (Jan 17)',
        weekday: 'friday',
        expectedDateStr: '2025-01-17', // Next Friday (2 days from today)
        shouldMatchToday: true,
      },
      {
        // Monday is 5 days ahead - getFirstRepeatOccurrence finds next Monday
        name: 'WEEKLY on Monday - schedules for next Monday (Jan 20)',
        weekday: 'monday',
        expectedDateStr: '2025-01-20', // Next Monday (5 days from today)
        shouldMatchToday: true,
      },
    ];

    scenarios.forEach(({ name, weekday, expectedDateStr, shouldMatchToday }) => {
      it(name, () => {
        testScheduler.run(({ hot, expectObservable }) => {
          const startTimeStr = '10:00';
          const fallbackDueDay = '2025-01-20'; // Fallback date

          const repeatCfg: TaskRepeatCfgCopy = {
            ...baseRepeatCfg,
            startDate: '2025-01-15', // Today (Wednesday)
            repeatCycle: 'WEEKLY',
            repeatEvery: 1,
            monday: weekday === 'monday',
            tuesday: weekday === 'tuesday',
            wednesday: weekday === 'wednesday',
            thursday: weekday === 'thursday',
            friday: weekday === 'friday',
            saturday: weekday === 'saturday',
            sunday: weekday === 'sunday',
          };

          const action = addTaskRepeatCfgToTask({
            taskRepeatCfg: repeatCfg,
            taskId: 'test-task-id',
            startTime: startTimeStr,
            remindAt: TaskReminderOptionId.AtStart,
          });

          actions$ = hot('-a', { a: action });

          const taskWithFallback: Task = {
            ...baseTask,
            dueDay: fallbackDueDay,
          };

          taskService.getByIdOnce$.and.returnValue(of(taskWithFallback));

          // Calculate expected date
          const targetDateStr = shouldMatchToday ? expectedDateStr : fallbackDueDay;
          const expectedDateTime = getDateTimeFromClockString(
            startTimeStr,
            dateStrToUtcDate(targetDateStr!).getTime(),
          );

          const expectedAction = TaskSharedActions.scheduleTaskWithTime({
            task: taskWithFallback,
            dueWithTime: expectedDateTime,
            remindAt: remindOptionToMilliseconds(
              expectedDateTime,
              TaskReminderOptionId.AtStart,
            ),
            isMoveToBacklog: false,
            isSkipAutoRemoveFromToday: isToday(expectedDateTime),
          });

          expectObservable(effects.addRepeatCfgToTaskUpdateTask$).toBe('-a', {
            a: expectedAction,
          });
        });
      });
    });
  });

  describe('Scenario: Monthly repeat patterns', () => {
    it('should schedule for today when MONTHLY startDate equals today', () => {
      // Today is Jan 15, startDate is also Jan 15
      testScheduler.run(({ hot, expectObservable }) => {
        const startTimeStr = '09:00';

        const repeatCfg: TaskRepeatCfgCopy = {
          ...baseRepeatCfg,
          startDate: '2025-01-15', // today
          repeatCycle: 'MONTHLY',
          repeatEvery: 1,
        };

        const action = addTaskRepeatCfgToTask({
          taskRepeatCfg: repeatCfg,
          taskId: 'test-task-id',
          startTime: startTimeStr,
          remindAt: TaskReminderOptionId.AtStart,
        });

        actions$ = hot('-a', { a: action });
        taskService.getByIdOnce$.and.returnValue(of(baseTask));

        // Should calculate Jan 15, 2025 as the target date
        const expectedDateTime = getDateTimeFromClockString(
          startTimeStr,
          dateStrToUtcDate('2025-01-15').getTime(),
        );

        const expectedAction = TaskSharedActions.scheduleTaskWithTime({
          task: baseTask,
          dueWithTime: expectedDateTime,
          remindAt: remindOptionToMilliseconds(
            expectedDateTime,
            TaskReminderOptionId.AtStart,
          ),
          isMoveToBacklog: false,
          isSkipAutoRemoveFromToday: isToday(expectedDateTime),
        });

        expectObservable(effects.addRepeatCfgToTaskUpdateTask$).toBe('-a', {
          a: expectedAction,
        });
      });
    });

    it('should schedule for future date when MONTHLY startDate is in the future', () => {
      // Today is Jan 15, startDate is Jan 20 (5 days out)
      testScheduler.run(({ hot, expectObservable }) => {
        const startTimeStr = '09:00';
        const expectedDateStr = '2025-01-20';

        const repeatCfg: TaskRepeatCfgCopy = {
          ...baseRepeatCfg,
          startDate: '2025-01-20',
          repeatCycle: 'MONTHLY',
          repeatEvery: 1,
        };

        const action = addTaskRepeatCfgToTask({
          taskRepeatCfg: repeatCfg,
          taskId: 'test-task-id',
          startTime: startTimeStr,
          remindAt: TaskReminderOptionId.AtStart,
        });

        actions$ = hot('-a', { a: action });

        taskService.getByIdOnce$.and.returnValue(of(baseTask));

        // getFirstRepeatOccurrence returns Jan 20, 2025 (the next valid occurrence)
        const expectedDateTime = getDateTimeFromClockString(
          startTimeStr,
          dateStrToUtcDate(expectedDateStr).getTime(),
        );

        const expectedAction = TaskSharedActions.scheduleTaskWithTime({
          task: baseTask,
          dueWithTime: expectedDateTime,
          remindAt: remindOptionToMilliseconds(
            expectedDateTime,
            TaskReminderOptionId.AtStart,
          ),
          isMoveToBacklog: false,
          isSkipAutoRemoveFromToday: isToday(expectedDateTime),
        });

        expectObservable(effects.addRepeatCfgToTaskUpdateTask$).toBe('-a', {
          a: expectedAction,
        });
      });
    });
  });

  describe('Scenario: Daily patterns', () => {
    it('should schedule for startDate when DAILY startDate equals today', () => {
      testScheduler.run(({ hot, expectObservable }) => {
        const startTimeStr = '08:00';

        const repeatCfg: TaskRepeatCfgCopy = {
          ...baseRepeatCfg,
          startDate: '2025-01-15', // today
          repeatCycle: 'DAILY',
          repeatEvery: 1,
        };

        const action = addTaskRepeatCfgToTask({
          taskRepeatCfg: repeatCfg,
          taskId: 'test-task-id',
          startTime: startTimeStr,
          remindAt: TaskReminderOptionId.AtStart,
        });

        actions$ = hot('-a', { a: action });
        taskService.getByIdOnce$.and.returnValue(of(baseTask));

        // DAILY always matches today
        const expectedDateTime = getDateTimeFromClockString(
          startTimeStr,
          dateStrToUtcDate('2025-01-15').getTime(),
        );

        const expectedAction = TaskSharedActions.scheduleTaskWithTime({
          task: baseTask,
          dueWithTime: expectedDateTime,
          remindAt: remindOptionToMilliseconds(
            expectedDateTime,
            TaskReminderOptionId.AtStart,
          ),
          isMoveToBacklog: false,
          isSkipAutoRemoveFromToday: isToday(expectedDateTime),
        });

        expectObservable(effects.addRepeatCfgToTaskUpdateTask$).toBe('-a', {
          a: expectedAction,
        });
      });
    });

    it('should schedule for startDate when DAILY repeatEvery > 1', () => {
      testScheduler.run(({ hot, expectObservable }) => {
        const startTimeStr = '08:00';

        // First occurrence is startDate itself, regardless of repeatEvery.
        const repeatCfg: TaskRepeatCfgCopy = {
          ...baseRepeatCfg,
          startDate: '2025-01-15',
          repeatCycle: 'DAILY',
          repeatEvery: 2,
        };

        const action = addTaskRepeatCfgToTask({
          taskRepeatCfg: repeatCfg,
          taskId: 'test-task-id',
          startTime: startTimeStr,
          remindAt: TaskReminderOptionId.AtStart,
        });

        actions$ = hot('-a', { a: action });
        taskService.getByIdOnce$.and.returnValue(of(baseTask));

        const expectedDateTime = getDateTimeFromClockString(
          startTimeStr,
          dateStrToUtcDate('2025-01-15').getTime(),
        );

        const expectedAction = TaskSharedActions.scheduleTaskWithTime({
          task: baseTask,
          dueWithTime: expectedDateTime,
          remindAt: remindOptionToMilliseconds(
            expectedDateTime,
            TaskReminderOptionId.AtStart,
          ),
          isMoveToBacklog: false,
          isSkipAutoRemoveFromToday: isToday(expectedDateTime),
        });

        expectObservable(effects.addRepeatCfgToTaskUpdateTask$).toBe('-a', {
          a: expectedAction,
        });
      });
    });
  });

  describe('Scenario: Edge cases and error handling', () => {
    it('should schedule for future startDate correctly (not fallback)', () => {
      testScheduler.run(({ hot, expectObservable }) => {
        const startTimeStr = '10:00';
        const futureStartDate = '2025-02-01';

        // startDate is in the future - getFirstRepeatOccurrence handles this correctly
        const repeatCfg: TaskRepeatCfgCopy = {
          ...baseRepeatCfg,
          startDate: futureStartDate, // Future date
          repeatCycle: 'DAILY',
          repeatEvery: 1,
        };

        const action = addTaskRepeatCfgToTask({
          taskRepeatCfg: repeatCfg,
          taskId: 'test-task-id',
          startTime: startTimeStr,
          remindAt: TaskReminderOptionId.AtStart,
        });

        actions$ = hot('-a', { a: action });

        taskService.getByIdOnce$.and.returnValue(of(baseTask));

        // Future startDate is handled by getFirstRepeatOccurrence - returns the startDate
        const expectedDateTime = getDateTimeFromClockString(
          startTimeStr,
          dateStrToUtcDate(futureStartDate).getTime(),
        );

        const expectedAction = TaskSharedActions.scheduleTaskWithTime({
          task: baseTask,
          dueWithTime: expectedDateTime,
          remindAt: remindOptionToMilliseconds(
            expectedDateTime,
            TaskReminderOptionId.AtStart,
          ),
          isMoveToBacklog: false,
          isSkipAutoRemoveFromToday: isToday(expectedDateTime),
        });

        expectObservable(effects.addRepeatCfgToTaskUpdateTask$).toBe('-a', {
          a: expectedAction,
        });
      });
    });
  });

  /**
   * Critical tests for isSkipAutoRemoveFromToday behavior (#5594 fix)
   * These tests explicitly verify that:
   * - Tasks scheduled for TODAY have isSkipAutoRemoveFromToday = true (stay in today list)
   * - Tasks scheduled for FUTURE have isSkipAutoRemoveFromToday = false (removed from today list)
   */
  describe('Scenario: isSkipAutoRemoveFromToday behavior', () => {
    it('should set isSkipAutoRemoveFromToday=TRUE when task is scheduled for TODAY', () => {
      // Today is Jan 15, 2025 (Wednesday) - DAILY pattern means scheduled for today
      testScheduler.run(({ hot, expectObservable }) => {
        const startTimeStr = '09:00';

        const repeatCfg: TaskRepeatCfgCopy = {
          ...baseRepeatCfg,
          startDate: '2025-01-15', // Today
          repeatCycle: 'DAILY',
          repeatEvery: 1,
        };

        const action = addTaskRepeatCfgToTask({
          taskRepeatCfg: repeatCfg,
          taskId: 'test-task-id',
          startTime: startTimeStr,
          remindAt: TaskReminderOptionId.AtStart,
        });

        actions$ = hot('-a', { a: action });
        taskService.getByIdOnce$.and.returnValue(of(baseTask));

        // Scheduled for today (Jan 15) - should stay in today list
        const expectedDateTime = getDateTimeFromClockString(
          startTimeStr,
          dateStrToUtcDate('2025-01-15').getTime(),
        );

        const expectedAction = TaskSharedActions.scheduleTaskWithTime({
          task: baseTask,
          dueWithTime: expectedDateTime,
          remindAt: remindOptionToMilliseconds(
            expectedDateTime,
            TaskReminderOptionId.AtStart,
          ),
          isMoveToBacklog: false,
          isSkipAutoRemoveFromToday: true, // MUST be true for today
        });

        expectObservable(effects.addRepeatCfgToTaskUpdateTask$).toBe('-a', {
          a: expectedAction,
        });
      });
    });

    it('should set isSkipAutoRemoveFromToday=FALSE when task is scheduled for FUTURE (WEEKLY)', () => {
      // Today is Jan 15, 2025 (Wednesday) - WEEKLY on Friday means scheduled for Jan 17
      testScheduler.run(({ hot, expectObservable }) => {
        const startTimeStr = '09:00';

        const repeatCfg: TaskRepeatCfgCopy = {
          ...baseRepeatCfg,
          startDate: '2025-01-15',
          repeatCycle: 'WEEKLY',
          repeatEvery: 1,
          monday: false,
          tuesday: false,
          wednesday: false,
          thursday: false,
          friday: true, // Friday is 2 days from Wednesday
          saturday: false,
          sunday: false,
        };

        const action = addTaskRepeatCfgToTask({
          taskRepeatCfg: repeatCfg,
          taskId: 'test-task-id',
          startTime: startTimeStr,
          remindAt: TaskReminderOptionId.AtStart,
        });

        actions$ = hot('-a', { a: action });
        taskService.getByIdOnce$.and.returnValue(of(baseTask));

        // Scheduled for Jan 17 (Friday) - should be removed from today list
        const expectedDateTime = getDateTimeFromClockString(
          startTimeStr,
          dateStrToUtcDate('2025-01-17').getTime(),
        );

        const expectedAction = TaskSharedActions.scheduleTaskWithTime({
          task: baseTask,
          dueWithTime: expectedDateTime,
          remindAt: remindOptionToMilliseconds(
            expectedDateTime,
            TaskReminderOptionId.AtStart,
          ),
          isMoveToBacklog: false,
          isSkipAutoRemoveFromToday: false, // MUST be false for future dates
        });

        expectObservable(effects.addRepeatCfgToTaskUpdateTask$).toBe('-a', {
          a: expectedAction,
        });
      });
    });

    it('should set isSkipAutoRemoveFromToday=FALSE when task is scheduled for FUTURE (MONTHLY)', () => {
      // Today is Jan 15, 2025 - startDate Jan 20 means scheduled for Jan 20
      testScheduler.run(({ hot, expectObservable }) => {
        const startTimeStr = '09:00';

        const repeatCfg: TaskRepeatCfgCopy = {
          ...baseRepeatCfg,
          startDate: '2025-01-20', // 5 days out
          repeatCycle: 'MONTHLY',
          repeatEvery: 1,
        };

        const action = addTaskRepeatCfgToTask({
          taskRepeatCfg: repeatCfg,
          taskId: 'test-task-id',
          startTime: startTimeStr,
          remindAt: TaskReminderOptionId.AtStart,
        });

        actions$ = hot('-a', { a: action });
        taskService.getByIdOnce$.and.returnValue(of(baseTask));

        // Scheduled for Jan 20 - should be removed from today list
        const expectedDateTime = getDateTimeFromClockString(
          startTimeStr,
          dateStrToUtcDate('2025-01-20').getTime(),
        );

        const expectedAction = TaskSharedActions.scheduleTaskWithTime({
          task: baseTask,
          dueWithTime: expectedDateTime,
          remindAt: remindOptionToMilliseconds(
            expectedDateTime,
            TaskReminderOptionId.AtStart,
          ),
          isMoveToBacklog: false,
          isSkipAutoRemoveFromToday: false, // MUST be false for future dates
        });

        expectObservable(effects.addRepeatCfgToTaskUpdateTask$).toBe('-a', {
          a: expectedAction,
        });
      });
    });

    it('should set isSkipAutoRemoveFromToday=FALSE when startDate is in the FUTURE', () => {
      // Today is Jan 15, 2025 - startDate Feb 1 means scheduled for Feb 1
      testScheduler.run(({ hot, expectObservable }) => {
        const startTimeStr = '09:00';

        const repeatCfg: TaskRepeatCfgCopy = {
          ...baseRepeatCfg,
          startDate: '2025-02-01', // Future start date
          repeatCycle: 'DAILY',
          repeatEvery: 1,
        };

        const action = addTaskRepeatCfgToTask({
          taskRepeatCfg: repeatCfg,
          taskId: 'test-task-id',
          startTime: startTimeStr,
          remindAt: TaskReminderOptionId.AtStart,
        });

        actions$ = hot('-a', { a: action });
        taskService.getByIdOnce$.and.returnValue(of(baseTask));

        // Scheduled for Feb 1 (future start date) - should be removed from today list
        const expectedDateTime = getDateTimeFromClockString(
          startTimeStr,
          dateStrToUtcDate('2025-02-01').getTime(),
        );

        const expectedAction = TaskSharedActions.scheduleTaskWithTime({
          task: baseTask,
          dueWithTime: expectedDateTime,
          remindAt: remindOptionToMilliseconds(
            expectedDateTime,
            TaskReminderOptionId.AtStart,
          ),
          isMoveToBacklog: false,
          isSkipAutoRemoveFromToday: false, // MUST be false for future start dates
        });

        expectObservable(effects.addRepeatCfgToTaskUpdateTask$).toBe('-a', {
          a: expectedAction,
        });
      });
    });

    it('should set isSkipAutoRemoveFromToday=TRUE when WEEKLY matches TODAY', () => {
      // Today is Jan 15, 2025 (Wednesday) - WEEKLY on Wednesday means today
      testScheduler.run(({ hot, expectObservable }) => {
        const startTimeStr = '09:00';

        const repeatCfg: TaskRepeatCfgCopy = {
          ...baseRepeatCfg,
          startDate: '2025-01-15',
          repeatCycle: 'WEEKLY',
          repeatEvery: 1,
          monday: false,
          tuesday: false,
          wednesday: true, // Wednesday is today
          thursday: false,
          friday: false,
          saturday: false,
          sunday: false,
        };

        const action = addTaskRepeatCfgToTask({
          taskRepeatCfg: repeatCfg,
          taskId: 'test-task-id',
          startTime: startTimeStr,
          remindAt: TaskReminderOptionId.AtStart,
        });

        actions$ = hot('-a', { a: action });
        taskService.getByIdOnce$.and.returnValue(of(baseTask));

        // Scheduled for today (Wednesday) - should stay in today list
        const expectedDateTime = getDateTimeFromClockString(
          startTimeStr,
          dateStrToUtcDate('2025-01-15').getTime(),
        );

        const expectedAction = TaskSharedActions.scheduleTaskWithTime({
          task: baseTask,
          dueWithTime: expectedDateTime,
          remindAt: remindOptionToMilliseconds(
            expectedDateTime,
            TaskReminderOptionId.AtStart,
          ),
          isMoveToBacklog: false,
          isSkipAutoRemoveFromToday: true, // MUST be true for today
        });

        expectObservable(effects.addRepeatCfgToTaskUpdateTask$).toBe('-a', {
          a: expectedAction,
        });
      });
    });
  });

  /**
   * Deterministic tests for updateTaskAfterMakingItRepeatable$ effect.
   * Today is fixed to Wednesday, January 15, 2025.
   * NOTE: jasmine.clock() is installed, so setTimeout won't fire without tick().
   * Use synchronous subscribe patterns instead of done callbacks.
   */
  describe('Scenario: updateTaskAfterMakingItRepeatable$ deterministic', () => {
    const baseTaskWithSubTasks: TaskWithSubTasks = {
      ...baseTask,
      subTasks: [],
      dueDay: '2025-01-15',
      created: FIXED_WEDNESDAY.getTime(),
    };

    it('should dispatch planTaskForDay for WEEKLY on Friday with precise created timestamp', () => {
      const weeklyFridayCfg: TaskRepeatCfgCopy = {
        ...baseRepeatCfg,
        repeatCycle: 'WEEKLY',
        repeatEvery: 1,
        startDate: '2025-01-15',
        monday: false,
        tuesday: false,
        wednesday: false,
        thursday: false,
        friday: true,
        saturday: false,
        sunday: false,
      };

      const action = addTaskRepeatCfgToTask({
        taskRepeatCfg: weeklyFridayCfg,
        taskId: 'test-task-id',
      });

      actions$ = of(action);
      taskService.getByIdWithSubTaskData$.and.returnValue(of(baseTaskWithSubTasks));

      spyOn(effects as any, '_updateRegularTaskInstance');

      // Friday Jan 17, 2025 at noon
      const expectedCreated = new Date(2025, 0, 17, 12, 0, 0, 0).getTime();

      let result: any;
      effects.updateTaskAfterMakingItRepeatable$.subscribe((r) => {
        result = r;
      });

      // eslint-disable-next-line @typescript-eslint/no-unused-vars
      const { subTasks: _ignored, ...expectedTask } = baseTaskWithSubTasks;
      expect(result).toEqual(
        PlannerActions.planTaskForDay({
          task: expectedTask as any,
          day: '2025-01-17',
        }),
      );

      // Verify created is set to noon on Friday Jan 17
      expect(taskService.update).toHaveBeenCalledWith('test-task-id', {
        created: expectedCreated,
        dueDay: '2025-01-17',
      });

      // Verify lastTaskCreationDay is the future date
      expect(taskRepeatCfgService.updateTaskRepeatCfg).toHaveBeenCalledWith(
        'repeat-cfg-id',
        jasmine.objectContaining({
          lastTaskCreationDay: '2025-01-17',
          lastTaskCreation: expectedCreated,
        }),
      );

      expect((effects as any)._updateRegularTaskInstance).toHaveBeenCalled();
    });

    it('should dispatch planTaskForDay for WEEKLY on Monday (5 days future)', () => {
      const weeklyMondayCfg: TaskRepeatCfgCopy = {
        ...baseRepeatCfg,
        repeatCycle: 'WEEKLY',
        repeatEvery: 1,
        startDate: '2025-01-15',
        monday: true,
        tuesday: false,
        wednesday: false,
        thursday: false,
        friday: false,
        saturday: false,
        sunday: false,
      };

      const action = addTaskRepeatCfgToTask({
        taskRepeatCfg: weeklyMondayCfg,
        taskId: 'test-task-id',
      });

      actions$ = of(action);
      taskService.getByIdWithSubTaskData$.and.returnValue(of(baseTaskWithSubTasks));

      spyOn(effects as any, '_updateRegularTaskInstance');

      let result: any;
      effects.updateTaskAfterMakingItRepeatable$.subscribe((r) => {
        result = r;
      });

      // eslint-disable-next-line @typescript-eslint/no-unused-vars
      const { subTasks: _ignored, ...expectedTask } = baseTaskWithSubTasks;
      expect(result).toEqual(
        PlannerActions.planTaskForDay({
          task: expectedTask as any,
          day: '2025-01-20',
        }),
      );

      expect(taskService.update).toHaveBeenCalledWith('test-task-id', {
        created: new Date(2025, 0, 20, 12, 0, 0, 0).getTime(),
        dueDay: '2025-01-20',
      });
    });

    it('should NOT dispatch planTaskForDay for WEEKLY on Wednesday (today)', () => {
      const weeklyWednesdayCfg: TaskRepeatCfgCopy = {
        ...baseRepeatCfg,
        repeatCycle: 'WEEKLY',
        repeatEvery: 1,
        startDate: '2025-01-15',
        monday: false,
        tuesday: false,
        wednesday: true,
        thursday: false,
        friday: false,
        saturday: false,
        sunday: false,
      };

      const action = addTaskRepeatCfgToTask({
        taskRepeatCfg: weeklyWednesdayCfg,
        taskId: 'test-task-id',
      });

      actions$ = of(action);
      taskService.getByIdWithSubTaskData$.and.returnValue(of(baseTaskWithSubTasks));

      spyOn(effects as any, '_updateRegularTaskInstance');

      let emitted = false;
      effects.updateTaskAfterMakingItRepeatable$.subscribe(() => {
        emitted = true;
      });

      expect(emitted).toBe(false);
      // Today branch: dueDay already matches, no taskService.update call
      expect(taskService.update).not.toHaveBeenCalled();
      expect(taskRepeatCfgService.updateTaskRepeatCfg).toHaveBeenCalledWith(
        'repeat-cfg-id',
        jasmine.objectContaining({
          lastTaskCreationDay: '2025-01-15',
        }),
      );
      expect((effects as any)._updateRegularTaskInstance).toHaveBeenCalled();
    });

    it('should dispatch planTaskForDay for MONTHLY when startDate is in the future', () => {
      // Today is Wednesday Jan 15, 2025; startDate is Jan 20 (5 days out).
      const monthlyRepeatCfg: TaskRepeatCfgCopy = {
        ...baseRepeatCfg,
        repeatCycle: 'MONTHLY',
        repeatEvery: 1,
        startDate: '2025-01-20',
      };

      const action = addTaskRepeatCfgToTask({
        taskRepeatCfg: monthlyRepeatCfg,
        taskId: 'test-task-id',
      });

      actions$ = of(action);
      taskService.getByIdWithSubTaskData$.and.returnValue(of(baseTaskWithSubTasks));

      spyOn(effects as any, '_updateRegularTaskInstance');

      let result: any;
      effects.updateTaskAfterMakingItRepeatable$.subscribe((r) => {
        result = r;
      });

      // eslint-disable-next-line @typescript-eslint/no-unused-vars
      const { subTasks: _ignored, ...expectedTask } = baseTaskWithSubTasks;
      expect(result).toEqual(
        PlannerActions.planTaskForDay({
          task: expectedTask as any,
          day: '2025-01-20',
        }),
      );

      expect(taskService.update).toHaveBeenCalledWith('test-task-id', {
        created: new Date(2025, 0, 20, 12, 0, 0, 0).getTime(),
        dueDay: '2025-01-20',
      });
    });

    it('should NOT dispatch planTaskForDay for MONTHLY when startDate equals today', () => {
      const monthlyRepeatCfg: TaskRepeatCfgCopy = {
        ...baseRepeatCfg,
        repeatCycle: 'MONTHLY',
        repeatEvery: 1,
        startDate: '2025-01-15', // today
      };

      const action = addTaskRepeatCfgToTask({
        taskRepeatCfg: monthlyRepeatCfg,
        taskId: 'test-task-id',
      });

      actions$ = of(action);
      taskService.getByIdWithSubTaskData$.and.returnValue(of(baseTaskWithSubTasks));

      spyOn(effects as any, '_updateRegularTaskInstance');

      let emitted = false;
      effects.updateTaskAfterMakingItRepeatable$.subscribe(() => {
        emitted = true;
      });

      expect(emitted).toBe(false);
      expect(taskService.update).not.toHaveBeenCalled();
    });

    it('should dispatch planTaskForDay for YEARLY when startDate is in the future', () => {
      // Today is Jan 15, 2025; startDate is Feb 15, 2025 (~1 month out).
      const yearlyRepeatCfg: TaskRepeatCfgCopy = {
        ...baseRepeatCfg,
        repeatCycle: 'YEARLY',
        repeatEvery: 1,
        startDate: '2025-02-15',
      };

      const action = addTaskRepeatCfgToTask({
        taskRepeatCfg: yearlyRepeatCfg,
        taskId: 'test-task-id',
      });

      actions$ = of(action);
      taskService.getByIdWithSubTaskData$.and.returnValue(of(baseTaskWithSubTasks));

      spyOn(effects as any, '_updateRegularTaskInstance');

      let result: any;
      effects.updateTaskAfterMakingItRepeatable$.subscribe((r) => {
        result = r;
      });

      // eslint-disable-next-line @typescript-eslint/no-unused-vars
      const { subTasks: _ignored, ...expectedTask } = baseTaskWithSubTasks;
      expect(result).toEqual(
        PlannerActions.planTaskForDay({
          task: expectedTask as any,
          day: '2025-02-15',
        }),
      );

      expect(taskService.update).toHaveBeenCalledWith('test-task-id', {
        created: new Date(2025, 1, 15, 12, 0, 0, 0).getTime(),
        dueDay: '2025-02-15',
      });
    });

    it('should NOT dispatch planTaskForDay for timed task with future occurrence', () => {
      const weeklyFridayCfg: TaskRepeatCfgCopy = {
        ...baseRepeatCfg,
        repeatCycle: 'WEEKLY',
        repeatEvery: 1,
        startDate: '2025-01-15',
        monday: false,
        tuesday: false,
        wednesday: false,
        thursday: false,
        friday: true,
        saturday: false,
        sunday: false,
      };

      const action = addTaskRepeatCfgToTask({
        taskRepeatCfg: weeklyFridayCfg,
        taskId: 'test-task-id',
        startTime: '10:00',
        remindAt: TaskReminderOptionId.AtStart,
      });

      actions$ = of(action);
      taskService.getByIdWithSubTaskData$.and.returnValue(of(baseTaskWithSubTasks));

      spyOn(effects as any, '_updateRegularTaskInstance');

      let emitted = false;
      effects.updateTaskAfterMakingItRepeatable$.subscribe(() => {
        emitted = true;
      });

      expect(emitted).toBe(false);
      // Should still update created for duplicate prevention
      expect(taskService.update).toHaveBeenCalledWith('test-task-id', {
        created: new Date(2025, 0, 17, 12, 0, 0, 0).getTime(),
        dueDay: '2025-01-17',
      });
      expect(taskRepeatCfgService.updateTaskRepeatCfg).toHaveBeenCalledWith(
        'repeat-cfg-id',
        jasmine.objectContaining({
          lastTaskCreationDay: '2025-01-17',
        }),
      );
      expect((effects as any)._updateRegularTaskInstance).toHaveBeenCalled();
    });

    it('should dispatch planTaskForDay for DAILY with future startDate', () => {
      const futureStartCfg: TaskRepeatCfgCopy = {
        ...baseRepeatCfg,
        repeatCycle: 'DAILY',
        repeatEvery: 1,
        startDate: '2025-02-01', // Future start date
      };

      const action = addTaskRepeatCfgToTask({
        taskRepeatCfg: futureStartCfg,
        taskId: 'test-task-id',
      });

      actions$ = of(action);
      taskService.getByIdWithSubTaskData$.and.returnValue(of(baseTaskWithSubTasks));

      spyOn(effects as any, '_updateRegularTaskInstance');

      let result: any;
      effects.updateTaskAfterMakingItRepeatable$.subscribe((r) => {
        result = r;
      });

      // eslint-disable-next-line @typescript-eslint/no-unused-vars
      const { subTasks: _ignored, ...expectedTask } = baseTaskWithSubTasks;
      expect(result).toEqual(
        PlannerActions.planTaskForDay({
          task: expectedTask as any,
          day: '2025-02-01',
        }),
      );

      expect(taskService.update).toHaveBeenCalledWith('test-task-id', {
        created: new Date(2025, 1, 1, 12, 0, 0, 0).getTime(),
        dueDay: '2025-02-01',
      });
    });

    it('should NOT double-schedule timed task via _updateRegularTaskInstance', () => {
      // Regression test: _updateRegularTaskInstance must NOT dispatch a second
      // scheduleTaskWithTime for timed tasks. Effect 1 already handles scheduling
      // with the correct future date. Without the guard, _updateRegularTaskInstance
      // would use new Date() (today), overwriting the correct future date.
      const weeklyFridayCfg: TaskRepeatCfgCopy = {
        ...baseRepeatCfg,
        repeatCycle: 'WEEKLY',
        repeatEvery: 1,
        startDate: '2025-01-15',
        startTime: '10:00',
        remindAt: TaskReminderOptionId.AtStart,
        monday: false,
        tuesday: false,
        wednesday: false,
        thursday: false,
        friday: true,
        saturday: false,
        sunday: false,
      };

      const action = addTaskRepeatCfgToTask({
        taskRepeatCfg: weeklyFridayCfg,
        taskId: 'test-task-id',
        startTime: '10:00',
        remindAt: TaskReminderOptionId.AtStart,
      });

      actions$ = of(action);
      taskService.getByIdWithSubTaskData$.and.returnValue(of(baseTaskWithSubTasks));

      // Do NOT spy on _updateRegularTaskInstance — let it actually run
      let emitted = false;
      effects.updateTaskAfterMakingItRepeatable$.subscribe(() => {
        emitted = true;
      });

      expect(emitted).toBe(false); // isTimedTask filter blocks planTaskForDay
      // Critical: scheduleTask and reScheduleTask must NOT be called
      expect(taskService.scheduleTask).not.toHaveBeenCalled();
      expect(taskService.reScheduleTask).not.toHaveBeenCalled();
    });
  });
});
