/**
 * ChartExport — image-export failure vs not-ready (arch 05 §7 fail-loud).
 *
 * `getImageUrl` (injected by LivePreview) returns `null` ONLY when no view is
 * live — the not-ready case — and rejects on a real rasterize/serialize failure.
 * These two outcomes must land as DIFFERENT toasts: the not-ready one tells the
 * user to wait (no `detail`), the failure one carries diagnostic `detail` so the
 * problem can be reported. Burying the rejection behind the not-ready copy is the
 * regression this guards.
 */

import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { act } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import { useNotificationStore } from '../stores/NotificationStore';
import { usePopoverStore } from '../stores/PopoverStore';
import { useSnippetStore } from '../stores/SnippetStore';
import { useDatasetStore } from '../stores/DatasetStore';
import { ChartExport, type ChartExportProps } from './ChartExport';

(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;

let container: HTMLDivElement;
let root: Root;

beforeEach(() => {
  useNotificationStore.getState().clear();
  usePopoverStore.setState({ openId: null });
  useSnippetStore.getState().reset();
  useDatasetStore.getState().reset();
  // A non-empty draft makes `hasSpec` true so the export trigger is enabled.
  useSnippetStore.setState({ draftText: '{"mark":"point"}' });

  container = document.createElement('div');
  document.body.appendChild(container);
  root = createRoot(container);
});

afterEach(() => {
  act(() => root.unmount());
  container.remove();
  useNotificationStore.getState().clear();
  usePopoverStore.setState({ openId: null });
  useSnippetStore.getState().reset();
  useDatasetStore.getState().reset();
});

/** Mount, open the export popover (portaled to <body>), and click "Download PNG".
 *  `downloadImage` awaits `getImageUrl`, so the caller flushes microtasks after. */
async function mountAndDownloadPng(getImageUrl: ChartExportProps['getImageUrl']) {
  act(() => {
    root.render(<ChartExport chartReady getImageUrl={getImageUrl} />);
  });
  act(() => usePopoverStore.getState().show('chart-export'));
  const pngBtn = Array.from(document.body.querySelectorAll('button')).find(
    (b) => b.textContent?.trim() === 'Download PNG',
  );
  if (!pngBtn) throw new Error('Download PNG button not found — popover did not render');
  await act(async () => {
    pngBtn.dispatchEvent(new MouseEvent('click', { bubbles: true }));
    await Promise.resolve();
  });
}

describe('ChartExport image-export error handling', () => {
  test('a rejecting getImageUrl surfaces the failure with diagnostic detail', async () => {
    await mountAndDownloadPng(vi.fn().mockRejectedValue(new TypeError('tainted canvas')));

    const toasts = useNotificationStore.getState().notifications;
    expect(toasts).toHaveLength(1);
    expect(toasts[0].kind).toBe('error');
    // A real failure carries its diagnostic detail (not the "wait, it's not ready" copy).
    expect(toasts[0].detail).toBe('TypeError: tainted canvas');
    expect(toasts[0].message).not.toMatch(/ready yet/i);
  });

  test('a null getImageUrl reports not-ready, with no diagnostic detail', async () => {
    await mountAndDownloadPng(vi.fn().mockResolvedValue(null));

    const toasts = useNotificationStore.getState().notifications;
    expect(toasts).toHaveLength(1);
    expect(toasts[0].kind).toBe('error');
    expect(toasts[0].message).toMatch(/ready yet/i);
    // Nothing for the user to report — the not-ready case omits detail.
    expect(toasts[0].detail).toBeUndefined();
  });
});
