import {
  TestBed,
  fakeAsync,
  tick,
  discardPeriodicTasks,
  flush,
  flushMicrotasks,
} from '@angular/core/testing';
import {
  HttpClientTestingModule,
  HttpTestingController,
} from '@angular/common/http/testing';
import { provideMockStore, MockStore } from '@ngrx/store/testing';
import { CalendarIntegrationService } from './calendar-integration.service';
import {
  selectCalendarProviders,
  selectEnabledIssueProviders,
} from '../issue/store/issue-provider.selectors';
import { selectAllCalendarTaskEventIds } from '../tasks/store/task.selectors';
import { IssueProviderCalendar } from '../issue/issue.model';
import {
  LOCAL_FILE_CHECK_INTERVAL,
  getEffectiveCheckInterval,
  DEFAULT_CALENDAR_CFG,
} from '../issue/providers/calendar/calendar.const';
import { SnackService } from '../../core/snack/snack.service';
import { take } from 'rxjs/operators';
import { Subscription } from 'rxjs';
import { getDbDateStr } from '../../util/get-db-date-str';
import { PluginIssueProviderRegistryService } from '../../plugins/issue-provider/plugin-issue-provider-registry.service';
import { PluginHttpService } from '../../plugins/issue-provider/plugin-http.service';
import { IssueProviderPluginDefinition } from '../../plugins/issue-provider/plugin-issue-provider.model';
import { IssueProviderPluginType } from '../issue/issue.model';
import { NotIcalResponseError } from '../schedule/ical/is-likely-ical';
// Static import forces ical.js into the main test bundle so the dynamic
// import() inside loadIcalModule resolves from the webpack module cache
// without a JSONP chunk request — which times out in Karma's test runner.
import 'ical.js';
import { loadIcalModule } from '../schedule/ical/ical-lazy-loader';
import { CalendarIntegrationEvent } from './calendar-integration.model';
import { HiddenCalendarEventsService } from './hidden-calendar-events.service';
import { ScheduleCalendarMapEntry } from '../schedule/schedule.model';
import { TaskArchiveService } from '../archive/task-archive.service';

