/**
 * ThemeBuilderModal — structure and store wiring. The draft/save/font logic
 * lives in CustomThemeStore (tested there); these cover the component's seams:
 * the empty state, creation seeded from the active chart theme, the gallery
 * cards, and the Save flow. vega-embed is mocked out (integration-heavy).
 */

import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { act } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import { THEME_PREVIEW_SPECS } from '@core/theme-preview-specs';
import { useAppStore } from '../stores/AppStore';
import { useCustomThemeStore } from '../stores/CustomThemeStore';
import { renderSpec } from '../services/chart-renderer';
import { ThemeBuilderModal } from './ThemeBuilderModal';

vi.mock('../services/chart-renderer', () => ({ renderSpec: vi.fn() }));

const okHandle = () => ({
  destroy() {},
  resize() {},
  toImageURL: () => Promise.resolve(''),
  inspectData: () => null,
  onDataChange: () => () => {},
});

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

let container: HTMLDivElement;
let root: Root;

beforeEach(() => {
  vi.useFakeTimers();
  vi.mocked(renderSpec).mockReset();
  vi.mocked(renderSpec).mockImplementation(() => Promise.resolve(okHandle()));
  useCustomThemeStore.getState().reset();
  useAppStore.getState().setChartTheme('astrolabe');
  container = document.createElement('div');
  document.body.appendChild(container);
  act(() => {
    root = createRoot(container);
  });
});

afterEach(() => {
  act(() => root.unmount());
  container.remove();
  vi.useRealTimers();
});

const renderModal = () => act(() => root.render(<ThemeBuilderModal />));

describe('ThemeBuilderModal', () => {
  test('shows the empty state when there are no themes', () => {
    renderModal();
    expect(container.textContent).toContain('No custom themes yet');
    expect(container.textContent).toContain('Create a theme to start editing.');
  });

  test('New theme creates a copy of the active chart theme and opens the draft', () => {
    renderModal();
    const newButton = [...container.querySelectorAll('button')].find(
      (b) => b.textContent === 'New theme',
    )!;
    act(() => newButton.click());

    const s = useCustomThemeStore.getState();
    expect(s.themes).toHaveLength(1);
    expect(s.themes[0].name).toBe('Astrolabe copy');
    // Seeded from the house config, not empty.
    expect(s.themes[0].config.font).toContain('IBM Plex');

    const nameInput = container.querySelector<HTMLInputElement>('#theme-name')!;
    expect(nameInput.value).toBe('Astrolabe copy');
    // One gallery card per preview spec.
    expect(container.querySelectorAll('figure')).toHaveLength(THEME_PREVIEW_SPECS.length);
  });

  test('a preset selection seeds the copy from that preset', () => {
    useAppStore.getState().setChartTheme('fivethirtyeight');
    renderModal();
    const newButton = [...container.querySelectorAll('button')].find(
      (b) => b.textContent === 'New theme',
    )!;
    act(() => newButton.click());
    expect(useCustomThemeStore.getState().themes[0].name).toBe('FiveThirtyEight copy');
  });

  test('Save is disabled until the draft is dirty, then commits', () => {
    renderModal();
    act(() => {
      useCustomThemeStore.getState().createTheme('Brand', { font: 'Helvetica' });
    });

    const save = () =>
      [...container.querySelectorAll('button')].find((b) => b.textContent === 'Save theme')!;
    expect(save().disabled).toBe(true);

    act(() => useCustomThemeStore.getState().updateDraft({ name: 'Brand 2026' }));
    expect(save().disabled).toBe(false);

    act(() => save().click());
    expect(useCustomThemeStore.getState().themes[0].name).toBe('Brand 2026');
    expect(save().disabled).toBe(true);
  });

  test('Save stays disabled while the config text is invalid JSON', () => {
    renderModal();
    act(() => {
      useCustomThemeStore.getState().createTheme('Brand', {});
    });
    act(() => useCustomThemeStore.getState().updateDraft({ configText: '{oops' }));
    const save = [...container.querySelectorAll('button')].find(
      (b) => b.textContent === 'Save theme',
    )!;
    expect(save.disabled).toBe(true);
    expect(container.textContent).toContain('Invalid JSON');
  });

  test('a gallery card surfaces a render failure instead of blanking silently', async () => {
    vi.mocked(renderSpec).mockRejectedValue(new Error('Vega could not render this config'));
    renderModal();
    act(() => {
      useCustomThemeStore.getState().createTheme('Brand', {});
    });
    // Fire the gallery's render debounce and flush the async render chain.
    await act(async () => {
      await vi.advanceTimersByTimeAsync(400);
    });
    expect(container.textContent).toContain('Vega could not render this config');
    expect(container.querySelector('[role="alert"]')).toBeTruthy();
  });

  test('selecting another theme from the list swaps the draft', () => {
    renderModal();
    act(() => {
      useCustomThemeStore.getState().createTheme('First', {});
      useCustomThemeStore.getState().createTheme('Second', {});
    });
    const firstItem = [...container.querySelectorAll('button')].find(
      (b) => b.textContent === 'First',
    )!;
    act(() => firstItem.click());
    expect(container.querySelector<HTMLInputElement>('#theme-name')!.value).toBe('First');
  });
});
