/**
 * SyncStateToast tests — Task 2 (TDD RED → GREEN)
 *
 * Covers all states from UI-SPEC §SyncStateToast:
 *   - pending: "Syncing…" + spinner role="status"
 *   - done: "Saved" + Check icon, auto-dismiss after 2s, invalidates ['events']
 *   - failed (generic): "Didn't save. Try again." role="alert", persists with dismiss button
 *   - failed (conflict/412): conflict copy, invalidates ['events']
 *   - dead: "Not saved. Check your connection." role="alert"
 *   - refetchInterval active only while pending
 *   - renders nothing when lastSyncedUid is null
 */

import 'temporal-polyfill/global';
import React from 'react';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { render, screen, waitFor, act, fireEvent } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { SyncStateToast } from './SyncStateToast.js';

// ── Module mocks ──────────────────────────────────────────────────────────────
// vi.hoisted() required for variables referenced inside vi.mock() factories (D-03-04-hoisting)

const { mockLastSyncedUid, mockSetLastSyncedUid, mockFetchSyncStatus } = vi.hoisted(() => {
  // Declare with explicit type so later null assignments (line 82+) stay type-safe
  const mockLastSyncedUid: { value: string | null } = { value: 'test-uid-123' };
  return {
    mockLastSyncedUid,
    mockSetLastSyncedUid: vi.fn(),
    mockFetchSyncStatus: vi.fn(),
  };
});

vi.mock('../store/calendarStore.js', () => ({
  useCalendarStore: (selector: (s: Record<string, unknown>) => unknown) => {
    const state = {
      lastSyncedUid: mockLastSyncedUid.value,
      setLastSyncedUid: mockSetLastSyncedUid,
    };
    return selector(state);
  },
}));

vi.mock('../api/client.js', () => ({
  fetchSyncStatus: mockFetchSyncStatus,
}));

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

function makeQueryClient() {
  return new QueryClient({
    defaultOptions: {
      queries: { retry: false },
    },
  });
}

function renderToast(queryClient: QueryClient) {
  return render(
    <QueryClientProvider client={queryClient}>
      <SyncStateToast />
    </QueryClientProvider>,
  );
}

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

