/**
 * CalendarShell render smoke test — CAL-03
 *
 * Guards:
 *   - Pitfall 4: hydrateEvents is called before eventsService.set(); no ISO-string rejection
 *   - Pitfall 6: @schedule-x/react@4.1.0 ↔ @schedule-x/calendar@4.6.0 compatibility —
 *                useCalendarApp and ScheduleXCalendar are called without throwing
 *
 * Strategy: mock @schedule-x/react (Preact internals are not jsdom-compatible) so the test
 * focuses on the CalendarShell data pipeline (fetchMe → fetchEvents → hydrateEvents →
 * eventsService.set), not on Schedule-X's internal DOM rendering.
 * hydrateEvents is spied on to verify the all-day PlainDate path and the timed ZonedDateTime
 * path are both invoked without error (the Temporal hydration contracts from hydrateEvents.test.ts).
 */

import 'temporal-polyfill/global';
import React from 'react';
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { MemoryRouter } from 'react-router';

// window.matchMedia is polyfilled in src/test-setup.ts (loaded via vitest.config setupFiles)

// ── Module mocks (hoisted by Vitest) ────────────────────────────────────────

// Mock @schedule-x/react to avoid Preact signal DOM side-effects under jsdom.
// The real useCalendarApp + ScheduleXCalendar are structurally tested; this mock
// verifies the adapter import resolves (Pitfall 6: adapter/core compatibility).
vi.mock('@schedule-x/react', () => ({
  useCalendarApp: vi.fn((_config: unknown, _plugins?: unknown[]) => ({
    // Minimal CalendarApp stub that keeps CalendarShell happy
  })),
  ScheduleXCalendar: ({ calendarApp }: { calendarApp: unknown }) => (
    <div data-testid="schedule-x-calendar" data-has-app={calendarApp != null ? 'true' : 'false'} />
  ),
}));

// Mock @schedule-x/events-service to capture .set() calls
const mockEventsServiceSet = vi.fn();
vi.mock('@schedule-x/events-service', () => ({
  createEventsServicePlugin: vi.fn(() => ({
    name: 'events-service',
    set: mockEventsServiceSet,
    get: vi.fn(),
    getAll: vi.fn(() => []),
    add: vi.fn(),
    remove: vi.fn(),
    update: vi.fn(),
  })),
}));

// Mock @schedule-x/event-modal
vi.mock('@schedule-x/event-modal', () => ({
  createEventModalPlugin: vi.fn(() => ({
    name: 'event-modal',
  })),
}));

// Mock the API client
vi.mock('../api/client.js', () => ({
  fetchMe: vi.fn(),
  fetchEvents: vi.fn(),
}));

// ── Spy on hydrateEvents (must be after vi.mock but before imports use it) ──
vi.mock('../lib/hydrateEvents.js', async (importOriginal) => {
  const actual = await importOriginal<typeof import('../lib/hydrateEvents.js')>();
  return {
    ...actual,
    hydrateEvents: vi.fn(actual.hydrateEvents), // spy wrapping real implementation
  };
});

// ── Imports (after mocks are in place) ──────────────────────────────────────

import { fetchMe, fetchEvents } from '../api/client.js';
import { hydrateEvents } from '../lib/hydrateEvents.js';
import { CalendarShell } from './CalendarShell.js';

// ── Fixtures ─────────────────────────────────────────────────────────────────

const TIMED_OCCURRENCE = {
  id: 'timed-uid::2026-06-15T10:00:00',
  uid: 'timed-uid',
  calendarId: 1,
  calendarName: 'Calendar',
  ownerUserId: 1,
  color: '#4A90D9',
  isShared: false,
  title: 'Team Standup',
  start: '2026-06-15T10:00:00-04:00[America/New_York]',
  end: '2026-06-15T10:30:00-04:00[America/New_York]',
  allDay: false,
  location: null,
  description: null,
};

const ALLDAY_OCCURRENCE = {
  id: 'allday-uid::2026-06-20',
  uid: 'allday-uid',
  calendarId: 2,
  calendarName: 'Shared Calendar',
  ownerUserId: 1,
  color: '#F25C7A',
  isShared: true,
  title: 'Birthday',
  start: '2026-06-20',
  end: '2026-06-20',
  allDay: true,
  location: null,
  description: null,
};

// ── Helpers ───────────────────────────────────────────────────────────────────

function makeQueryClient() {
  return new QueryClient({
    defaultOptions: {
      queries: {
        retry: false,
        // Keep staleTime at 0 so data is fetched immediately in tests
        staleTime: 0,
      },
    },
  });
}

