import { describe, expect, it } from 'vitest';
import {
  CURRENT_THEME_VERSION,
  THEME_FONT_OPTIONS,
  applyFontToConfig,
  collectFontFamilies,
  createCustomTheme,
} from './custom-theme';
import { THEME_PREVIEW_SPECS } from './theme-preview-specs';

describe('createCustomTheme', () => {
  it('stamps version, timestamps, and carries the config through', () => {
    const now = new Date('2026-06-12T10:00:00Z');
    const theme = createCustomTheme({ name: 'Brand', config: { font: 'Georgia' }, now });
    expect(theme.version).toBe(CURRENT_THEME_VERSION);
    expect(theme.name).toBe('Brand');
    expect(theme.config).toEqual({ font: 'Georgia' });
    expect(theme.created).toBe(now.toISOString());
    expect(theme.modified).toBe(now.toISOString());
  });
});

describe('applyFontToConfig', () => {
  it('sets the top-level font on an empty config', () => {
    expect(applyFontToConfig({}, 'Georgia, serif')).toEqual({ font: 'Georgia, serif' });
  });

  it('rewrites every explicit font slot at any depth', () => {
    const config = {
      font: 'Helvetica',
      title: { font: 'Helvetica', subtitleFont: 'Helvetica', fontSize: 16 },
      axis: { labelFont: 'Helvetica', titleFont: 'Helvetica', labelFontSize: 11 },
      axisX: { labelFont: 'Helvetica' },
      legend: { labelFont: 'Helvetica', titleFont: 'Helvetica' },
      header: { labelFont: 'Helvetica', titleFont: 'Helvetica' },
      text: { font: 'Helvetica' },
    };
    const out = applyFontToConfig(config, 'Georgia');
    expect(out).toEqual({
      font: 'Georgia',
      title: { font: 'Georgia', subtitleFont: 'Georgia', fontSize: 16 },
      axis: { labelFont: 'Georgia', titleFont: 'Georgia', labelFontSize: 11 },
      axisX: { labelFont: 'Georgia' },
      legend: { labelFont: 'Georgia', titleFont: 'Georgia' },
      header: { labelFont: 'Georgia', titleFont: 'Georgia' },
      text: { font: 'Georgia' },
    });
  });

  it('leaves non-font properties (including fontSize/fontWeight) untouched', () => {
    const config = {
      background: '#fff',
      title: { fontSize: 16, fontWeight: 600 },
      range: { category: ['#111', '#222'] },
    };
    expect(applyFontToConfig(config, 'Georgia')).toEqual({ ...config, font: 'Georgia' });
  });

  it('does not mutate the input', () => {
    const config = { title: { font: 'Helvetica' } };
    applyFontToConfig(config, 'Georgia');
    expect(config).toEqual({ title: { font: 'Helvetica' } });
  });
});

describe('collectFontFamilies', () => {
  it('collects the top-level font and every *Font slot', () => {
    const config = {
      font: 'Inter',
      axis: { labelFont: 'Spectral', titleFont: 'Spectral' },
      legend: { labelFont: 'Caveat' },
    };
    expect(collectFontFamilies(config)).toEqual(new Set(['Inter', 'Spectral', 'Caveat']));
  });

  it('walks arrays (layered specs)', () => {
    const spec = { layer: [{ mark: { font: 'A' } }, { mark: { font: 'B' } }] };
    expect(collectFontFamilies(spec)).toEqual(new Set(['A', 'B']));
  });

  it('skips data and datasets (rows are never fonts)', () => {
    const spec = {
      data: { values: [{ font: 'NotAFont', x: 1 }] },
      datasets: { d: [{ titleFont: 'AlsoNot' }] },
      config: { font: 'Real' },
    };
    expect(collectFontFamilies(spec)).toEqual(new Set(['Real']));
  });

  it('ignores non-string font values', () => {
    expect(collectFontFamilies({ font: 42, fontSize: 11 })).toEqual(new Set());
  });

  it('returns family stacks verbatim (the render gate loads them as-is)', () => {
    expect(collectFontFamilies({ font: '"Inter", system-ui, sans-serif' })).toEqual(
      new Set(['"Inter", system-ui, sans-serif']),
    );
  });

  it('finds the font a theme applies (round-trip with applyFontToConfig)', () => {
    const applied = applyFontToConfig({ axis: { labelFont: 'Old' } }, 'New');
    expect(collectFontFamilies(applied)).toEqual(new Set(['New']));
  });
});