describe('SyncStateToast', () => {
  let queryClient: QueryClient;

  beforeEach(() => {
    vi.clearAllMocks();
    queryClient = makeQueryClient();
    mockLastSyncedUid.value = 'test-uid-123';
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  it('renders nothing when lastSyncedUid is null', () => {
    mockLastSyncedUid.value = null;
    const { container } = renderToast(queryClient);
    expect(container.firstChild).toBeNull();
  });

  it('pending: renders "Syncing…" with role="status"', async () => {
    mockFetchSyncStatus.mockResolvedValue({ uid: 'test-uid-123', status: 'pending' });

    renderToast(queryClient);

    await waitFor(
      () => {
        expect(screen.getByRole('status')).toBeInTheDocument();
        expect(screen.getByText('Syncing…')).toBeInTheDocument();
      },
      { timeout: 3000 },
    );
  });

  it('done: renders "Saved"', async () => {
    mockFetchSyncStatus.mockResolvedValue({ uid: 'test-uid-123', status: 'done' });

    renderToast(queryClient);

    await waitFor(
      () => {
        expect(screen.getByText('Saved')).toBeInTheDocument();
      },
      { timeout: 3000 },
    );
  });

  it('done: calls queryClient.invalidateQueries for ["events"]', async () => {
    const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries');
    mockFetchSyncStatus.mockResolvedValue({ uid: 'test-uid-123', status: 'done' });

    renderToast(queryClient);

    await waitFor(
      () => {
        expect(screen.getByText('Saved')).toBeInTheDocument();
      },
      { timeout: 3000 },
    );

    expect(invalidateSpy).toHaveBeenCalledWith(expect.objectContaining({ queryKey: ['events'] }));
  });

  it('done: auto-dismisses after 2s via setLastSyncedUid(null)', async () => {
    vi.useFakeTimers({ shouldAdvanceTime: true });
    mockFetchSyncStatus.mockResolvedValue({ uid: 'test-uid-123', status: 'done' });

    renderToast(queryClient);

    // Wait for the "Saved" state (real timer resolution happens via shouldAdvanceTime)
    await act(async () => {
      await vi.runAllTimersAsync();
    });

    expect(mockSetLastSyncedUid).toHaveBeenCalledWith(null);
  });

  it('failed (generic): renders "Didn\'t save. Try again." with role="alert"', async () => {
    mockFetchSyncStatus.mockResolvedValue({ uid: 'test-uid-123', status: 'failed' });

    renderToast(queryClient);

    await waitFor(
      () => {
        expect(screen.getByRole('alert')).toBeInTheDocument();
        expect(screen.getByText("Didn't save. Try again.")).toBeInTheDocument();
      },
      { timeout: 3000 },
    );
  });

  it('failed (generic): persists and has a dismiss button', async () => {
    mockFetchSyncStatus.mockResolvedValue({ uid: 'test-uid-123', status: 'failed' });

    renderToast(queryClient);

    await waitFor(
      () => {
        expect(screen.getByRole('alert')).toBeInTheDocument();
      },
      { timeout: 3000 },
    );

    // Should have a dismiss button
    const dismissBtn = screen.getByRole('button', { name: /dismiss/i });
    expect(dismissBtn).toBeInTheDocument();
  });

  it('failed (conflict/412): renders conflict copy and invalidates ["events"]', async () => {
    const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries');
    mockFetchSyncStatus.mockResolvedValue({
      uid: 'test-uid-123',
      status: 'failed',
      error: '412 Conflict',
    });

    renderToast(queryClient);

    await waitFor(
      () => {
        expect(
          screen.getByText('This event changed elsewhere — review the latest version'),
        ).toBeInTheDocument();
      },
      { timeout: 3000 },
    );

    expect(invalidateSpy).toHaveBeenCalledWith(expect.objectContaining({ queryKey: ['events'] }));
  });

  it('dead: renders "Not saved. Check your connection." with role="alert"', async () => {
    mockFetchSyncStatus.mockResolvedValue({ uid: 'test-uid-123', status: 'dead' });

    renderToast(queryClient);

    await waitFor(
      () => {
        expect(screen.getByRole('alert')).toBeInTheDocument();
        expect(screen.getByText('Not saved. Check your connection.')).toBeInTheDocument();
      },
      { timeout: 3000 },
    );
  });

  it('refetchInterval is active (3000ms) while pending', async () => {
    let callCount = 0;
    mockFetchSyncStatus.mockImplementation(() => {
      callCount++;
      return Promise.resolve({ uid: 'test-uid-123', status: 'pending' });
    });

    vi.useFakeTimers({ shouldAdvanceTime: true });
    renderToast(queryClient);

    // Wait for first fetch
    await act(async () => {
      await vi.advanceTimersByTimeAsync(100);
    });
    const countAfterFirst = callCount;
    expect(countAfterFirst).toBeGreaterThanOrEqual(1);

    // Advance 3s to trigger refetch
    await act(async () => {
      await vi.advanceTimersByTimeAsync(3100);
    });

    expect(callCount).toBeGreaterThan(countAfterFirst);
  });

  it('dismiss button on failed toast calls setLastSyncedUid(null)', async () => {
    mockFetchSyncStatus.mockResolvedValue({ uid: 'test-uid-123', status: 'failed' });

    renderToast(queryClient);

    await waitFor(
      () => {
        expect(screen.getByRole('alert')).toBeInTheDocument();
      },
      { timeout: 3000 },
    );

    const dismissBtn = screen.getByRole('button', { name: /dismiss/i });
    fireEvent.click(dismissBtn);

    expect(mockSetLastSyncedUid).toHaveBeenCalledWith(null);
  });
});
