/* eslint-disable @typescript-eslint/naming-convention */
import { HideSubTasksMode, Task, TaskDetailTargetPanel, TaskState } from '../task.model';
import { initialTaskState, taskReducer } from './task.reducer';
import { convertOpToAction } from '../../../op-log/apply/operation-converter.util';
import * as fromActions from './task.actions';
import { TaskSharedActions } from '../../../root-store/meta/task-shared.actions';
import { INBOX_PROJECT } from '../../project/project.const';
import {
  TimeTrackingActions,
  syncTimeSpent,
} from '../../time-tracking/store/time-tracking.actions';
import { _resetDevErrorState } from '../../../util/dev-error';
import { PlannerActions } from '../../planner/store/planner.actions';
import { loadAllData } from '../../../root-store/meta/load-all-data.action';
import { ActionType, OpType, Operation } from '../../../op-log/core/operation.types';

describe('Task Reducer', () => {
  const createTask = (id: string, partial: Partial<Task> = {}): Task => ({
    id,
    title: `Task ${id}`,
    created: Date.now(),
    isDone: false,
    subTaskIds: [],
    tagIds: [],
    projectId: INBOX_PROJECT.id,
    parentId: undefined,
    timeSpentOnDay: {},
    timeEstimate: 0,
    timeSpent: 0,
    dueDay: undefined,
    dueWithTime: undefined,
    attachments: [],
    ...partial,
  });

  // Create some test tasks
  const task1 = createTask('task1');
  const task2 = createTask('task2');
  const subTask1 = createTask('subTask1', { parentId: 'task1' });
  const subTask2 = createTask('subTask2', { parentId: 'task1' });

  // Create initial state with some tasks
  const stateWithTasks: TaskState = {
    ...initialTaskState,
    ids: ['task1', 'task2', 'subTask1', 'subTask2'],
    entities: {
      task1: { ...task1, subTaskIds: ['subTask1', 'subTask2'] },
      task2,
      subTask1,
      subTask2,
    },
    currentTaskId: 'task1',
  };

  const stubWindowConfirm = (returnValue: boolean): void => {
    if (jasmine.isSpy(window.confirm)) {
      (window.confirm as jasmine.Spy).and.returnValue(returnValue);
    } else {
      spyOn(window, 'confirm').and.returnValue(returnValue);
    }
  };

  describe('unknown action', () => {
    it('should return the default state', () => {
      const action = { type: 'UNKNOWN' };
      const state = taskReducer(initialTaskState, action);

      expect(state).toBe(initialTaskState);
    });
  });

  describe('Task operations still handled by task reducer', () => {
    it('should handle unknown actions by returning current state', () => {
      const unknownAction = { type: 'UNKNOWN_ACTION' } as any;
      const state = taskReducer(stateWithTasks, unknownAction);

      expect(state).toBe(stateWithTasks);
    });
  });

  describe('Note: CRUD operations moved to meta-reducer', () => {
    it('should note that addTask is now handled by TaskSharedActions in meta-reducer', () => {
      // This test documents that addTask has been moved to the meta-reducer
      // and is no longer handled directly by the task reducer.
      // See task-shared.reducer.spec.ts for comprehensive addTask tests.
      expect(TaskSharedActions.addTask).toBeDefined();
    });

    it('should note that deleteTask is now handled by TaskSharedActions in meta-reducer', () => {
      // This test documents that deleteTask has been moved to the meta-reducer
      // and is no longer handled directly by the task reducer.
      // See task-shared.reducer.spec.ts for comprehensive deleteTask tests.
      expect(TaskSharedActions.deleteTask).toBeDefined();
    });
  });

  describe('setSelectedTask', () => {
    it('should keep the selected task open when an explicit target panel is requested', () => {
      const state: TaskState = {
        ...stateWithTasks,
        selectedTaskId: 'task1',
        taskDetailTargetPanel: null,
      };

      const result = taskReducer(
        state,
        fromActions.setSelectedTask({
          id: 'task1',
          taskDetailTargetPanel: TaskDetailTargetPanel.Notes,
        }),
      );

      expect(result.selectedTaskId).toBe('task1');
      expect(result.taskDetailTargetPanel).toBe(TaskDetailTargetPanel.Notes);
    });

    it('should still toggle the selected task closed for the default target', () => {
      const state: TaskState = {
        ...stateWithTasks,
        selectedTaskId: 'task1',
        taskDetailTargetPanel: TaskDetailTargetPanel.Notes,
      };

      const result = taskReducer(
        state,
        fromActions.setSelectedTask({
          id: 'task1',
          taskDetailTargetPanel: TaskDetailTargetPanel.Default,
        }),
      );

      expect(result.selectedTaskId).toBeNull();
      expect(result.taskDetailTargetPanel).toBeNull();
    });
  });

  describe('Subtask operations', () => {
    it('should add a subtask to a parent task', () => {
      const newSubTask = createTask('subTask3');
      const action = fromActions.addSubTask({
        task: newSubTask,
        parentId: 'task1',
      });
      const state = taskReducer(stateWithTasks, action);

      expect(state.entities['task1']!.subTaskIds).toContain('subTask3');
      expect(state.entities['subTask3']).toEqual({ ...newSubTask, parentId: 'task1' });
    });

    it('should not duplicate parent subTaskIds when addSubTask is replayed', () => {
      const newSubTask = createTask('subTask3');
      const action = fromActions.addSubTask({
        task: newSubTask,
        parentId: 'task1',
      });

      const stateAfterFirstAdd = taskReducer(stateWithTasks, action);
      const stateAfterReplay = taskReducer(stateAfterFirstAdd, action);

      expect(stateAfterReplay.entities['task1']!.subTaskIds).toEqual([
        'subTask1',
        'subTask2',
        'subTask3',
      ]);
    });

    it('should roll up the parent time estimate when adding a subtask with an estimate', () => {
      const parent = createTask('parent');
      const subTask = createTask('subTask', { timeEstimate: 2.5 * 60 * 60 * 1000 });
      const state: TaskState = {
        ...initialTaskState,
        ids: ['parent'],
        entities: {
          parent,
        },
      };

      const result = taskReducer(
        state,
        fromActions.addSubTask({
          task: subTask,
          parentId: 'parent',
        }),
      );

      expect(result.entities['parent']!.subTaskIds).toEqual(['subTask']);
      expect(result.entities['parent']!.timeEstimate).toBe(2.5 * 60 * 60 * 1000);
    });
  });

  describe('moveSubTask (anchor-based)', () => {
    const createStateWithSubtasks = (): TaskState => ({
      ...initialTaskState,
      ids: ['parent1', 'parent2', 'sub1', 'sub2', 'sub3', 'sub4'],
      entities: {
        parent1: createTask('parent1', { subTaskIds: ['sub1', 'sub2', 'sub3'] }),
        parent2: createTask('parent2', { subTaskIds: ['sub4'] }),
        sub1: createTask('sub1', { parentId: 'parent1' }),
        sub2: createTask('sub2', { parentId: 'parent1' }),
        sub3: createTask('sub3', { parentId: 'parent1' }),
        sub4: createTask('sub4', { parentId: 'parent2' }),
      },
    });

    describe('reordering within same parent', () => {
      it('should move subtask to start when afterTaskId is null', () => {
        const state = createStateWithSubtasks();
        const action = fromActions.moveSubTask({
          taskId: 'sub3',
          srcTaskId: 'parent1',
          targetTaskId: 'parent1',
          afterTaskId: null,
        });

        const result = taskReducer(state, action);
        expect(result.entities['parent1']!.subTaskIds).toEqual(['sub3', 'sub1', 'sub2']);
      });

      it('should move subtask after specified anchor', () => {
        const state = createStateWithSubtasks();
        const action = fromActions.moveSubTask({
          taskId: 'sub3',
          srcTaskId: 'parent1',
          targetTaskId: 'parent1',
          afterTaskId: 'sub1',
        });

        const result = taskReducer(state, action);
        expect(result.entities['parent1']!.subTaskIds).toEqual(['sub1', 'sub3', 'sub2']);
      });

      it('should move subtask to end when anchor is last item', () => {
        const state = createStateWithSubtasks();
        const action = fromActions.moveSubTask({
          taskId: 'sub1',
          srcTaskId: 'parent1',
          targetTaskId: 'parent1',
          afterTaskId: 'sub3',
        });

        const result = taskReducer(state, action);
        expect(result.entities['parent1']!.subTaskIds).toEqual(['sub2', 'sub3', 'sub1']);
      });

      it('should handle moving item that is already at target position', () => {
        const state = createStateWithSubtasks();
        const action = fromActions.moveSubTask({
          taskId: 'sub2',
          srcTaskId: 'parent1',
          targetTaskId: 'parent1',
          afterTaskId: 'sub1',
        });

        const result = taskReducer(state, action);
        expect(result.entities['parent1']!.subTaskIds).toEqual(['sub1', 'sub2', 'sub3']);
      });
    });

    describe('moving between parents', () => {
      it('should move subtask to different parent at start', () => {
        const state = createStateWithSubtasks();
        const action = fromActions.moveSubTask({
          taskId: 'sub1',
          srcTaskId: 'parent1',
          targetTaskId: 'parent2',
          afterTaskId: null,
        });

        const result = taskReducer(state, action);
        expect(result.entities['parent1']!.subTaskIds).toEqual(['sub2', 'sub3']);
        expect(result.entities['parent2']!.subTaskIds).toEqual(['sub1', 'sub4']);
        expect(result.entities['sub1']!.parentId).toBe('parent2');
      });

      it('should move subtask to different parent after anchor', () => {
        const state = createStateWithSubtasks();
        const action = fromActions.moveSubTask({
          taskId: 'sub1',
          srcTaskId: 'parent1',
          targetTaskId: 'parent2',
          afterTaskId: 'sub4',
        });

        const result = taskReducer(state, action);
        expect(result.entities['parent1']!.subTaskIds).toEqual(['sub2', 'sub3']);
        expect(result.entities['parent2']!.subTaskIds).toEqual(['sub4', 'sub1']);
        expect(result.entities['sub1']!.parentId).toBe('parent2');
      });

      it('should update projectId when moving to parent with different project', () => {
        const state: TaskState = {
          ...initialTaskState,
          ids: ['parent1', 'parent2', 'sub1'],
          entities: {
            parent1: createTask('parent1', {
              subTaskIds: ['sub1'],
              projectId: 'project1',
            }),
            parent2: createTask('parent2', { subTaskIds: [], projectId: 'project2' }),
            sub1: createTask('sub1', { parentId: 'parent1', projectId: 'project1' }),
          },
        };

        const action = fromActions.moveSubTask({
          taskId: 'sub1',
          srcTaskId: 'parent1',
          targetTaskId: 'parent2',
          afterTaskId: null,
        });

        const result = taskReducer(state, action);
        expect(result.entities['sub1']!.projectId).toBe('project2');
      });
    });

    describe('edge cases', () => {
      it('should handle anchor not found by appending to end', () => {
        const state = createStateWithSubtasks();
        const action = fromActions.moveSubTask({
          taskId: 'sub1',
          srcTaskId: 'parent1',
          targetTaskId: 'parent1',
          afterTaskId: 'non-existent',
        });

        const result = taskReducer(state, action);
        expect(result.entities['parent1']!.subTaskIds).toEqual(['sub2', 'sub3', 'sub1']);
      });

      it('should handle moving to empty parent', () => {
        const state: TaskState = {
          ...initialTaskState,
          ids: ['parent1', 'parent2', 'sub1'],
          entities: {
            parent1: createTask('parent1', { subTaskIds: ['sub1'] }),
            parent2: createTask('parent2', { subTaskIds: [] }),
            sub1: createTask('sub1', { parentId: 'parent1' }),
          },
        };

        const action = fromActions.moveSubTask({
          taskId: 'sub1',
          srcTaskId: 'parent1',
          targetTaskId: 'parent2',
          afterTaskId: null,
        });

        const result = taskReducer(state, action);
        expect(result.entities['parent1']!.subTaskIds).toEqual([]);
        expect(result.entities['parent2']!.subTaskIds).toEqual(['sub1']);
      });
    });
  });

  describe('moveSubTaskUp', () => {
    it('should move subtask up one position', () => {
      const state = taskReducer(
        stateWithTasks,
        fromActions.moveSubTaskUp({
          id: 'subTask2',
          parentId: 'task1',
        }),
      );

      expect(state.entities['task1']!.subTaskIds).toEqual(['subTask2', 'subTask1']);
    });

    it('should not change order when subtask is already at top', () => {
      const state = taskReducer(
        stateWithTasks,
        fromActions.moveSubTaskUp({
          id: 'subTask1',
          parentId: 'task1',
        }),
      );

      expect(state.entities['task1']!.subTaskIds).toEqual(['subTask1', 'subTask2']);
    });

    it('should return unchanged state when parent not found', () => {
      const state = taskReducer(
        stateWithTasks,
        fromActions.moveSubTaskUp({
          id: 'subTask1',
          parentId: 'non-existent',
        }),
      );

      expect(state).toBe(stateWithTasks);
    });

    it('should return unchanged state when subtask not in parent', () => {
      const state = taskReducer(
        stateWithTasks,
        fromActions.moveSubTaskUp({
          id: 'subTask1',
          parentId: 'task2',
        }),
      );

      expect(state).toBe(stateWithTasks);
    });
  });

  describe('moveSubTaskDown', () => {
    it('should move subtask down one position', () => {
      const state = taskReducer(
        stateWithTasks,
        fromActions.moveSubTaskDown({
          id: 'subTask1',
          parentId: 'task1',
        }),
      );

      expect(state.entities['task1']!.subTaskIds).toEqual(['subTask2', 'subTask1']);
    });

    it('should not change order when subtask is already at bottom', () => {
      const state = taskReducer(
        stateWithTasks,
        fromActions.moveSubTaskDown({
          id: 'subTask2',
          parentId: 'task1',
        }),
      );

      expect(state.entities['task1']!.subTaskIds).toEqual(['subTask1', 'subTask2']);
    });

    it('should return unchanged state when parent not found', () => {
      const state = taskReducer(
        stateWithTasks,
        fromActions.moveSubTaskDown({
          id: 'subTask1',
          parentId: 'non-existent',
        }),
      );

      expect(state).toBe(stateWithTasks);
    });

    it('should return unchanged state when subtask not in parent', () => {
      const state = taskReducer(
        stateWithTasks,
        fromActions.moveSubTaskDown({
          id: 'subTask1',
          parentId: 'task2',
        }),
      );

      expect(state).toBe(stateWithTasks);
    });
  });

  describe('moveSubTaskToTop', () => {
    it('should move subtask to top of list', () => {
      const stateWithThreeSubtasks: TaskState = {
        ...initialTaskState,
        ids: ['task1', 'sub1', 'sub2', 'sub3'],
        entities: {
          task1: createTask('task1', { subTaskIds: ['sub1', 'sub2', 'sub3'] }),
          sub1: createTask('sub1', { parentId: 'task1' }),
          sub2: createTask('sub2', { parentId: 'task1' }),
          sub3: createTask('sub3', { parentId: 'task1' }),
        },
      };

      const state = taskReducer(
        stateWithThreeSubtasks,
        fromActions.moveSubTaskToTop({
          id: 'sub3',
          parentId: 'task1',
        }),
      );

      expect(state.entities['task1']!.subTaskIds).toEqual(['sub3', 'sub1', 'sub2']);
    });

    it('should not change order when subtask is already at top', () => {
      const state = taskReducer(
        stateWithTasks,
        fromActions.moveSubTaskToTop({
          id: 'subTask1',
          parentId: 'task1',
        }),
      );

      expect(state.entities['task1']!.subTaskIds).toEqual(['subTask1', 'subTask2']);
    });

    it('should return unchanged state when parent not found', () => {
      const state = taskReducer(
        stateWithTasks,
        fromActions.moveSubTaskToTop({
          id: 'subTask1',
          parentId: 'non-existent',
        }),
      );

      expect(state).toBe(stateWithTasks);
    });

    it('should return unchanged state when subtask not in parent', () => {
      const state = taskReducer(
        stateWithTasks,
        fromActions.moveSubTaskToTop({
          id: 'subTask1',
          parentId: 'task2',
        }),
      );

      expect(state).toBe(stateWithTasks);
    });
  });

  describe('moveSubTaskToBottom', () => {
    it('should move subtask to bottom of list', () => {
      const stateWithThreeSubtasks: TaskState = {
        ...initialTaskState,
        ids: ['task1', 'sub1', 'sub2', 'sub3'],
        entities: {
          task1: createTask('task1', { subTaskIds: ['sub1', 'sub2', 'sub3'] }),
          sub1: createTask('sub1', { parentId: 'task1' }),
          sub2: createTask('sub2', { parentId: 'task1' }),
          sub3: createTask('sub3', { parentId: 'task1' }),
        },
      };

      const state = taskReducer(
        stateWithThreeSubtasks,
        fromActions.moveSubTaskToBottom({
          id: 'sub1',
          parentId: 'task1',
        }),
      );

      expect(state.entities['task1']!.subTaskIds).toEqual(['sub2', 'sub3', 'sub1']);
    });

    it('should not change order when subtask is already at bottom', () => {
      const state = taskReducer(
        stateWithTasks,
        fromActions.moveSubTaskToBottom({
          id: 'subTask2',
          parentId: 'task1',
        }),
      );

      expect(state.entities['task1']!.subTaskIds).toEqual(['subTask1', 'subTask2']);
    });

    it('should return unchanged state when parent not found', () => {
      const state = taskReducer(
        stateWithTasks,
        fromActions.moveSubTaskToBottom({
          id: 'subTask1',
          parentId: 'non-existent',
        }),
      );

      expect(state).toBe(stateWithTasks);
    });

    it('should return unchanged state when subtask not in parent', () => {
      const state = taskReducer(
        stateWithTasks,
        fromActions.moveSubTaskToBottom({
          id: 'subTask1',
          parentId: 'task2',
        }),
      );

      expect(state).toBe(stateWithTasks);
    });
  });

  describe('Current task operations', () => {
    it('should set current task', () => {
      const action = fromActions.setCurrentTask({ id: 'task2' });
      const state = taskReducer(stateWithTasks, action);

      expect(state.currentTaskId).toBe('task2');
    });

    it('should unset current task', () => {
      const action = fromActions.unsetCurrentTask();
      const state = taskReducer(stateWithTasks, action);

      expect(state.currentTaskId).toBeNull();
      expect(state.lastCurrentTaskId).toBe('task1');
    });

    it('should preserve lastCurrentTaskId on a no-op unsetCurrentTask', () => {
      const pausedState: TaskState = {
        ...stateWithTasks,
        currentTaskId: null,
        lastCurrentTaskId: 'task1',
      };
      const state = taskReducer(pausedState, fromActions.unsetCurrentTask());

      expect(state.currentTaskId).toBeNull();
      expect(state.lastCurrentTaskId).toBe('task1');
    });
  });

  describe('removeTasksFromTodayTag action', () => {
    it('should maintain task order by moving removed tasks to the beginning', () => {
      const stateWithOrderedTasks: TaskState = {
        ...initialTaskState,
        ids: ['task1', 'task2', 'task3', 'task4'],
        entities: {
          task1: createTask('task1'),
          task2: createTask('task2'),
          task3: createTask('task3'),
          task4: createTask('task4'),
        },
      };

      const action = TaskSharedActions.removeTasksFromTodayTag({
        taskIds: ['task2', 'task4'],
      });
      const state = taskReducer(stateWithOrderedTasks, action);

      // The removed tasks should be moved to the beginning while maintaining their relative order
      expect(state.ids).toEqual(['task2', 'task4', 'task1', 'task3']);
    });

    it('should ignore all invalid IDs and leave state unchanged', () => {
      stubWindowConfirm(false);
      if (!jasmine.isSpy(window.alert)) {
        spyOn(window, 'alert');
      }

      const action = TaskSharedActions.removeTasksFromTodayTag({
        taskIds: ['nonexistent1', 'nonexistent2'],
      });
      const state = taskReducer(stateWithTasks, action);

      expect(state.ids).toEqual(stateWithTasks.ids);
      expect(state.entities).toEqual(stateWithTasks.entities);
    });

    it('should filter out invalid IDs and only reorder valid ones', () => {
      stubWindowConfirm(false);
      if (!jasmine.isSpy(window.alert)) {
        spyOn(window, 'alert');
      }

      const stateWithOrderedTasks: TaskState = {
        ...initialTaskState,
        ids: ['task1', 'task2', 'task3', 'task4'],
        entities: {
          task1: createTask('task1'),
          task2: createTask('task2'),
          task3: createTask('task3'),
          task4: createTask('task4'),
        },
      };

      const action = TaskSharedActions.removeTasksFromTodayTag({
        taskIds: ['task2', 'nonexistent', 'task4'],
      });
      const state = taskReducer(stateWithOrderedTasks, action);

      expect(state.ids).toEqual(['task2', 'task4', 'task1', 'task3']);
      expect(state.entities['task2']).toBeDefined();
      expect(state.entities['task4']).toBeDefined();
      expect(state.entities['nonexistent' as any]).toBeUndefined();
    });

    it('should call devError when orphan IDs are detected', () => {
      _resetDevErrorState();
      stubWindowConfirm(false);
      const alertSpy = jasmine.isSpy(window.alert)
        ? (window.alert as jasmine.Spy)
        : spyOn(window, 'alert');

      const action = TaskSharedActions.removeTasksFromTodayTag({
        taskIds: ['nonexistent1'],
      });
      taskReducer(stateWithTasks, action);

      expect(alertSpy).toHaveBeenCalled();
    });
  });

  describe('TaskSharedActions.addTagToTask', () => {
    it('should add tagId to task tagIds', () => {
      const action = TaskSharedActions.addTagToTask({
        taskId: 'task1',
        tagId: 'tag1',
      });
      const state = taskReducer(stateWithTasks, action);

      expect(state.entities['task1']!.tagIds).toContain('tag1');
    });

    it('should ensure tagId uniqueness', () => {
      const stateWithTag = taskReducer(
        stateWithTasks,
        TaskSharedActions.addTagToTask({ taskId: 'task1', tagId: 'tag1' }),
      );

      const action = TaskSharedActions.addTagToTask({
        taskId: 'task1',
        tagId: 'tag1',
      });
      const state = taskReducer(stateWithTag, action);

      expect(state.entities['task1']!.tagIds.length).toBe(1);
      expect(state.entities['task1']!.tagIds).toContain('tag1');
    });
  });

  describe('Incremental parent time update optimization', () => {
    const createTaskWithTime = (
      id: string,
      timeSpentOnDay: { [key: string]: number },
      parentId?: string,
    ): Task =>
      createTask(id, {
        timeSpentOnDay,
        timeSpent: Object.values(timeSpentOnDay).reduce((a, b) => a + b, 0),
        parentId,
      });

    it('should incrementally update parent timeSpentOnDay when subtask time is added', () => {
      const parentTask = createTaskWithTime('parent', {
        '2024-01-01': 3600,
        '2024-01-02': 1800,
      });
      const subtask1 = createTaskWithTime(
        'sub1',
        { '2024-01-01': 1800, '2024-01-02': 900 },
        'parent',
      );
      const subtask2 = createTaskWithTime(
        'sub2',
        { '2024-01-01': 1800, '2024-01-02': 900 },
        'parent',
      );

      const stateWithParent: TaskState = {
        ...initialTaskState,
        ids: ['parent', 'sub1', 'sub2'],
        entities: {
          parent: { ...parentTask, subTaskIds: ['sub1', 'sub2'] },
          sub1: subtask1,
          sub2: subtask2,
        },
      };

      // Add 600ms to subtask1 on 2024-01-01
      const action = TimeTrackingActions.addTimeSpent({
        task: subtask1,
        date: '2024-01-01',
        duration: 600,
        isFromTrackingReminder: false,
      });
      const state = taskReducer(stateWithParent, action);

      // Parent should have incremental update: 3600 + 600 = 4200 for 01-01
      expect(state.entities['parent']!.timeSpentOnDay['2024-01-01']).toBe(4200);
      expect(state.entities['parent']!.timeSpentOnDay['2024-01-02']).toBe(1800);
      expect(state.entities['parent']!.timeSpent).toBe(6000); // 4200 + 1800
    });

    it('should handle adding time to a new day', () => {
      const parentTask = createTaskWithTime('parent', { '2024-01-01': 3600 });
      const subtask = createTaskWithTime('sub', { '2024-01-01': 3600 }, 'parent');

      const stateWithParent: TaskState = {
        ...initialTaskState,
        ids: ['parent', 'sub'],
        entities: {
          parent: { ...parentTask, subTaskIds: ['sub'] },
          sub: subtask,
        },
      };

      // Add time to a new day (2024-01-02)
      const action = TimeTrackingActions.addTimeSpent({
        task: subtask,
        date: '2024-01-02',
        duration: 1800,
        isFromTrackingReminder: false,
      });
      const state = taskReducer(stateWithParent, action);

      expect(state.entities['parent']!.timeSpentOnDay['2024-01-01']).toBe(3600);
      expect(state.entities['parent']!.timeSpentOnDay['2024-01-02']).toBe(1800);
      expect(state.entities['parent']!.timeSpent).toBe(5400);
    });

    it('should correctly update subtask and parent timeSpent totals', () => {
      const parentTask = createTaskWithTime('parent', { '2024-01-01': 1000 });
      const subtask = createTaskWithTime('sub', { '2024-01-01': 1000 }, 'parent');

      const stateWithParent: TaskState = {
        ...initialTaskState,
        ids: ['parent', 'sub'],
        entities: {
          parent: { ...parentTask, subTaskIds: ['sub'] },
          sub: subtask,
        },
      };

      // Add more time
      const action = TimeTrackingActions.addTimeSpent({
        task: subtask,
        date: '2024-01-01',
        duration: 500,
        isFromTrackingReminder: false,
      });
      const state = taskReducer(stateWithParent, action);

      // Subtask should have updated timeSpent
      expect(state.entities['sub']!.timeSpentOnDay['2024-01-01']).toBe(1500);
      expect(state.entities['sub']!.timeSpent).toBe(1500);

      // Parent should have incremental update
      expect(state.entities['parent']!.timeSpentOnDay['2024-01-01']).toBe(1500);
      expect(state.entities['parent']!.timeSpent).toBe(1500);
    });
  });

  describe('moveToArchive action - orphan subtask handling', () => {
    // These tests document the defensive fix for a race condition where:
    // 1. Client A adds subtask to parent
    // 2. Client B does SYNC_IMPORT before parent.subTaskIds is synced
    // 3. Client A archives parent
    // 4. Client B receives archive op with stale subTaskIds (missing the new subtask)
    // The fix ensures subtasks are looked up from state, not just from the action payload.

    beforeEach(() => {
      // Mock confirm to return false to prevent devError from throwing
      stubWindowConfirm(false);
      if (!jasmine.isSpy(window.alert)) {
        spyOn(window, 'alert');
      }
    });

    it('should remove subtasks even when they are in state but not in payload subTaskIds', () => {
      // State has a subtask that points to parent, but parent's subTaskIds is empty
      // This simulates the race condition scenario
      const orphanSubTask = createTask('orphan-sub', { parentId: 'task1' });
      const parentWithEmptySubTaskIds = { ...task1, subTaskIds: [] };

      const stateWithOrphan: TaskState = {
        ...initialTaskState,
        ids: ['task1', 'orphan-sub'],
        entities: {
          task1: parentWithEmptySubTaskIds,
          'orphan-sub': orphanSubTask,
        },
      };

      // Archive action has parent with empty subTaskIds (stale data from another client)
      const action = TaskSharedActions.moveToArchive({
        tasks: [parentWithEmptySubTaskIds as any],
      });

      const state = taskReducer(stateWithOrphan, action);

      // AFTER FIX: Both parent and orphan subtask should be removed
      expect(state.ids).not.toContain('task1');
      expect(state.ids).not.toContain('orphan-sub');
      expect(state.entities['task1']).toBeUndefined();
      expect(state.entities['orphan-sub']).toBeUndefined();
    });

    it('should remove all subtasks: those in payload AND those in state', () => {
      // Parent has sub1 in subTaskIds, but sub2 is orphaned (in state but not in subTaskIds)
      const sub1 = createTask('sub1', { parentId: 'task1' });
      const sub2 = createTask('sub2', { parentId: 'task1' }); // orphan
      const parentWithOnlySub1 = { ...task1, subTaskIds: ['sub1'] };

      const stateWithMixed: TaskState = {
        ...initialTaskState,
        ids: ['task1', 'sub1', 'sub2'],
        entities: {
          task1: parentWithOnlySub1,
          sub1: sub1,
          sub2: sub2,
        },
      };

      const action = TaskSharedActions.moveToArchive({
        tasks: [parentWithOnlySub1 as any],
      });

      const state = taskReducer(stateWithMixed, action);

      // All should be removed
      expect(state.ids).toEqual([]);
      expect(state.entities['task1']).toBeUndefined();
      expect(state.entities['sub1']).toBeUndefined();
      expect(state.entities['sub2']).toBeUndefined();
    });

    it('should clear currentTaskId if it was an orphan subtask', () => {
      const orphanSubTask = createTask('orphan-sub', { parentId: 'task1' });
      const parentWithEmptySubTaskIds = { ...task1, subTaskIds: [] };

      const stateWithOrphanAsCurrent: TaskState = {
        ...initialTaskState,
        ids: ['task1', 'orphan-sub'],
        entities: {
          task1: parentWithEmptySubTaskIds,
          'orphan-sub': orphanSubTask,
        },
        currentTaskId: 'orphan-sub', // Current task is the orphan
      };

      const action = TaskSharedActions.moveToArchive({
        tasks: [parentWithEmptySubTaskIds as any],
      });

      const state = taskReducer(stateWithOrphanAsCurrent, action);

      // Current task should be cleared since orphan subtask was removed
      expect(state.currentTaskId).toBeNull();
    });
  });

  describe('syncTimeSpent', () => {
    it('should be a no-op for local dispatch', () => {
      const action = syncTimeSpent({
        taskId: 'task1',
        date: '2024-01-01',
        duration: 5000,
      });

      const state = taskReducer(stateWithTasks, action);

      expect(state).toBe(stateWithTasks);
    });

    it('should apply duration for remote dispatch', () => {
      const taskWithTime = createTask('task-r', {
        timeSpentOnDay: { '2024-01-01': 3000 },
        timeSpent: 3000,
      });
      const stateWithTime: TaskState = {
        ...initialTaskState,
        ids: ['task-r'],
        entities: { 'task-r': taskWithTime },
      };

      const action = syncTimeSpent({
        taskId: 'task-r',
        date: '2024-01-01',
        duration: 5000,
      });
      // Simulate remote by adding isRemote flag
      const remoteAction = { ...action, meta: { ...action.meta, isRemote: true } };
      const state = taskReducer(stateWithTime, remoteAction);

      expect(state.entities['task-r']!.timeSpentOnDay['2024-01-01']).toBe(8000);
      expect(state.entities['task-r']!.timeSpent).toBe(8000);
    });

    it('should handle remote dispatch for missing task gracefully', () => {
      const action = syncTimeSpent({
        taskId: 'nonexistent',
        date: '2024-01-01',
        duration: 5000,
      });
      const remoteAction = { ...action, meta: { ...action.meta, isRemote: true } };
      const state = taskReducer(stateWithTasks, remoteAction);

      expect(state).toBe(stateWithTasks);
    });
  });

  describe('PlannerActions.planTaskForDay', () => {
    it('should clear remindAt when rescheduling a task', () => {
      const taskWithReminder = createTask('task-remind', {
        remindAt: Date.now() - 60000,
        dueDay: '2024-01-01',
      });
      const stateWithReminder: TaskState = {
        ...initialTaskState,
        ids: ['task-remind'],
        entities: {
          'task-remind': taskWithReminder,
        },
        currentTaskId: null,
      };

      const action = PlannerActions.planTaskForDay({
        task: taskWithReminder,
        day: '2024-01-02',
      });

      const result = taskReducer(stateWithReminder, action);

      expect(result.entities['task-remind']!.remindAt).toBeUndefined();
      expect(result.entities['task-remind']!.dueDay).toBe('2024-01-02');
      expect(result.entities['task-remind']!.dueWithTime).toBeUndefined();
    });
  });

  // -----------------------------------------------------------------------
  // timeSpentOnDay normalization (issue #7104)
  //
  // Legacy archive/persisted tasks can have timeSpentOnDay: undefined when
  // they were created before the field was introduced. Rather than adding
  // optional-chaining guards at every individual access site (which TypeScript
  // won't enforce since the type declares the field as required), we normalize
  // to {} at the data boundary so the rest of the codebase can trust the
  // invariant: timeSpentOnDay is always a valid object, never undefined.
  // -----------------------------------------------------------------------

  describe('loadAllData - timeSpentOnDay normalization', () => {
    it('should normalize tasks with undefined timeSpentOnDay to {} on load', () => {
      const taskWithUndefined = createTask('t1', { timeSpentOnDay: undefined as any });
      const appDataComplete = {
        task: {
          ids: ['t1'],
          entities: { t1: taskWithUndefined },
          currentTaskId: null,
          selectedTaskId: null,
          lastCurrentTaskId: null,
          isDataLoaded: false,
        },
      } as any;

      const result = taskReducer(initialTaskState, loadAllData({ appDataComplete }));

      expect(result.entities['t1']!.timeSpentOnDay).toEqual({});
    });

    it('should leave tasks with valid timeSpentOnDay untouched', () => {
      const taskWithTime = createTask('t1', {
        timeSpentOnDay: { '2026-04-01': 3600000 },
      });
      const appDataComplete = {
        task: {
          ids: ['t1'],
          entities: { t1: taskWithTime },
          currentTaskId: null,
          selectedTaskId: null,
          lastCurrentTaskId: null,
          isDataLoaded: false,
        },
      } as any;

      const result = taskReducer(initialTaskState, loadAllData({ appDataComplete }));

      expect(result.entities['t1']!.timeSpentOnDay).toEqual({ '2026-04-01': 3600000 });
    });
  });

  describe('loadAllData - subTaskIds normalization', () => {
    it('should remove duplicate subTaskIds on load', () => {
      const parent = createTask('parent', { subTaskIds: ['subTask', 'subTask'] });
      const subTask = createTask('subTask', { parentId: 'parent' });
      const appDataComplete = {
        task: {
          ids: ['parent', 'subTask'],
          entities: { parent, subTask },
          currentTaskId: null,
          selectedTaskId: null,
          lastCurrentTaskId: null,
          isDataLoaded: false,
        },
      } as any;

      const result = taskReducer(initialTaskState, loadAllData({ appDataComplete }));

      expect(result.entities['parent']!.subTaskIds).toEqual(['subTask']);
    });
  });

  describe('addSubTask - undefined timeSpentOnDay guard', () => {
    it('should not crash when the first subtask of a parent has undefined timeSpentOnDay', () => {
      // The crash at task.reducer.ts:473 only fires on the FIRST subtask of a parent
      // (subTaskIds.length === 0), where it tries to inherit the parent's timeSpentOnDay.
      // If the new subtask has timeSpentOnDay: undefined, Object.keys(undefined) throws.
      const parentWithNoSubs = createTask('parentNoSubs', { subTaskIds: [] });
      const state: TaskState = {
        ...initialTaskState,
        ids: ['parentNoSubs'],
        entities: { parentNoSubs: parentWithNoSubs },
      };
      const newSubTask = createTask('sub99', { timeSpentOnDay: undefined as any });
      const action = fromActions.addSubTask({
        task: newSubTask,
        parentId: 'parentNoSubs',
      });

      expect(() => taskReducer(state, action)).not.toThrow();
      const result = taskReducer(state, action);
      expect(result.entities['parentNoSubs']!.subTaskIds).toContain('sub99');
    });
  });

  describe('roundTimeSpentForDay - undefined timeSpentOnDay guard', () => {
    it('should not crash when the task has undefined timeSpentOnDay', () => {
      const stateWithUndefined: TaskState = {
        ...initialTaskState,
        ids: ['t1'],
        entities: {
          t1: createTask('t1', { subTaskIds: [], timeSpentOnDay: undefined as any }),
        },
      };
      const action = fromActions.roundTimeSpentForDay({
        day: '2026-04-02',
        taskIds: ['t1'],
        isRoundUp: false,
        roundTo: 'QUARTER' as any,
        projectId: undefined,
      });

      expect(() => taskReducer(stateWithUndefined, action)).not.toThrow();
    });
  });

  // Regression: subtask collapse state (_hideSubTasksMode) must survive a restart.
  // It only persists if the action that writes it is captured to the op-log,
  // which requires isPersistent metadata. `updateTaskUi` carries an absolute
  // value (replay-safe); toggleSubTaskMode resolves the value and dispatches it.
  // See issue #8781.
  describe('updateTaskUi persistence metadata', () => {
    it('should be a persistent TASK Update action', () => {
      const action = fromActions.updateTaskUi({
        task: { id: 'task1', changes: { _hideSubTasksMode: undefined } },
      });

      expect(action.meta).toBeDefined();
      expect(action.meta.isPersistent).toBe(true);
      expect(action.meta.entityType).toBe('TASK');
      expect(action.meta.entityId).toBe('task1');
      expect(action.meta.opType).toBe(OpType.Update);
    });

    // The metadata assertion above proves the change is *eligible* for capture.
    // This one proves it actually survives the whole op-log path end to end:
    // dispatch -> capture into an operation payload -> serialize -> convert the
    // op back to an action -> replay through the reducer. The JSON serialize hop
    // mirrors the sync transport and the SQLite op-log backend, both of which
    // round-trip op payloads as JSON, so it is where a lost value would regress.
    // See issue #8781.
    it('should round-trip _hideSubTasksMode through capture, serialization and replay', () => {
      const stateShown: TaskState = {
        ...initialTaskState,
        ids: ['task1'],
        entities: { task1: createTask('task1') },
      };

      // 1. Dispatch: the persistent action toggleSubTaskMode dispatches on collapse.
      const action = fromActions.updateTaskUi({
        task: {
          id: 'task1',
          changes: { _hideSubTasksMode: HideSubTasksMode.HideAll },
        },
      });

      // 2. Capture: the effects store the action fields under payload.actionPayload.
      const op: Operation = {
        id: 'op-8781',
        actionType: action.type as ActionType,
        opType: action.meta.opType,
        entityType: action.meta.entityType,
        entityId: action.meta.entityId as string,
        payload: { actionPayload: { task: action.task }, entityChanges: [] },
        clientId: 'clientA',
        vectorClock: { clientA: 1 },
        timestamp: 0,
        schemaVersion: 1,
      };

      // 3. Serialize over the wire / into the op-log, then read it back.
      const wireOp = JSON.parse(JSON.stringify(op)) as Operation;

      // 4. Convert the persisted op back into a replayable action and replay it.
      const replayAction = convertOpToAction(wireOp);
      const replayed = taskReducer(stateShown, replayAction);

      expect(replayed.entities.task1?._hideSubTasksMode).toBe(HideSubTasksMode.HideAll);
    });
  });
});