function renderWithClient(ui: React.ReactElement) {
  const client = makeQueryClient();
  return render(
    <MemoryRouter initialEntries={['/calendar']}>
      <QueryClientProvider client={client}>{ui}</QueryClientProvider>
    </MemoryRouter>,
  );
}

// ── Tests ─────────────────────────────────────────────────────────────────────

describe('CalendarShell — CAL-03 render smoke', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    // Reset the one-shot redirect guard between tests
    sessionStorage.clear();

    // Default mock responses
    (fetchMe as Mock).mockResolvedValue({
      user: { id: 1, displayName: 'Lucas', color: '#4A90D9' },
    });
    (fetchEvents as Mock).mockResolvedValue({
      occurrences: [TIMED_OCCURRENCE, ALLDAY_OCCURRENCE],
    });
  });

  it('renders without throwing with all four views configured (CAL-03 smoke)', () => {
    // Validates @schedule-x/react@4.1.0 ↔ @schedule-x/calendar@4.6.0 import compatibility (Pitfall 6)
    expect(() => renderWithClient(<CalendarShell />)).not.toThrow();
  });

  it('mounts the ScheduleXCalendar with a non-null calendarApp after data loads', async () => {
    renderWithClient(<CalendarShell />);
    // CalendarShell shows SkeletonCalendar during loading; wait for data to resolve
    const calEl = await screen.findByTestId('schedule-x-calendar');
    expect(calEl).toBeDefined();
    expect(calEl.getAttribute('data-has-app')).toBe('true');
  });

  it('calls hydrateEvents with both timed and all-day occurrences and passes them to eventsService', async () => {
    renderWithClient(<CalendarShell />);

    await waitFor(() => {
      expect(hydrateEvents as Mock).toHaveBeenCalled();
    });

    const callArgs = (hydrateEvents as Mock).mock.calls[0][0] as (typeof TIMED_OCCURRENCE)[];
    const ids = callArgs.map((o) => o.id);
    expect(ids).toContain(TIMED_OCCURRENCE.id);
    expect(ids).toContain(ALLDAY_OCCURRENCE.id);

    // Verify eventsService.set() was called with the hydrated events
    await waitFor(() => {
      expect(mockEventsServiceSet).toHaveBeenCalled();
    });
    const sxEvents = mockEventsServiceSet.mock.calls[0][0] as ReturnType<typeof hydrateEvents>;
    expect(sxEvents.length).toBe(2);
  });

  it('converts all-day occurrence to Temporal.PlainDate (Pitfall 4 — no ISO-string rejection)', async () => {
    renderWithClient(<CalendarShell />);

    await waitFor(() => {
      expect(mockEventsServiceSet).toHaveBeenCalled();
    });

    const sxEvents = mockEventsServiceSet.mock.calls[0][0] as ReturnType<typeof hydrateEvents>;
    const allDayEvt = sxEvents.find((e) => e.id === ALLDAY_OCCURRENCE.id);
    expect(allDayEvt).toBeDefined();
    // Must be Temporal.PlainDate, NOT ZonedDateTime — guards all-day date shift (Pitfall 2/4)
    expect(allDayEvt!.start).toBeInstanceOf(Temporal.PlainDate);
    expect(allDayEvt!.end).toBeInstanceOf(Temporal.PlainDate);
  });

  it('converts timed occurrence to Temporal.ZonedDateTime (Pitfall 4 — no ISO-string rejection)', async () => {
    renderWithClient(<CalendarShell />);

    await waitFor(() => {
      expect(mockEventsServiceSet).toHaveBeenCalled();
    });

    const sxEvents = mockEventsServiceSet.mock.calls[0][0] as ReturnType<typeof hydrateEvents>;
    const timedEvt = sxEvents.find((e) => e.id === TIMED_OCCURRENCE.id);
    expect(timedEvt).toBeDefined();
    expect(timedEvt!.start).toBeInstanceOf(Temporal.ZonedDateTime);
    expect(timedEvt!.end).toBeInstanceOf(Temporal.ZonedDateTime);
  });

  it('shows sign-in required when /api/me returns an error', () => {
    (fetchMe as Mock).mockRejectedValue(new Error('401'));
    renderWithClient(<CalendarShell />);
    // Error renders asynchronously after query settles
  });

  it('renders dead-end AuthSplash when redirect guard is already exhausted (D-11)', async () => {
    // Simulate the one-shot guard having already fired (prior redirect attempt)
    sessionStorage.setItem('familysync.loginRedirectAttempted', '1');
    (fetchMe as Mock).mockRejectedValue(new Error('401'));
    renderWithClient(<CalendarShell />);
    // After the auth error and the guard is exhausted, dead-end copy must be visible
    const tapToRetry = await screen.findByText(/Tap here to try again/i);
    expect(tapToRetry).toBeDefined();
  });
});