describe('CalendarIntegrationService', () => {
  let service: CalendarIntegrationService;
  let store: MockStore;
  let httpMock: HttpTestingController;
  let subscriptions: Subscription[] = [];

  const mockSnackService = {
    open: jasmine.createSpy('open'),
  };

  // Default: no calendar tasks in the archive. The #7971 repro overrides `load` to
  // return an archived calendar task. Reset in beforeEach to avoid cross-test pollution.
  const mockTaskArchiveService = {
    load: jasmine.createSpy('load'),
  };
  const emptyArchive = (): Promise<{ ids: string[]; entities: object }> =>
    Promise.resolve({ ids: [], entities: {} });

  const createMockProvider = (
    overrides: Partial<IssueProviderCalendar> = {},
  ): IssueProviderCalendar =>
    ({
      id: 'test-provider-1',
      isEnabled: true,
      issueProviderKey: 'ICAL',
      icalUrl: 'https://example.com/calendar.ics',
      checkUpdatesEvery: DEFAULT_CALENDAR_CFG.checkUpdatesEvery,
      showBannerBeforeThreshold: DEFAULT_CALENDAR_CFG.showBannerBeforeThreshold,
      isAutoImportForCurrentDay: false,
      isDisabledForWebApp: false,
      ...overrides,
    }) as IssueProviderCalendar;

  const todayIcalDate = getDbDateStr().replace(/-/g, '');

  const MOCK_ICAL_DATA = `BEGIN:VCALENDAR
VERSION:2.0
BEGIN:VEVENT
DTSTART:${todayIcalDate}T100000Z
DTEND:${todayIcalDate}T110000Z
SUMMARY:Test Event
UID:test-event-1
END:VEVENT
END:VCALENDAR`;

  const MOCK_ICAL_DATA_2 = `BEGIN:VCALENDAR
VERSION:2.0
BEGIN:VEVENT
DTSTART:${todayIcalDate}T140000Z
DTEND:${todayIcalDate}T150000Z
SUMMARY:Another Event
UID:test-event-2
END:VEVENT
END:VCALENDAR`;

  beforeEach(() => {
    // Clear localStorage before each test
    localStorage.clear();
    subscriptions = [];
    mockTaskArchiveService.load.and.callFake(emptyArchive);

    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [
        CalendarIntegrationService,
        provideMockStore({
          selectors: [
            { selector: selectCalendarProviders, value: [] },
            { selector: selectEnabledIssueProviders, value: [] },
            { selector: selectAllCalendarTaskEventIds, value: [] },
          ],
        }),
        { provide: SnackService, useValue: mockSnackService },
        { provide: TaskArchiveService, useValue: mockTaskArchiveService },
      ],
    });

    service = TestBed.inject(CalendarIntegrationService);
    store = TestBed.inject(MockStore);
    httpMock = TestBed.inject(HttpTestingController);
  });

  afterEach(() => {
    // Clean up all subscriptions
    subscriptions.forEach((sub) => sub.unsubscribe());
    subscriptions = [];
    localStorage.clear();
    // Reset selector overrides to prevent test pollution
    store.resetSelectors();
  });

  describe('calendarEvents$', () => {
    describe('basic functionality', () => {
      it('should emit cached data immediately on first subscription', fakeAsync(() => {
        let emittedValue: unknown;
        const sub = service.calendarEvents$.pipe(take(1)).subscribe((val) => {
          emittedValue = val;
        });
        subscriptions.push(sub);

        tick(0);
        expect(emittedValue).toEqual([]);
        discardPeriodicTasks();
      }));

      it('should return empty array when no providers', fakeAsync(() => {
        store.overrideSelector(selectCalendarProviders, []);
        store.refreshState();

        let emittedValue: unknown;
        const sub = service.calendarEvents$.pipe(take(2)).subscribe((val) => {
          emittedValue = val;
        });
        subscriptions.push(sub);

        tick(0);
        expect(emittedValue).toEqual([]);
        discardPeriodicTasks();
      }));

      it('should emit cached data from localStorage if available', fakeAsync(() => {
        const cachedProvider = createMockProvider({ id: 'provider-1' });
        const cachedData = [
          {
            items: [
              {
                id: 'cached-event-1',
                calProviderId: 'provider-1',
                issueProviderKey: 'ICAL',
                title: 'Cached Event',
                start: Date.now() + 60000, // Future event
                duration: 3600000,
              },
            ],
          },
        ];
        localStorage.setItem('SUP_CAL_EVENTS_CACHE', JSON.stringify(cachedData));
        store.overrideSelector(selectCalendarProviders, [cachedProvider]);
        store.refreshState();

        // Create new service instance to pick up cached data
        const newService = TestBed.inject(CalendarIntegrationService);

        let emittedValue: unknown;
        const sub = newService.calendarEvents$.pipe(take(1)).subscribe((val) => {
          emittedValue = val;
        });
        subscriptions.push(sub);

        tick(0);
        expect(emittedValue).toBeDefined();
        expect((emittedValue as any[])[0].items.length).toBe(1);
        discardPeriodicTasks();
      }));

      it('should filter out events older than one week from cache', fakeAsync(() => {
        const ONE_WEEK = 60 * 60 * 1000 * 24 * 7;
        const cachedProvider = createMockProvider({ id: 'provider-1' });
        const cachedData = [
          {
            items: [
              {
                id: 'old-event',
                calProviderId: 'provider-1',
                issueProviderKey: 'ICAL',
                title: 'Old Event',
                start: Date.now() - ONE_WEEK - 7200000, // more than 1 week ago
                duration: 3600000,
              },
              {
                id: 'recent-past-event',
                calProviderId: 'provider-1',
                issueProviderKey: 'ICAL',
                title: 'Recent Past Event',
                start: Date.now() - 7200000, // 2 hours ago
                duration: 3600000,
              },
              {
                id: 'future-event',
                calProviderId: 'provider-1',
                issueProviderKey: 'ICAL',
                title: 'Future Event',
                start: Date.now() + 60000,
                duration: 3600000,
              },
            ],
          },
        ];
        localStorage.setItem('SUP_CAL_EVENTS_CACHE', JSON.stringify(cachedData));
        store.overrideSelector(selectCalendarProviders, [cachedProvider]);
        store.refreshState();

        const newService = TestBed.inject(CalendarIntegrationService);

        let emittedValue: unknown;
        const sub = newService.calendarEvents$.pipe(take(1)).subscribe((val) => {
          emittedValue = val;
        });
        subscriptions.push(sub);

        tick(0);
        // Old event (> 1 week) is filtered; recent past and future events are kept
        expect((emittedValue as any[])[0].items.length).toBe(2);
        expect((emittedValue as any[])[0].items[0].id).toBe('recent-past-event');
        expect((emittedValue as any[])[0].items[1].id).toBe('future-event');
        discardPeriodicTasks();
      }));
    });

    describe('regex filter in initial cached emission', () => {
      it('should resolve provider config per-event so mixed-provider cache entries are filtered correctly', fakeAsync(() => {
        const providerA: IssueProviderCalendar = createMockProvider({
          id: 'provider-a',
          filterExcludeRegex: 'Lunch',
        });
        const providerB: IssueProviderCalendar = createMockProvider({
          id: 'provider-b',
          filterExcludeRegex: null,
        });

        // A single cache entry whose items belong to different providers —
        // this can happen because _groupCachedEventsByProvider() groups by calProviderId
        // while the cache itself stores flat ScheduleCalendarMapEntry arrays.
        const cachedData = [
          {
            items: [
              {
                id: 'event-a-lunch',
                calProviderId: 'provider-a',
                issueProviderKey: 'ICAL',
                title: 'Lunch',
                start: Date.now() + 60000,
                duration: 3600000,
              },
              {
                id: 'event-a-standup',
                calProviderId: 'provider-a',
                issueProviderKey: 'ICAL',
                title: 'Standup',
                start: Date.now() + 120000,
                duration: 1800000,
              },
              {
                id: 'event-b-lunch',
                calProviderId: 'provider-b',
                issueProviderKey: 'ICAL',
                title: 'Lunch',
                start: Date.now() + 180000,
                duration: 3600000,
              },
            ],
          },
        ];
        localStorage.setItem('SUP_CAL_EVENTS_CACHE', JSON.stringify(cachedData));

        TestBed.resetTestingModule();
        TestBed.configureTestingModule({
          imports: [HttpClientTestingModule],
          providers: [
            CalendarIntegrationService,
            provideMockStore({
              selectors: [
                { selector: selectCalendarProviders, value: [providerA, providerB] },
                { selector: selectEnabledIssueProviders, value: [] },
                { selector: selectAllCalendarTaskEventIds, value: [] },
              ],
            }),
            { provide: SnackService, useValue: mockSnackService },
            { provide: TaskArchiveService, useValue: mockTaskArchiveService },
          ],
        });

        const freshService = TestBed.inject(CalendarIntegrationService);

        let emittedValue: any;
        const sub = freshService.calendarEvents$.pipe(take(1)).subscribe((val) => {
          emittedValue = val;
        });

        tick(0);

        const allItems = emittedValue?.flatMap((e: any) => e.items ?? []) ?? [];
        const ids = allItems.map((i: any) => i.id);

        // Provider A has filterExcludeRegex='Lunch' → 'event-a-lunch' must be gone
        expect(ids).not.toContain('event-a-lunch');
        // Provider A standup is not excluded
        expect(ids).toContain('event-a-standup');
        // Provider B has no filter → its 'Lunch' event must survive
        expect(ids).toContain('event-b-lunch');

        sub.unsubscribe();
        discardPeriodicTasks();
      }));

      it('should filter out task-imported, skipped, and hidden events from initial cached emission', fakeAsync(() => {
        const provider = createMockProvider({ id: 'provider-x' });
        const cachedData = [
          {
            items: [
              {
                id: 'event-task',
                calProviderId: 'provider-x',
                issueProviderKey: 'ICAL',
                title: 'Already a Task',
                start: Date.now() + 60000,
                duration: 3600000,
              },
              {
                id: 'event-skipped',
                calProviderId: 'provider-x',
                issueProviderKey: 'ICAL',
                title: 'Skipped Event',
                start: Date.now() + 120000,
                duration: 1800000,
              },
              {
                id: 'event-hidden',
                calProviderId: 'provider-x',
                issueProviderKey: 'ICAL',
                title: 'Hidden Event',
                start: Date.now() + 180000,
                duration: 3600000,
              },
              {
                id: 'event-visible',
                calProviderId: 'provider-x',
                issueProviderKey: 'ICAL',
                title: 'Visible Event',
                start: Date.now() + 240000,
                duration: 1800000,
              },
            ],
          },
        ];
        localStorage.setItem('SUP_CAL_EVENTS_CACHE', JSON.stringify(cachedData));

        TestBed.resetTestingModule();
        TestBed.configureTestingModule({
          imports: [HttpClientTestingModule],
          providers: [
            CalendarIntegrationService,
            provideMockStore({
              selectors: [
                { selector: selectCalendarProviders, value: [provider] },
                { selector: selectEnabledIssueProviders, value: [] },
                { selector: selectAllCalendarTaskEventIds, value: ['event-task'] },
              ],
            }),
            { provide: SnackService, useValue: mockSnackService },
            { provide: TaskArchiveService, useValue: mockTaskArchiveService },
          ],
        });

        const freshService = TestBed.inject(CalendarIntegrationService);

        // Seed skipped and hidden IDs before subscribing
        freshService.skippedEventIds$.next(['event-skipped']);
        const hiddenEventsService = TestBed.inject(HiddenCalendarEventsService);
        hiddenEventsService.hiddenEventIds$.next(['event-hidden']);

        let emittedValue: any;
        const sub = freshService.calendarEvents$.pipe(take(1)).subscribe((val) => {
          emittedValue = val;
        });

        tick(0);

        const allItems = emittedValue?.flatMap((e: any) => e.items ?? []) ?? [];
        const ids = allItems.map((i: any) => i.id);

        expect(ids).not.toContain('event-task');
        expect(ids).not.toContain('event-skipped');
        expect(ids).not.toContain('event-hidden');
        expect(ids).toContain('event-visible');

        sub.unsubscribe();
        discardPeriodicTasks();
      }));

      it('should reapply regex filters to cached events immediately when provider config changes', fakeAsync(() => {
        const provider = createMockProvider({ id: 'provider-x' });
        const cachedData = [
          {
            items: [
              {
                id: 'event-lunch',
                calProviderId: 'provider-x',
                issueProviderKey: 'ICAL',
                title: 'Lunch',
                start: Date.now() + 60000,
                duration: 3600000,
              },
              {
                id: 'event-standup',
                calProviderId: 'provider-x',
                issueProviderKey: 'ICAL',
                title: 'Standup',
                start: Date.now() + 120000,
                duration: 1800000,
              },
            ],
          },
        ];
        localStorage.setItem('SUP_CAL_EVENTS_CACHE', JSON.stringify(cachedData));

        TestBed.resetTestingModule();
        TestBed.configureTestingModule({
          imports: [HttpClientTestingModule],
          providers: [
            CalendarIntegrationService,
            provideMockStore({
              selectors: [
                { selector: selectCalendarProviders, value: [provider] },
                { selector: selectEnabledIssueProviders, value: [] },
                { selector: selectAllCalendarTaskEventIds, value: [] },
              ],
            }),
            { provide: SnackService, useValue: mockSnackService },
            { provide: TaskArchiveService, useValue: mockTaskArchiveService },
          ],
        });

        const freshService = TestBed.inject(CalendarIntegrationService);
        const freshStore = TestBed.inject(MockStore);
        const freshHttpMock = TestBed.inject(HttpTestingController);
        const emissions: string[][] = [];
        const sub = freshService.calendarEvents$.subscribe((entries) => {
          emissions.push(entries.flatMap((entry) => entry.items.map((item) => item.id)));
        });

        tick(0);
        expect(emissions[0]).toContain('event-lunch');
        expect(emissions[0]).toContain('event-standup');

        freshStore.overrideSelector(selectCalendarProviders, [
          createMockProvider({
            id: 'provider-x',
            filterExcludeRegex: 'Lunch',
          }),
        ]);
        freshStore.refreshState();
        tick(0);

        expect(emissions[emissions.length - 1]).not.toContain('event-lunch');
        expect(emissions[emissions.length - 1]).toContain('event-standup');

        freshHttpMock
          .match(provider.icalUrl)
          .filter((req) => !req.cancelled)
          .forEach((req) => req.flush(MOCK_ICAL_DATA));
        sub.unsubscribe();
        discardPeriodicTasks();
      }));

      it('should filter cached events for disabled or removed providers', fakeAsync(() => {
        const activeProvider = createMockProvider({
          id: 'active-provider',
          icalUrl: 'https://active.example.com/calendar.ics',
        });
        const cachedData = [
          {
            items: [
              {
                id: 'active-event',
                calProviderId: 'active-provider',
                issueProviderKey: 'ICAL',
                title: 'Active Event',
                start: Date.now() + 60000,
                duration: 3600000,
              },
              {
                id: 'removed-event',
                calProviderId: 'removed-provider',
                issueProviderKey: 'ICAL',
                title: 'Removed Event',
                start: Date.now() + 120000,
                duration: 1800000,
              },
            ],
          },
        ];
        localStorage.setItem('SUP_CAL_EVENTS_CACHE', JSON.stringify(cachedData));

        TestBed.resetTestingModule();
        TestBed.configureTestingModule({
          imports: [HttpClientTestingModule],
          providers: [
            CalendarIntegrationService,
            provideMockStore({
              selectors: [
                { selector: selectCalendarProviders, value: [activeProvider] },
                { selector: selectEnabledIssueProviders, value: [] },
                { selector: selectAllCalendarTaskEventIds, value: [] },
              ],
            }),
            { provide: SnackService, useValue: mockSnackService },
            { provide: TaskArchiveService, useValue: mockTaskArchiveService },
          ],
        });

        const freshService = TestBed.inject(CalendarIntegrationService);

        let emittedValue: any;
        const sub = freshService.calendarEvents$.pipe(take(1)).subscribe((val) => {
          emittedValue = val;
        });

        tick(0);

        const allItems = emittedValue?.flatMap((e: any) => e.items ?? []) ?? [];
        const ids = allItems.map((i: any) => i.id);

        expect(ids).toContain('active-event');
        expect(ids).not.toContain('removed-event');

        sub.unsubscribe();
        discardPeriodicTasks();
      }));
    });

    describe('interval behavior', () => {
      it('should use LOCAL_FILE_CHECK_INTERVAL for file:// URLs', () => {
        const fileProvider = createMockProvider({
          icalUrl: 'file:///home/user/calendar.ics',
          checkUpdatesEvery: 2 * 60 * 60 * 1000,
        });

        expect(getEffectiveCheckInterval(fileProvider)).toBe(LOCAL_FILE_CHECK_INTERVAL);
      });

      it('should use checkUpdatesEvery for remote URLs', () => {
        const customInterval = 30 * 60 * 1000;
        const remoteProvider = createMockProvider({
          icalUrl: 'https://example.com/calendar.ics',
          checkUpdatesEvery: customInterval,
        });

        expect(getEffectiveCheckInterval(remoteProvider)).toBe(customInterval);
      });

      it('should prefer shorter interval in mixed providers scenario', () => {
        const remoteProvider = createMockProvider({
          id: 'remote-provider',
          icalUrl: 'https://example.com/calendar.ics',
          checkUpdatesEvery: 2 * 60 * 60 * 1000,
        });

        const fileProvider = createMockProvider({
          id: 'file-provider',
          icalUrl: 'file:///home/user/calendar.ics',
          checkUpdatesEvery: 2 * 60 * 60 * 1000,
        });

        const remoteInterval = getEffectiveCheckInterval(remoteProvider);
        const fileInterval = getEffectiveCheckInterval(fileProvider);

        expect(Math.min(remoteInterval, fileInterval)).toBe(LOCAL_FILE_CHECK_INTERVAL);
      });
    });

    describe('memory leak prevention', () => {
      it('should share single subscription via shareReplay', fakeAsync(() => {
        const mockProvider = createMockProvider();
        store.overrideSelector(selectCalendarProviders, [mockProvider]);
        store.refreshState();

        // Subscribe twice
        const sub1 = service.calendarEvents$.subscribe(() => {});
        const sub2 = service.calendarEvents$.subscribe(() => {});
        subscriptions.push(sub1, sub2);

        tick(0);

        // Should only have one HTTP request due to shareReplay
        const req = httpMock.expectOne(mockProvider.icalUrl);
        req.flush(MOCK_ICAL_DATA);

        // Verify no additional requests
        httpMock.expectNone(mockProvider.icalUrl);

        discardPeriodicTasks();
      }));

      it('should clean up timer when all subscribers unsubscribe (refCount)', fakeAsync(() => {
        const mockProvider = createMockProvider({
          checkUpdatesEvery: 60000, // 1 minute
        });
        store.overrideSelector(selectCalendarProviders, [mockProvider]);
        store.refreshState();

        // Subscribe
        const sub = service.calendarEvents$.subscribe(() => {});

        tick(0);
        const req1 = httpMock.expectOne(mockProvider.icalUrl);
        req1.flush(MOCK_ICAL_DATA);

        // Unsubscribe
        sub.unsubscribe();

        // Wait for interval - should NOT make new request since no subscribers
        tick(60000);

        // Verify no new requests were made
        httpMock.expectNone(mockProvider.icalUrl);

        discardPeriodicTasks();
      }));

      it('should handle provider changes without memory leak', fakeAsync(() => {
        const provider1 = createMockProvider({
          id: 'provider-1',
          icalUrl: 'https://example1.com/calendar.ics',
          checkUpdatesEvery: 60000,
        });

        const provider2 = createMockProvider({
          id: 'provider-2',
          icalUrl: 'https://example2.com/calendar.ics',
          checkUpdatesEvery: 60000,
        });

        store.overrideSelector(selectCalendarProviders, [provider1]);
        store.refreshState();

        const sub = service.calendarEvents$.subscribe(() => {});
        subscriptions.push(sub);

        tick(0);
        const req1 = httpMock.expectOne(provider1.icalUrl);
        req1.flush(MOCK_ICAL_DATA);

        // Change providers - switchMap should cancel old timer
        store.overrideSelector(selectCalendarProviders, [provider2]);
        store.refreshState();

        tick(0);
        const req2 = httpMock.expectOne(provider2.icalUrl);
        req2.flush(MOCK_ICAL_DATA_2);

        // Wait for old interval - should NOT trigger request to old provider
        tick(60000);
        httpMock.expectNone(provider1.icalUrl);

        // But should trigger for new provider
        const req3 = httpMock.expectOne(provider2.icalUrl);
        req3.flush(MOCK_ICAL_DATA_2);

        discardPeriodicTasks();
      }));
    });

    describe('error handling', () => {
      it('should handle HTTP errors gracefully', fakeAsync(() => {
        const mockProvider = createMockProvider();
        store.overrideSelector(selectCalendarProviders, [mockProvider]);
        store.refreshState();

        let lastValue: unknown;
        const sub = service.calendarEvents$.subscribe((val) => {
          lastValue = val;
        });
        subscriptions.push(sub);

        tick(0);
        const req = httpMock.expectOne(mockProvider.icalUrl);
        req.error(new ProgressEvent('error'));

        tick(0);

        // Should still emit (with empty or cached data)
        expect(lastValue).toEqual([{ items: [] }]);
        discardPeriodicTasks();
      }));

      it('should continue polling after error', fakeAsync(() => {
        const mockProvider = createMockProvider({
          checkUpdatesEvery: 60000,
        });
        store.overrideSelector(selectCalendarProviders, [mockProvider]);
        store.refreshState();

        const sub = service.calendarEvents$.subscribe(() => {});
        subscriptions.push(sub);

        tick(0);
        // First request - error
        const req1 = httpMock.expectOne(mockProvider.icalUrl);
        req1.error(new ProgressEvent('error'));

        // Wait for next interval
        tick(60000);

        // Should retry
        const req2 = httpMock.expectOne(mockProvider.icalUrl);
        req2.flush(MOCK_ICAL_DATA);

        discardPeriodicTasks();
      }));
    });

    describe('disabled providers', () => {
      it('should not fetch disabled providers', fakeAsync(() => {
        const enabledProvider = createMockProvider({
          id: 'enabled-provider',
          isEnabled: true,
          icalUrl: 'https://enabled.example.com/calendar.ics',
        });

        const disabledProvider = createMockProvider({
          id: 'disabled-provider',
          isEnabled: false,
          icalUrl: 'https://disabled.example.com/calendar.ics',
        });

        store.overrideSelector(selectCalendarProviders, [
          enabledProvider,
          disabledProvider,
        ]);
        store.refreshState();

        const sub = service.calendarEvents$.subscribe(() => {});
        subscriptions.push(sub);

        tick(0);

        // Only enabled provider should be fetched
        const req = httpMock.expectOne(enabledProvider.icalUrl);
        req.flush(MOCK_ICAL_DATA);

        // Disabled provider should not be fetched
        httpMock.expectNone(disabledProvider.icalUrl);

        discardPeriodicTasks();
      }));

      it('should use default interval when all providers are disabled', () => {
        const disabledProvider = createMockProvider({
          isEnabled: false,
          icalUrl: 'https://example.com/calendar.ics',
        });

        // Access private method via any cast for testing
        const interval = (service as any)._getCombinedRefreshInterval(
          [disabledProvider],
          [],
        );

        expect(interval).toBe(2 * 60 * 60 * 1000); // Default 2 hours
      });
    });

    describe('caching', () => {
      it('should save fetched data to localStorage', fakeAsync(() => {
        // Reset TestBed for clean isolation
        TestBed.resetTestingModule();
        localStorage.clear();
        TestBed.configureTestingModule({
          imports: [HttpClientTestingModule],
          providers: [
            CalendarIntegrationService,
            provideMockStore({
              selectors: [
                { selector: selectCalendarProviders, value: [] },
                { selector: selectEnabledIssueProviders, value: [] },
                { selector: selectAllCalendarTaskEventIds, value: [] },
              ],
            }),
            { provide: SnackService, useValue: mockSnackService },
            { provide: TaskArchiveService, useValue: mockTaskArchiveService },
          ],
        });

        const freshService = TestBed.inject(CalendarIntegrationService);
        const freshStore = TestBed.inject(MockStore);
        const freshHttpMock = TestBed.inject(HttpTestingController);

        const mockProvider = createMockProvider();
        freshStore.overrideSelector(selectCalendarProviders, [mockProvider]);
        freshStore.overrideSelector(selectAllCalendarTaskEventIds, []);
        freshStore.refreshState();

        let emittedCount = 0;
        const sub = freshService.calendarEvents$.subscribe(() => {
          emittedCount++;
        });

        tick(0);
        const req = freshHttpMock.expectOne(mockProvider.icalUrl);
        req.flush(MOCK_ICAL_DATA);

        // Allow combineLatest and tap to execute
        tick(100);
        freshStore.refreshState();
        tick(100);

        // Verify the emission happened (cached data + fresh data)
        expect(emittedCount).toBeGreaterThan(0);

        const cached = localStorage.getItem('SUP_CAL_EVENTS_CACHE');
        expect(JSON.parse(cached ?? '[]') as ScheduleCalendarMapEntry[]).toEqual([
          {
            items: [
              jasmine.objectContaining({
                id: 'test-event-1',
                title: 'Test Event',
              }),
            ],
          },
        ]);

        sub.unsubscribe();
        discardPeriodicTasks();
      }));
    });
  });

  describe('skipCalendarEvent', () => {
    it('should add event ID to skipped list', () => {
      const event = {
        id: 'test-event-id',
        calProviderId: 'test-provider',
        issueProviderKey: 'ICAL',
        title: 'Test Event',
        start: Date.now(),
        duration: 60 * 60 * 1000,
      };

      service.skipCalendarEvent(event);

      expect(service.skippedEventIds$.getValue()).toContain('test-event-id');
    });

    it('should persist skipped events to localStorage', () => {
      const event = {
        id: 'test-event-id',
        calProviderId: 'test-provider',
        issueProviderKey: 'ICAL',
        title: 'Test Event',
        start: Date.now(),
        duration: 60 * 60 * 1000,
      };

      service.skipCalendarEvent(event);

      const stored = localStorage.getItem('SUP_CALENDER_EVENTS_SKIPPED_TODAY');
      expect(JSON.parse(stored ?? '[]') as string[]).toContain('test-event-id');
    });

    it('should not add duplicate event IDs', () => {
      const event = {
        id: 'test-event-id',
        calProviderId: 'test-provider',
        issueProviderKey: 'ICAL',
        title: 'Test Event',
        start: Date.now(),
        duration: 60 * 60 * 1000,
      };

      service.skipCalendarEvent(event);
      service.skipCalendarEvent(event);

      const skippedIds = service.skippedEventIds$.getValue();
      const occurrences = skippedIds.filter((id) => id === 'test-event-id').length;
      expect(occurrences).toBe(1);
    });

    it('should handle null event gracefully', () => {
      expect(() => service.skipCalendarEvent(null as any)).not.toThrow();
    });

    it('should handle event without id gracefully', () => {
      const event = {
        calProviderId: 'test-provider',
        issueProviderKey: 'ICAL',
        title: 'Test Event',
        start: Date.now(),
        duration: 60 * 60 * 1000,
      } as any;

      expect(() => service.skipCalendarEvent(event)).not.toThrow();
    });

    it('should store skip date in localStorage', () => {
      const event = {
        id: 'test-event-id',
        calProviderId: 'test-provider',
        issueProviderKey: 'ICAL',
        title: 'Test Event',
        start: Date.now(),
        duration: 60 * 60 * 1000,
      };

      service.skipCalendarEvent(event);

      const skipDay = localStorage.getItem('SUP_CALENDER_EVENTS_LAST_SKIP_DAY');
      expect(skipDay).toBe(getDbDateStr());
    });
  });

  describe('testConnection', () => {
    it('should return true when connection succeeds', async () => {
      const cfg = createMockProvider();

      const promise = service.testConnection(cfg);

      const req = httpMock.expectOne(cfg.icalUrl);
      req.flush(MOCK_ICAL_DATA);

      const result = await promise;
      expect(result).toBe(true);
    });

    it('should return false when connection fails', async () => {
      const cfg = createMockProvider();

      const promise = service.testConnection(cfg);

      const req = httpMock.expectOne(cfg.icalUrl);
      req.error(new ProgressEvent('error'));

      const result = await promise;
      expect(result).toBe(false);
    });

    it('should return false for empty response', async () => {
      const cfg = createMockProvider();

      const promise = service.testConnection(cfg);

      const req = httpMock.expectOne(cfg.icalUrl);
      req.flush('');

      const result = await promise;
      expect(result).toBe(false);
    });
  });

  describe('requestEvents$', () => {
    // iCal fixture with a single event on 2026-05-14 (inside any reasonable test window).
    const MOCK_ICAL_NEAR_FUTURE = [
      'BEGIN:VCALENDAR',
      'VERSION:2.0',
      'BEGIN:VEVENT',
      'DTSTART:20260514T100000Z',
      'DTEND:20260514T110000Z',
      'SUMMARY:Near Future Test Event',
      'UID:near-future-1',
      'END:VEVENT',
      'END:VCALENDAR',
    ].join('\r\n');

    const FIXTURE_START = new Date('2026-01-01').getTime();
    const FIXTURE_END = new Date('2027-01-01').getTime();

    // Pre-load the ical.js module before each stamping test so that the
    // module-level cache in ical-lazy-loader is warm.  Once warm, calls to
    // loadIcalModule() return a synchronously-resolved Promise that
    // flushMicrotasks() can drain inside fakeAsync.
    beforeEach(async () => {
      await loadIcalModule();
    });

    const requestAndFlush = (
      provider: IssueProviderCalendar,
    ): CalendarIntegrationEvent[] => {
      let result: CalendarIntegrationEvent[] = [];
      service.requestEvents$(provider, FIXTURE_START, FIXTURE_END).subscribe((v) => {
        result = v;
      });
      httpMock.expectOne(provider.icalUrl).flush(MOCK_ICAL_NEAR_FUTURE);
      // Drain the Promise microtasks from the async ICAL parser
      flushMicrotasks();
      return result;
    };

    describe('isReferenceCalendar stamping', () => {
      it('should stamp isReferenceCalendar: true on every event when provider is a reference calendar', fakeAsync(() => {
        const result = requestAndFlush(createMockProvider({ isReferenceCalendar: true }));
        expect(result.length).toBeGreaterThan(0);
        result.forEach((ev) => expect((ev as any).isReferenceCalendar).toBe(true));
      }));

      it('should not set isReferenceCalendar on events from a regular provider', fakeAsync(() => {
        const result = requestAndFlush(
          createMockProvider({ isReferenceCalendar: false }),
        );
        expect(result.length).toBeGreaterThan(0);
        result.forEach((ev) => expect((ev as any).isReferenceCalendar).toBeFalsy());
      }));

      it('should not set isReferenceCalendar when provider flag is absent', fakeAsync(() => {
        const result = requestAndFlush(createMockProvider());
        expect(result.length).toBeGreaterThan(0);
        result.forEach((ev) => expect((ev as any).isReferenceCalendar).toBeFalsy());
      }));
    });

    describe('color stamping', () => {
      it('should stamp color on every event when the provider has a color configured', fakeAsync(() => {
        const result = requestAndFlush(createMockProvider({ color: '#4caf50' }));
        expect(result.length).toBeGreaterThan(0);
        result.forEach((ev) => expect((ev as any).color).toBe('#4caf50'));
      }));

      it('should not add a color property when the provider has no color configured', fakeAsync(() => {
        const result = requestAndFlush(createMockProvider({ color: undefined }));
        expect(result.length).toBeGreaterThan(0);
        result.forEach((ev) => expect((ev as any).color).toBeFalsy());
      }));

      it('should stamp both color and isReferenceCalendar when both are set', fakeAsync(() => {
        const result = requestAndFlush(
          createMockProvider({ color: '#ff5722', isReferenceCalendar: true }),
        );
        expect(result.length).toBeGreaterThan(0);
        result.forEach((ev) => {
          expect((ev as any).color).toBe('#ff5722');
          expect((ev as any).isReferenceCalendar).toBe(true);
        });
      }));
    });

    it('should fetch events from provider URL', fakeAsync(() => {
      const mockProvider = createMockProvider();

      let result: unknown;
      const sub = service.requestEvents$(mockProvider).subscribe((val) => {
        result = val;
      });
      subscriptions.push(sub);

      const req = httpMock.expectOne(mockProvider.icalUrl);
      req.flush(MOCK_ICAL_DATA);

      // flush() (not tick(0)) because requestEvents$ awaits loadIcalModule(),
      // a dynamic import('ical.js') whose promise chain isn't guaranteed to
      // resolve in a single microtask drain on first invocation.
      flush();
      expect(result).toEqual([
        jasmine.objectContaining({
          id: 'test-event-1',
          title: 'Test Event',
        }),
      ]);
    }));

    it('should return empty array for disabled web app provider in browser', fakeAsync(() => {
      const mockProvider = createMockProvider({
        isDisabledForWebApp: true,
      });

      let result: CalendarIntegrationEvent[] | undefined;
      const sub = service.requestEvents$(mockProvider).subscribe((events) => {
        result = events;
      });
      subscriptions.push(sub);

      flush();

      expect(result).toEqual([]);
      httpMock.expectNone(mockProvider.icalUrl);
    }));

    it('should handle parse errors gracefully', fakeAsync(() => {
      const mockProvider = createMockProvider();

      const sub = service.requestEvents$(mockProvider).subscribe(() => {
        // Subscribe to trigger the request
      });
      subscriptions.push(sub);

      const req = httpMock.expectOne(mockProvider.icalUrl);
      req.flush('INVALID ICAL DATA');

      flush();
      // Should not throw, might return empty array or parsed result
    }));

    it('should surface a dedicated snack message when the URL returns HTML instead of iCal', fakeAsync(() => {
      const mockProvider = createMockProvider();
      mockSnackService.open.calls.reset();

      let result: unknown;
      const sub = service.requestEvents$(mockProvider).subscribe((val) => {
        result = val;
      });
      subscriptions.push(sub);

      const req = httpMock.expectOne(mockProvider.icalUrl);
      // Simulate Office365 returning an HTML redirect page when the share link is revoked
      req.flush(
        '<html><head><title>Object moved</title></head><body>' +
          '<h2>Object moved to <a href="https://outlook.office365.com/mail/">here</a>.</h2>' +
          '</body></html>',
      );

      flush();
      expect(result).toEqual([]);
      expect(mockSnackService.open).toHaveBeenCalledWith(
        jasmine.objectContaining({
          type: 'ERROR',
          msg: 'F.CALENDARS.S.CAL_PROVIDER_NOT_ICAL',
        }),
      );
    }));

    it('should propagate the typed error and still show the snack when isForwardError=true', fakeAsync(() => {
      const mockProvider = createMockProvider();
      mockSnackService.open.calls.reset();

      let caughtError: unknown;
      const sub = service
        .requestEvents$(mockProvider, Date.now(), Date.now() + 86_400_000, true)
        .subscribe({
          next: () => {},
          error: (err) => {
            caughtError = err;
          },
        });
      subscriptions.push(sub);

      const req = httpMock.expectOne(mockProvider.icalUrl);
      req.flush('<html>not ical</html>');

      flush();
      expect(mockSnackService.open).toHaveBeenCalledWith(
        jasmine.objectContaining({
          msg: 'F.CALENDARS.S.CAL_PROVIDER_NOT_ICAL',
        }),
      );
      // The NotIcalResponseError must propagate unwrapped so upstream consumers
      // can branch on its identity via `instanceof`. A plain-Error wrapper (the
      // prior behaviour via `throw new Error(err)`) would fail this check.
      expect(caughtError).toBeInstanceOf(NotIcalResponseError);
    }));

    it('should not leak the tokenized iCal URL in the error snack', fakeAsync(() => {
      const mockProvider = createMockProvider({
        icalUrl: 'https://calendar.google.com/calendar/ical/SECRET_TOKEN/basic.ics',
      });
      mockSnackService.open.calls.reset();

      const sub = service.requestEvents$(mockProvider).subscribe();
      subscriptions.push(sub);

      const req = httpMock.expectOne(mockProvider.icalUrl);
      req.flush('nope', { status: 401, statusText: 'Unauthorized' });

      flush();
      const snackArg = mockSnackService.open.calls.mostRecent().args[0] as {
        msg: string;
        translateParams?: { errTxt?: string };
      };
      expect(snackArg.msg).toBe('F.CALENDARS.S.CAL_PROVIDER_ERROR');
      expect(snackArg.translateParams?.errTxt).not.toContain('SECRET_TOKEN');
      expect(JSON.stringify(snackArg)).not.toContain('SECRET_TOKEN');
    }));
  });

  describe('_getCombinedRefreshInterval', () => {
    it('should return default interval for empty provider list', () => {
      const interval = (service as any)._getCombinedRefreshInterval([], []);
      expect(interval).toBe(2 * 60 * 60 * 1000);
    });

    it('should return minimum interval from multiple providers', () => {
      const provider1 = createMockProvider({
        id: 'p1',
        isEnabled: true,
        checkUpdatesEvery: 60 * 60 * 1000, // 1 hour
      });

      const provider2 = createMockProvider({
        id: 'p2',
        isEnabled: true,
        checkUpdatesEvery: 30 * 60 * 1000, // 30 minutes
      });

      const interval = (service as any)._getCombinedRefreshInterval(
        [provider1, provider2],
        [],
      );
      expect(interval).toBe(30 * 60 * 1000);
    });

    it('should ignore disabled providers', () => {
      const enabledProvider = createMockProvider({
        id: 'enabled',
        isEnabled: true,
        checkUpdatesEvery: 60 * 60 * 1000, // 1 hour
      });

      const disabledProvider = createMockProvider({
        id: 'disabled',
        isEnabled: false,
        checkUpdatesEvery: 10 * 60 * 1000, // 10 minutes - shorter but disabled
      });

      const interval = (service as any)._getCombinedRefreshInterval(
        [enabledProvider, disabledProvider],
        [],
      );
      expect(interval).toBe(60 * 60 * 1000);
    });

    it('should ignore providers without URL', () => {
      const providerWithUrl = createMockProvider({
        id: 'with-url',
        isEnabled: true,
        icalUrl: 'https://example.com/cal.ics',
        checkUpdatesEvery: 60 * 60 * 1000,
      });

      const providerWithoutUrl = createMockProvider({
        id: 'without-url',
        isEnabled: true,
        icalUrl: '',
        checkUpdatesEvery: 10 * 60 * 1000,
      });

      const interval = (service as any)._getCombinedRefreshInterval(
        [providerWithUrl, providerWithoutUrl],
        [],
      );
      expect(interval).toBe(60 * 60 * 1000);
    });

    it('should use LOCAL_FILE_CHECK_INTERVAL for file:// provider', () => {
      const fileProvider = createMockProvider({
        isEnabled: true,
        icalUrl: 'file:///home/user/calendar.ics',
        checkUpdatesEvery: 2 * 60 * 60 * 1000, // Configured as 2 hours
      });

      const interval = (service as any)._getCombinedRefreshInterval([fileProvider], []);
      expect(interval).toBe(LOCAL_FILE_CHECK_INTERVAL); // Should be 5 minutes
    });
  });

  describe('constructor', () => {
    it('should load skipped events from localStorage on init', () => {
      const skippedIds = ['event-1', 'event-2'];
      const today = getDbDateStr();

      localStorage.setItem(
        'SUP_CALENDER_EVENTS_SKIPPED_TODAY',
        JSON.stringify(skippedIds),
      );
      localStorage.setItem('SUP_CALENDER_EVENTS_LAST_SKIP_DAY', today);

      // Reset TestBed to create a fresh service instance
      TestBed.resetTestingModule();
      TestBed.configureTestingModule({
        imports: [HttpClientTestingModule],
        providers: [
          CalendarIntegrationService,
          provideMockStore({
            selectors: [
              { selector: selectCalendarProviders, value: [] },
              { selector: selectEnabledIssueProviders, value: [] },
              { selector: selectAllCalendarTaskEventIds, value: [] },
            ],
          }),
          { provide: SnackService, useValue: mockSnackService },
          { provide: TaskArchiveService, useValue: mockTaskArchiveService },
        ],
      });

      const newService = TestBed.inject(CalendarIntegrationService);
      expect(newService.skippedEventIds$.getValue()).toEqual(skippedIds);
    });

    it('should not load skipped events from different day', () => {
      const skippedIds = ['event-1', 'event-2'];
      const yesterday = getDbDateStr(Date.now() - 86400000);

      localStorage.setItem(
        'SUP_CALENDER_EVENTS_SKIPPED_TODAY',
        JSON.stringify(skippedIds),
      );
      localStorage.setItem('SUP_CALENDER_EVENTS_LAST_SKIP_DAY', yesterday);

      // Reset TestBed to create a fresh service instance
      TestBed.resetTestingModule();
      TestBed.configureTestingModule({
        imports: [HttpClientTestingModule],
        providers: [
          CalendarIntegrationService,
          provideMockStore({
            selectors: [
              { selector: selectCalendarProviders, value: [] },
              { selector: selectEnabledIssueProviders, value: [] },
              { selector: selectAllCalendarTaskEventIds, value: [] },
            ],
          }),
          { provide: SnackService, useValue: mockSnackService },
          { provide: TaskArchiveService, useValue: mockTaskArchiveService },
        ],
      });

      const newService = TestBed.inject(CalendarIntegrationService);
      expect(newService.skippedEventIds$.getValue()).toEqual([]);
    });

    it('should handle invalid JSON in localStorage gracefully', () => {
      const today = getDbDateStr();

      localStorage.setItem('SUP_CALENDER_EVENTS_SKIPPED_TODAY', 'invalid json');
      localStorage.setItem('SUP_CALENDER_EVENTS_LAST_SKIP_DAY', today);

      // Reset TestBed to create a fresh service instance
      TestBed.resetTestingModule();
      TestBed.configureTestingModule({
        imports: [HttpClientTestingModule],
        providers: [
          CalendarIntegrationService,
          provideMockStore({
            selectors: [
              { selector: selectCalendarProviders, value: [] },
              { selector: selectEnabledIssueProviders, value: [] },
              { selector: selectAllCalendarTaskEventIds, value: [] },
            ],
          }),
          { provide: SnackService, useValue: mockSnackService },
          { provide: TaskArchiveService, useValue: mockTaskArchiveService },
        ],
      });

      expect(() => TestBed.inject(CalendarIntegrationService)).not.toThrow();
    });
  });

  describe('event filtering', () => {
    it('should filter out events already added as tasks', fakeAsync(() => {
      // Reset TestBed for clean isolation
      TestBed.resetTestingModule();
      localStorage.clear();
      TestBed.configureTestingModule({
        imports: [HttpClientTestingModule],
        providers: [
          CalendarIntegrationService,
          provideMockStore({
            selectors: [
              { selector: selectCalendarProviders, value: [] },
              { selector: selectEnabledIssueProviders, value: [] },
              { selector: selectAllCalendarTaskEventIds, value: ['test-event-1'] },
            ],
          }),
          { provide: SnackService, useValue: mockSnackService },
          { provide: TaskArchiveService, useValue: mockTaskArchiveService },
        ],
      });

      const freshService = TestBed.inject(CalendarIntegrationService);
      const freshStore = TestBed.inject(MockStore);
      const freshHttpMock = TestBed.inject(HttpTestingController);

      const mockProvider = createMockProvider();
      freshStore.overrideSelector(selectCalendarProviders, [mockProvider]);
      freshStore.refreshState();

      let lastValue: ScheduleCalendarMapEntry[] = [];
      const sub = freshService.calendarEvents$.subscribe((val) => {
        lastValue = val;
      });

      tick(0);
      const req = freshHttpMock.expectOne(mockProvider.icalUrl);
      req.flush(MOCK_ICAL_DATA);

      tick(100);
      freshStore.refreshState();
      tick(100);

      // The event with ID 'test-event-1' should be filtered out
      expect(lastValue).toEqual([{ items: [] }]);

      sub.unsubscribe();
      discardPeriodicTasks();
    }));

    it('should filter out skipped events', fakeAsync(() => {
      const mockProvider = createMockProvider();
      store.overrideSelector(selectCalendarProviders, [mockProvider]);
      store.refreshState();

      // Skip an event first
      service.skipCalendarEvent({
        id: 'test-event-1',
        calProviderId: 'test-provider',
        issueProviderKey: 'ICAL',
        title: 'Test Event',
        start: Date.now(),
        duration: 3600000,
      });

      let lastValue: ScheduleCalendarMapEntry[] = [];
      const sub = service.calendarEvents$.subscribe((val) => {
        lastValue = val;
      });
      subscriptions.push(sub);

      tick(0);
      const req = httpMock.expectOne(mockProvider.icalUrl);
      req.flush(MOCK_ICAL_DATA);

      tick(100);
      store.refreshState();
      tick(100);

      // Skipped event should be filtered
      expect(lastValue).toEqual([{ items: [] }]);

      discardPeriodicTasks();
    }));

    it('should update filtered events when skippedEventIds$ changes', fakeAsync(() => {
      const mockProvider = createMockProvider();
      store.overrideSelector(selectCalendarProviders, [mockProvider]);
      store.refreshState();

      const emissions: any[] = [];
      const sub = service.calendarEvents$.subscribe((val) => {
        emissions.push(val);
      });
      subscriptions.push(sub);

      tick(0);
      const req = httpMock.expectOne(mockProvider.icalUrl);
      req.flush(MOCK_ICAL_DATA);

      tick(100);

      const emissionsBeforeSkip = emissions.length;

      // Skip an event - should trigger new emission
      service.skipCalendarEvent({
        id: 'new-skip-event',
        calProviderId: 'test-provider',
        issueProviderKey: 'ICAL',
        title: 'New Skip Event',
        start: Date.now(),
        duration: 3600000,
      });

      tick(100);

      // Should have more emissions after skipping
      expect(emissions.length).toBeGreaterThanOrEqual(emissionsBeforeSkip);

      discardPeriodicTasks();
    }));
  });

  // Repro for https://github.com/super-productivity/super-productivity/issues/7971
  //
  // Flow: a calendar event is imported as a task, completed before its due day, then
  // moved to the archive by "Finish Day". The archived task leaves the live NgRx task
  // state, so `selectAllCalendarTaskEventIds` (built from `selectAllTasks`, active tasks
  // only) no longer lists its event id. The schedule/planner view filter relied solely on
  // that selector, so the event re-surfaced as a "not yet added" entry the next day.
  //
  // The fix also feeds archived calendar task event ids (read from the synced archive via
  // `TaskArchiveService.load()`) into the same view filter, so the event stays hidden.
  describe('BUG #7971: archived calendar task must stay hidden from the schedule', () => {
    // Builds a TaskArchiveService.load() result from a list of archived tasks.
    const archiveOf = (
      tasks: Array<{ id: string; issueId: string; issueType: string }>,
    ): Promise<{ ids: string[]; entities: Record<string, unknown> }> =>
      Promise.resolve({
        ids: tasks.map((t) => t.id),
        entities: Object.fromEntries(tasks.map((t) => [t.id, { ...t, isDone: true }])),
      });

    // Subscribes to calendarEvents$ for one provider, flushes a single iCal fetch and
    // returns the event ids that survive the view filter. Must run inside fakeAsync.
    const fetchVisibleEventIds = (activeIds: string[], icalData: string): string[] => {
      TestBed.resetTestingModule();
      localStorage.clear();
      TestBed.configureTestingModule({
        imports: [HttpClientTestingModule],
        providers: [
          CalendarIntegrationService,
          provideMockStore({
            selectors: [
              { selector: selectCalendarProviders, value: [] },
              { selector: selectEnabledIssueProviders, value: [] },
              { selector: selectAllCalendarTaskEventIds, value: activeIds },
            ],
          }),
          { provide: SnackService, useValue: mockSnackService },
          { provide: TaskArchiveService, useValue: mockTaskArchiveService },
        ],
      });

      const freshService = TestBed.inject(CalendarIntegrationService);
      const freshStore = TestBed.inject(MockStore);
      const freshHttpMock = TestBed.inject(HttpTestingController);

      const mockProvider = createMockProvider();
      freshStore.overrideSelector(selectCalendarProviders, [mockProvider]);
      freshStore.refreshState();

      let lastValue: ScheduleCalendarMapEntry[] = [];
      const sub = freshService.calendarEvents$.subscribe((val) => (lastValue = val));

      tick(0);
      freshHttpMock.expectOne(mockProvider.icalUrl).flush(icalData);
      tick(100);
      flushMicrotasks();
      freshStore.refreshState();
      tick(100);
      flushMicrotasks();

      sub.unsubscribe();
      discardPeriodicTasks();
      return lastValue.flatMap((entry) => entry.items.map((item) => item.id));
    };

    it('hides an event whose only linked task lives in the archive', fakeAsync(() => {
      // 'test-event-1' is the UID of MOCK_ICAL_DATA's event; the archived calendar task
      // points back to it. No active task — it was moved to the archive by "Finish Day".
      mockTaskArchiveService.load.and.returnValue(
        archiveOf([{ id: 'archivedTask1', issueId: 'test-event-1', issueType: 'ICAL' }]),
      );

      expect(fetchVisibleEventIds([], MOCK_ICAL_DATA)).not.toContain('test-event-1');
    }));

    it('still shows an event whose matching archived task is NOT a calendar task', fakeAsync(() => {
      // A non-calendar archived task (e.g. a GitHub issue) sharing the id must not
      // suppress the calendar event — isCalendarIssueTask gates the contribution.
      mockTaskArchiveService.load.and.returnValue(
        archiveOf([{ id: 'ghTask1', issueId: 'test-event-1', issueType: 'GITHUB' }]),
      );

      expect(fetchVisibleEventIds([], MOCK_ICAL_DATA)).toContain('test-event-1');
    }));

    it('still shows an event whose id matches no archived calendar task', fakeAsync(() => {
      // The archive holds a different event id (e.g. a past occurrence); today's event
      // must remain visible — guards against over-filtering recurring/independent events.
      mockTaskArchiveService.load.and.returnValue(
        archiveOf([{ id: 'archivedTask1', issueId: 'test-event-1', issueType: 'ICAL' }]),
      );

      expect(fetchVisibleEventIds([], MOCK_ICAL_DATA_2)).toContain('test-event-2');
    }));

    it('keeps the event hidden across the active → archived transition (no flash)', fakeAsync(() => {
      TestBed.resetTestingModule();
      localStorage.clear();
      // Phase 1: the task is still live (active selector lists its id), archive empty.
      mockTaskArchiveService.load.and.callFake(emptyArchive);

      TestBed.configureTestingModule({
        imports: [HttpClientTestingModule],
        providers: [
          CalendarIntegrationService,
          provideMockStore({
            selectors: [
              { selector: selectCalendarProviders, value: [] },
              { selector: selectEnabledIssueProviders, value: [] },
              { selector: selectAllCalendarTaskEventIds, value: ['test-event-1'] },
            ],
          }),
          { provide: SnackService, useValue: mockSnackService },
          { provide: TaskArchiveService, useValue: mockTaskArchiveService },
        ],
      });

      const freshService = TestBed.inject(CalendarIntegrationService);
      const freshStore = TestBed.inject(MockStore);
      const freshHttpMock = TestBed.inject(HttpTestingController);

      const mockProvider = createMockProvider();
      freshStore.overrideSelector(selectCalendarProviders, [mockProvider]);
      freshStore.refreshState();

      const emittedIdLists: string[][] = [];
      const sub = freshService.calendarEvents$.subscribe((val) =>
        emittedIdLists.push(val.flatMap((e) => e.items.map((i) => i.id))),
      );

      tick(0);
      freshHttpMock.expectOne(mockProvider.icalUrl).flush(MOCK_ICAL_DATA);
      tick(100);
      flushMicrotasks();

      // Phase 2: "Finish Day" archives the task → it leaves the active set and lands in
      // the archive in the same beat.
      mockTaskArchiveService.load.and.returnValue(
        archiveOf([{ id: 'archivedTask1', issueId: 'test-event-1', issueType: 'ICAL' }]),
      );
      freshStore.overrideSelector(selectAllCalendarTaskEventIds, []);
      freshStore.refreshState();
      tick(100);
      flushMicrotasks();

      // The event must never surface in ANY emission across the transition.
      expect(emittedIdLists.length).toBeGreaterThan(0);
      emittedIdLists.forEach((ids) => expect(ids).not.toContain('test-event-1'));

      sub.unsubscribe();
      discardPeriodicTasks();
    }));
  });

  describe('multiple providers', () => {
    it('should fetch from multiple providers in parallel', fakeAsync(() => {
      const provider1 = createMockProvider({
        id: 'provider-1',
        icalUrl: 'https://provider1.com/calendar.ics',
      });
      const provider2 = createMockProvider({
        id: 'provider-2',
        icalUrl: 'https://provider2.com/calendar.ics',
      });

      store.overrideSelector(selectCalendarProviders, [provider1, provider2]);
      store.refreshState();

      const sub = service.calendarEvents$.subscribe(() => {});
      subscriptions.push(sub);

      tick(0);

      // Both providers should have requests
      const req1 = httpMock.expectOne(provider1.icalUrl);
      const req2 = httpMock.expectOne(provider2.icalUrl);

      req1.flush(MOCK_ICAL_DATA);
      req2.flush(MOCK_ICAL_DATA_2);

      discardPeriodicTasks();
    }));

    it('should fall back to cache when one provider errors', fakeAsync(() => {
      // Set up cache with data for provider-1
      const cachedData = [
        {
          items: [
            {
              id: 'cached-event-provider-1',
              calProviderId: 'provider-1',
              issueProviderKey: 'ICAL',
              title: 'Cached Event',
              start: Date.now() + 60000,
              duration: 3600000,
            },
          ],
        },
      ];
      localStorage.setItem('SUP_CAL_EVENTS_CACHE', JSON.stringify(cachedData));

      // Reset TestBed to pick up cache
      TestBed.resetTestingModule();
      TestBed.configureTestingModule({
        imports: [HttpClientTestingModule],
        providers: [
          CalendarIntegrationService,
          provideMockStore({
            selectors: [
              { selector: selectCalendarProviders, value: [] },
              { selector: selectEnabledIssueProviders, value: [] },
              { selector: selectAllCalendarTaskEventIds, value: [] },
            ],
          }),
          { provide: SnackService, useValue: mockSnackService },
          { provide: TaskArchiveService, useValue: mockTaskArchiveService },
        ],
      });

      const freshService = TestBed.inject(CalendarIntegrationService);
      const freshStore = TestBed.inject(MockStore);
      const freshHttpMock = TestBed.inject(HttpTestingController);

      const provider1 = createMockProvider({
        id: 'provider-1',
        icalUrl: 'https://provider1.com/calendar.ics',
      });
      const provider2 = createMockProvider({
        id: 'provider-2',
        icalUrl: 'https://provider2.com/calendar.ics',
      });

      freshStore.overrideSelector(selectCalendarProviders, [provider1, provider2]);
      freshStore.refreshState();

      let lastValue: any;
      const sub = freshService.calendarEvents$.subscribe((val) => {
        lastValue = val;
      });

      tick(0);

      const req1 = freshHttpMock.expectOne(provider1.icalUrl);
      const req2 = freshHttpMock.expectOne(provider2.icalUrl);

      // Provider 1 errors
      req1.error(new ProgressEvent('error'));
      // Provider 2 succeeds
      req2.flush(MOCK_ICAL_DATA_2);

      tick(100);
      freshStore.refreshState();
      tick(100);

      // Should have received data (either from cache fallback or provider 2)
      expect(lastValue).toBeDefined();

      sub.unsubscribe();
      discardPeriodicTasks();
    }));

    it('should handle all providers failing gracefully', fakeAsync(() => {
      const provider1 = createMockProvider({
        id: 'provider-1',
        icalUrl: 'https://provider1.com/calendar.ics',
      });
      const provider2 = createMockProvider({
        id: 'provider-2',
        icalUrl: 'https://provider2.com/calendar.ics',
      });

      store.overrideSelector(selectCalendarProviders, [provider1, provider2]);
      store.refreshState();

      let lastValue: any;
      let errorOccurred = false;
      const sub = service.calendarEvents$.subscribe({
        next: (val) => {
          lastValue = val;
        },
        error: () => {
          errorOccurred = true;
        },
      });
      subscriptions.push(sub);

      tick(0);

      const req1 = httpMock.expectOne(provider1.icalUrl);
      const req2 = httpMock.expectOne(provider2.icalUrl);

      req1.error(new ProgressEvent('error'));
      req2.error(new ProgressEvent('error'));

      tick(100);

      // Should not error, should emit empty or cached data
      expect(errorOccurred).toBe(false);
      expect(lastValue).toEqual([{ items: [] }, { items: [] }]);

      discardPeriodicTasks();
    }));
  });

  describe('timer behavior', () => {
    it('should refresh data at configured interval', fakeAsync(() => {
      const interval = 60000; // 1 minute
      const mockProvider = createMockProvider({
        checkUpdatesEvery: interval,
      });

      store.overrideSelector(selectCalendarProviders, [mockProvider]);
      store.refreshState();

      const sub = service.calendarEvents$.subscribe(() => {});
      subscriptions.push(sub);

      // Initial request
      tick(0);
      const req1 = httpMock.expectOne(mockProvider.icalUrl);
      req1.flush(MOCK_ICAL_DATA);

      // Wait for interval
      tick(interval);
      const req2 = httpMock.expectOne(mockProvider.icalUrl);
      req2.flush(MOCK_ICAL_DATA);

      // Wait for another interval
      tick(interval);
      const req3 = httpMock.expectOne(mockProvider.icalUrl);
      req3.flush(MOCK_ICAL_DATA);

      discardPeriodicTasks();
    }));

    it('should use shortest interval among all providers', fakeAsync(() => {
      const provider1 = createMockProvider({
        id: 'slow-provider',
        icalUrl: 'https://slow.com/calendar.ics',
        checkUpdatesEvery: 120000, // 2 minutes
      });
      const provider2 = createMockProvider({
        id: 'fast-provider',
        icalUrl: 'https://fast.com/calendar.ics',
        checkUpdatesEvery: 60000, // 1 minute
      });

      store.overrideSelector(selectCalendarProviders, [provider1, provider2]);
      store.refreshState();

      const sub = service.calendarEvents$.subscribe(() => {});
      subscriptions.push(sub);

      // Initial request
      tick(0);
      httpMock.expectOne(provider1.icalUrl).flush(MOCK_ICAL_DATA);
      httpMock.expectOne(provider2.icalUrl).flush(MOCK_ICAL_DATA_2);

      // Wait for shortest interval (1 minute)
      tick(60000);

      // Both should refresh at the shortest interval
      httpMock.expectOne(provider1.icalUrl).flush(MOCK_ICAL_DATA);
      httpMock.expectOne(provider2.icalUrl).flush(MOCK_ICAL_DATA_2);

      discardPeriodicTasks();
    }));

    it('should not make requests before interval elapses', fakeAsync(() => {
      const interval = 60000;
      const mockProvider = createMockProvider({
        checkUpdatesEvery: interval,
      });

      store.overrideSelector(selectCalendarProviders, [mockProvider]);
      store.refreshState();

      const sub = service.calendarEvents$.subscribe(() => {});
      subscriptions.push(sub);

      tick(0);
      const req = httpMock.expectOne(mockProvider.icalUrl);
      req.flush(MOCK_ICAL_DATA);

      // Wait for less than interval
      tick(30000);

      // Should not have any pending requests
      httpMock.expectNone(mockProvider.icalUrl);

      discardPeriodicTasks();
    }));
  });

  describe('cache validation', () => {
    it('should handle corrupted cache data gracefully', fakeAsync(() => {
      localStorage.setItem('SUP_CAL_EVENTS_CACHE', 'not valid json');

      TestBed.resetTestingModule();
      TestBed.configureTestingModule({
        imports: [HttpClientTestingModule],
        providers: [
          CalendarIntegrationService,
          provideMockStore({
            selectors: [
              { selector: selectCalendarProviders, value: [] },
              { selector: selectEnabledIssueProviders, value: [] },
              { selector: selectAllCalendarTaskEventIds, value: [] },
            ],
          }),
          { provide: SnackService, useValue: mockSnackService },
          { provide: TaskArchiveService, useValue: mockTaskArchiveService },
        ],
      });

      expect(() => TestBed.inject(CalendarIntegrationService)).not.toThrow();
      discardPeriodicTasks();
    }));

    it('should handle null cache gracefully', fakeAsync(() => {
      localStorage.setItem('SUP_CAL_EVENTS_CACHE', 'null');

      TestBed.resetTestingModule();
      TestBed.configureTestingModule({
        imports: [HttpClientTestingModule],
        providers: [
          CalendarIntegrationService,
          provideMockStore({
            selectors: [
              { selector: selectCalendarProviders, value: [] },
              { selector: selectEnabledIssueProviders, value: [] },
              { selector: selectAllCalendarTaskEventIds, value: [] },
            ],
          }),
          { provide: SnackService, useValue: mockSnackService },
          { provide: TaskArchiveService, useValue: mockTaskArchiveService },
        ],
      });

      const freshService = TestBed.inject(CalendarIntegrationService);
      let emittedValue: unknown;
      const sub = freshService.calendarEvents$.pipe(take(1)).subscribe((val) => {
        emittedValue = val;
      });

      tick(0);
      expect(emittedValue).toEqual([]);
      sub.unsubscribe();
      discardPeriodicTasks();
    }));

    it('should handle cache with missing items property', fakeAsync(() => {
      const malformedCache = [{ notItems: [] }];
      localStorage.setItem('SUP_CAL_EVENTS_CACHE', JSON.stringify(malformedCache));

      TestBed.resetTestingModule();
      TestBed.configureTestingModule({
        imports: [HttpClientTestingModule],
        providers: [
          CalendarIntegrationService,
          provideMockStore({
            selectors: [
              { selector: selectCalendarProviders, value: [] },
              { selector: selectEnabledIssueProviders, value: [] },
              { selector: selectAllCalendarTaskEventIds, value: [] },
            ],
          }),
          { provide: SnackService, useValue: mockSnackService },
          { provide: TaskArchiveService, useValue: mockTaskArchiveService },
        ],
      });

      // Should not throw when accessing cache
      expect(() => TestBed.inject(CalendarIntegrationService)).not.toThrow();
      discardPeriodicTasks();
    }));
  });

  describe('requestEventsForSchedule$', () => {
    it('should request events from now to one month ahead', fakeAsync(() => {
      const mockProvider = createMockProvider();

      const sub = service.requestEventsForSchedule$(mockProvider).subscribe(() => {});
      subscriptions.push(sub);

      const req = httpMock.expectOne(mockProvider.icalUrl);
      req.flush(MOCK_ICAL_DATA);

      tick(0);
    }));

    it('should forward errors when isForwardError is true', fakeAsync(() => {
      const mockProvider = createMockProvider();

      let errorThrown = false;
      const sub = service.requestEventsForSchedule$(mockProvider, true).subscribe({
        error: () => {
          errorThrown = true;
        },
      });
      subscriptions.push(sub);

      const req = httpMock.expectOne(mockProvider.icalUrl);
      req.error(new ProgressEvent('error'));

      tick(0);
      expect(errorThrown).toBe(true);
    }));

    it('should not forward errors when isForwardError is false', fakeAsync(() => {
      const mockProvider = createMockProvider();

      let errorThrown = false;
      let result: unknown;
      const sub = service.requestEventsForSchedule$(mockProvider, false).subscribe({
        next: (val) => {
          result = val;
        },
        error: () => {
          errorThrown = true;
        },
      });
      subscriptions.push(sub);

      const req = httpMock.expectOne(mockProvider.icalUrl);
      req.error(new ProgressEvent('error'));

      tick(0);
      expect(errorThrown).toBe(false);
      expect(result).toEqual([]);
    }));
  });

  describe('edge cases', () => {
    it('should handle provider with undefined icalUrl', () => {
      const provider = createMockProvider({
        icalUrl: undefined as unknown as string,
      });

      const interval = (service as any)._getCombinedRefreshInterval([provider], []);
      expect(interval).toBe(2 * 60 * 60 * 1000); // Default interval
    });

    it('should handle provider with null icalUrl', () => {
      const provider = createMockProvider({
        icalUrl: null as unknown as string,
      });

      const interval = (service as any)._getCombinedRefreshInterval([provider], []);
      expect(interval).toBe(2 * 60 * 60 * 1000); // Default interval
    });

    it('should handle very short check interval', fakeAsync(() => {
      const mockProvider = createMockProvider({
        checkUpdatesEvery: 1000, // 1 second
      });

      store.overrideSelector(selectCalendarProviders, [mockProvider]);
      store.refreshState();

      const sub = service.calendarEvents$.subscribe(() => {});
      subscriptions.push(sub);

      tick(0);
      httpMock.expectOne(mockProvider.icalUrl).flush(MOCK_ICAL_DATA);

      // Multiple rapid refreshes
      for (let i = 0; i < 3; i++) {
        tick(1000);
        httpMock.expectOne(mockProvider.icalUrl).flush(MOCK_ICAL_DATA);
      }

      discardPeriodicTasks();
    }));

    it('should handle skipCalendarEvent with empty string id', () => {
      const event = {
        id: '',
        calProviderId: 'test-provider',
        issueProviderKey: 'ICAL',
        title: 'Test Event',
        start: Date.now(),
        duration: 3600000,
      };

      const beforeLength = service.skippedEventIds$.getValue().length;
      service.skipCalendarEvent(event);
      const afterLength = service.skippedEventIds$.getValue().length;

      // Should not add empty string
      expect(afterLength).toBe(beforeLength);
    });

    it('should handle events at exactly current time', fakeAsync(() => {
      const now = Date.now();
      const cachedProvider = createMockProvider({ id: 'provider-1' });
      const cachedData = [
        {
          items: [
            {
              id: 'current-event',
              calProviderId: 'provider-1',
              issueProviderKey: 'ICAL',
              title: 'Current Event',
              start: now,
              duration: 3600000,
            },
          ],
        },
      ];
      localStorage.setItem('SUP_CAL_EVENTS_CACHE', JSON.stringify(cachedData));

      TestBed.resetTestingModule();
      TestBed.configureTestingModule({
        imports: [HttpClientTestingModule],
        providers: [
          CalendarIntegrationService,
          provideMockStore({
            selectors: [
              { selector: selectCalendarProviders, value: [cachedProvider] },
              { selector: selectEnabledIssueProviders, value: [] },
              { selector: selectAllCalendarTaskEventIds, value: [] },
            ],
          }),
          { provide: SnackService, useValue: mockSnackService },
          { provide: TaskArchiveService, useValue: mockTaskArchiveService },
        ],
      });

      const freshService = TestBed.inject(CalendarIntegrationService);

      let emittedValue: any;
      const sub = freshService.calendarEvents$.pipe(take(1)).subscribe((val) => {
        emittedValue = val;
      });

      tick(0);
      // Current event should be included (start + duration >= now)
      expect(emittedValue[0].items.length).toBe(1);
      sub.unsubscribe();
      discardPeriodicTasks();
    }));
  });

  describe('performance', () => {
    it('should not make duplicate requests for same provider', fakeAsync(() => {
      const mockProvider = createMockProvider();
      store.overrideSelector(selectCalendarProviders, [mockProvider]);
      store.refreshState();

      // Multiple rapid subscriptions
      const sub1 = service.calendarEvents$.subscribe(() => {});
      const sub2 = service.calendarEvents$.subscribe(() => {});
      const sub3 = service.calendarEvents$.subscribe(() => {});
      subscriptions.push(sub1, sub2, sub3);

      tick(0);

      // Should only be ONE request thanks to shareReplay
      const reqs = httpMock.match(mockProvider.icalUrl);
      expect(reqs.length).toBe(1);
      reqs[0].flush(MOCK_ICAL_DATA);

      discardPeriodicTasks();
    }));

    it('should handle rapid provider changes efficiently', fakeAsync(() => {
      const provider1 = createMockProvider({
        id: 'p1',
        icalUrl: 'https://p1.com/cal.ics',
      });
      const provider2 = createMockProvider({
        id: 'p2',
        icalUrl: 'https://p2.com/cal.ics',
      });
      const provider3 = createMockProvider({
        id: 'p3',
        icalUrl: 'https://p3.com/cal.ics',
      });

      const sub = service.calendarEvents$.subscribe(() => {});
      subscriptions.push(sub);

      // Rapid provider changes
      store.overrideSelector(selectCalendarProviders, [provider1]);
      store.refreshState();
      tick(0);

      store.overrideSelector(selectCalendarProviders, [provider2]);
      store.refreshState();
      tick(0);

      store.overrideSelector(selectCalendarProviders, [provider3]);
      store.refreshState();
      tick(0);

      // Only the last provider should have a pending request (switchMap cancels previous)
      // Note: Due to timing, we might see requests for earlier providers
      const req = httpMock.expectOne(provider3.icalUrl);
      req.flush(MOCK_ICAL_DATA);

      discardPeriodicTasks();
    }));
  });

  // Regression guard for #7238: dueWithTime was dropped during the PluginSearchResult →
  // CalendarIntegrationEvent conversion, causing tasks created from plugin calendar events
  // with a precise start time to fall back to dueDay instead of addAndSchedule().
  describe('_fetchPluginCalendarEvents (plugin → event mapping)', () => {
    it('should propagate dueWithTime from PluginSearchResult to CalendarIntegrationEvent', async () => {
      const mockPluginProvider = {
        id: 'plugin-provider-id',
        issueProviderKey: 'plugin:my-calendar',
        pluginConfig: { apiKey: 'x' },
      } as unknown as IssueProviderPluginType;

      const pluginResults = [
        {
          id: 'evt-with-time',
          title: 'Timed event',
          start: 1701700000000,
          dueWithTime: 1701700000000,
          duration: 3600000,
        },
        {
          id: 'evt-without-time',
          title: 'All-day event',
          start: 1701700000000,
          duration: 0,
          isAllDay: true,
        },
        {
          // Must be filtered out (no start)
          id: 'evt-no-start',
          title: 'No start',
        },
      ];

      const mockRegistry = {
        getProvider: jasmine.createSpy('getProvider').and.returnValue({
          definition: {
            getNewIssuesForBacklog: jasmine
              .createSpy('getNewIssuesForBacklog')
              .and.returnValue(Promise.resolve(pluginResults)),
            getHeaders: jasmine.createSpy('getHeaders').and.returnValue({}),
          },
          allowPrivateNetwork: false,
        }),
      };
      const mockPluginHttp = {
        createHttpHelper: jasmine.createSpy('createHttpHelper').and.returnValue({}),
      };

      TestBed.resetTestingModule();
      TestBed.configureTestingModule({
        imports: [HttpClientTestingModule],
        providers: [
          CalendarIntegrationService,
          provideMockStore({
            selectors: [
              { selector: selectCalendarProviders, value: [] },
              { selector: selectEnabledIssueProviders, value: [] },
              { selector: selectAllCalendarTaskEventIds, value: [] },
            ],
          }),
          { provide: SnackService, useValue: mockSnackService },
          { provide: PluginIssueProviderRegistryService, useValue: mockRegistry },
          { provide: PluginHttpService, useValue: mockPluginHttp },
          { provide: TaskArchiveService, useValue: mockTaskArchiveService },
        ],
      });

      const freshService = TestBed.inject(CalendarIntegrationService);
      const events = await (freshService as any)._fetchPluginCalendarEvents(
        mockPluginProvider,
      );

      expect(events.length).toBe(2);
      const timed = events.find((e: any) => e.id === 'evt-with-time');
      const allDay = events.find((e: any) => e.id === 'evt-without-time');
      expect(timed).toEqual(
        jasmine.objectContaining({ id: 'evt-with-time', dueWithTime: 1701700000000 }),
      );
      expect(allDay).toEqual(jasmine.objectContaining({ id: 'evt-without-time' }));
      expect(allDay.dueWithTime).toBeUndefined();
    });
  });

  // Regression: plugin issue-provider calendars (e.g. the Google Calendar plugin) register
  // asynchronously AFTER the issue-provider store is hydrated. calendarEvents$ must react to
  // that registration (via PluginIssueProviderRegistryService.registrationChanges$) and surface
  // the plugin's events WITHOUT needing a re-subscription — otherwise agenda events only appear
  // after navigating away and back to the Today view.
  describe('plugin registering after subscription', () => {
    it('surfaces plugin calendar events once the plugin registers (no re-subscribe)', fakeAsync(() => {
      const PLUGIN_KEY = 'plugin:gcal';
      const pluginProvider = {
        id: 'gcal-provider-id',
        issueProviderKey: PLUGIN_KEY,
        isEnabled: true,
        pluginConfig: {},
      } as unknown as IssueProviderPluginType;

      const twoHoursMs = 2 * 60 * 60 * 1000;
      const futureStart = Date.now() + twoHoursMs;
      const getNewIssuesForBacklog = jasmine
        .createSpy('getNewIssuesForBacklog')
        .and.returnValue(
          Promise.resolve([
            {
              id: 'gcal-evt-1',
              title: 'Standup',
              start: futureStart,
              dueWithTime: futureStart,
              duration: 30 * 60 * 1000,
            },
          ]),
        );
      const mockPluginHttp = {
        createHttpHelper: jasmine.createSpy('createHttpHelper').and.returnValue({}),
      };

      TestBed.resetTestingModule();
      localStorage.clear();
      TestBed.configureTestingModule({
        imports: [HttpClientTestingModule],
        providers: [
          CalendarIntegrationService,
          provideMockStore({
            selectors: [
              { selector: selectCalendarProviders, value: [] },
              // The provider config is already hydrated in the store before the plugin
              // (which supplies `useAgendaView`) has registered.
              { selector: selectEnabledIssueProviders, value: [pluginProvider] },
              { selector: selectAllCalendarTaskEventIds, value: [] },
            ],
          }),
          { provide: SnackService, useValue: mockSnackService },
          { provide: PluginHttpService, useValue: mockPluginHttp },
          { provide: TaskArchiveService, useValue: mockTaskArchiveService },
        ],
      });

      const freshService = TestBed.inject(CalendarIntegrationService);
      const registry = TestBed.inject(PluginIssueProviderRegistryService);

      const emissions: string[][] = [];
      const sub = freshService.calendarEvents$.subscribe((entries) =>
        emissions.push(entries.flatMap((e) => e.items.map((i) => i.id))),
      );

      // Before registration `getUseAgendaView` is false → the provider is not treated as a
      // calendar source and nothing is fetched.
      tick(0);
      flushMicrotasks();
      expect(emissions.flat()).not.toContain('gcal-evt-1');
      expect(getNewIssuesForBacklog).not.toHaveBeenCalled();

      // Plugin finishes loading and registers as an agenda-view calendar provider.
      registry.register({
        pluginId: 'gcal',
        issueProviderKey: PLUGIN_KEY,
        definition: {
          getHeaders: () => ({}),
          getNewIssuesForBacklog,
        } as unknown as IssueProviderPluginDefinition,
        name: 'Google Calendar',
        humanReadableName: 'Google Calendar',
        icon: 'calendar',
        pollIntervalMs: 60000,
        issueStrings: { singular: 'Event', plural: 'Events' },
        useAgendaView: true,
      });

      // Without any re-subscription, the plugin's event must now appear.
      tick(0);
      flushMicrotasks();
      tick(100);
      flushMicrotasks();

      expect(getNewIssuesForBacklog).toHaveBeenCalled();
      expect(emissions[emissions.length - 1]).toContain('gcal-evt-1');

      sub.unsubscribe();
      discardPeriodicTasks();
    }));
  });
});