describe('THEME_FONT_OPTIONS', () => {
  it('offers distinct, non-empty CSS stacks', () => {
    expect(THEME_FONT_OPTIONS.length).toBeGreaterThanOrEqual(4);
    const values = THEME_FONT_OPTIONS.map((f) => f.value);
    const labels = THEME_FONT_OPTIONS.map((f) => f.label);
    expect(new Set(values).size).toBe(values.length);
    expect(new Set(labels).size).toBe(labels.length);
    expect(values.every((v) => v.trim().length > 0)).toBe(true);
  });

  it('every self-hosted family (quoted primary) carries a category fallback', () => {
    // A quoted primary that hasn't loaded must degrade to a sensible system font,
    // so each roster stack lists at least one fallback after the primary.
    for (const { value } of THEME_FONT_OPTIONS.filter((o) => o.value.startsWith('"'))) {
      expect(value).toContain(',');
    }
  });
});

describe('THEME_PREVIEW_SPECS', () => {
  it('every gallery card is a self-contained inline-data spec', () => {
    expect(THEME_PREVIEW_SPECS.length).toBeGreaterThanOrEqual(6);
    for (const card of THEME_PREVIEW_SPECS) {
      expect(card.id).toBeTruthy();
      expect(card.caption).toBeTruthy();
      const data = card.spec.data as { values?: unknown[] };
      expect(Array.isArray(data.values)).toBe(true);
      expect(data.values!.length).toBeGreaterThan(0);
      expect(card.spec.$schema).toContain('vega-lite');
    }
  });

  it('card ids are unique', () => {
    const ids = THEME_PREVIEW_SPECS.map((c) => c.id);
    expect(new Set(ids).size).toBe(ids.length);
  });

  it('exercises every color family the builder controls', () => {
    type ColorDef = { type?: string; scale?: { domainMid?: unknown } };
    const colorOf = (spec: (typeof THEME_PREVIEW_SPECS)[number]['spec']): ColorDef | undefined =>
      (spec.encoding as { color?: ColorDef } | undefined)?.color;

    const colors = THEME_PREVIEW_SPECS.map((c) => colorOf(c.spec));
    // mark.color (a card with no color encoding), categorical, sequential
    // (quantitative, no midpoint), and diverging (quantitative with a midpoint).
    expect(colors.some((c) => c === undefined)).toBe(true);
    expect(colors.some((c) => c?.type === 'nominal')).toBe(true);
    expect(colors.some((c) => c?.type === 'quantitative' && c.scale?.domainMid === undefined)).toBe(
      true,
    );
    expect(colors.some((c) => c?.scale?.domainMid !== undefined)).toBe(true);
  });

  it('uses bare marks so config controls are not shadowed in the preview', () => {
    // A visual mark property hard-coded in a card overrides the same key in the
    // injected config, making the matching Marks control a no-op in the gallery.
    // Mark styling belongs in the config (a theme), never inline here.
    const SHADOWING = [
      'size',
      'filled',
      'point',
      'innerRadius',
      'outerRadius',
      'cornerRadius',
      'cornerRadiusEnd',
      'opacity',
      'interpolate',
      'shape',
      'strokeWidth',
      'padAngle',
      'discreteBandSize',
      'continuousBandSize',
    ];
    for (const card of THEME_PREVIEW_SPECS) {
      const mark: unknown = card.spec.mark;
      if (mark && typeof mark === 'object') {
        for (const key of SHADOWING) {
          expect(mark, `${card.id} hard-codes mark.${key}`).not.toHaveProperty(key);
        }
      }
    }
  });
});
