import { describe, expect, it } from 'vitest';
import { extractConfigFromSpec, isJsonObject, mergeConfigIntoSpec } from './spec-config';

describe('mergeConfigIntoSpec', () => {
  const theme = {
    background: 'transparent',
    font: 'IBM Plex Sans',
    axis: { labelColor: '#525252', gridDash: [2, 2] },
  };

  it('adds the config block to a spec without one', () => {
    const spec = { mark: 'bar', data: { values: [] } };
    const out = mergeConfigIntoSpec(spec, theme);
    expect(out.config).toEqual(theme);
    expect(out.mark).toBe('bar');
    expect(spec).not.toHaveProperty('config'); // input untouched
  });

  it('the spec’s existing config wins key-by-key, deep', () => {
    const spec = {
      mark: 'bar',
      config: { font: 'Georgia', axis: { labelColor: 'red' } },
    };
    const out = mergeConfigIntoSpec(spec, theme);
    expect(out.config).toEqual({
      background: 'transparent', // from the theme
      font: 'Georgia', // spec wins
      axis: { labelColor: 'red', gridDash: [2, 2] }, // merged: spec wins inside
    });
  });

  it('arrays are replaced, not merged', () => {
    const spec = { config: { axis: { gridDash: [8] } } };
    const out = mergeConfigIntoSpec(spec, theme) as { config: { axis: { gridDash: number[] } } };
    expect(out.config.axis.gridDash).toEqual([8]);
  });

  it('an empty config into a config-less spec stays config-less', () => {
    expect(mergeConfigIntoSpec({ mark: 'bar' }, {})).toEqual({ mark: 'bar' });
  });

  it('a non-object spec config is replaced by the merge', () => {
    const out = mergeConfigIntoSpec({ config: 'junk' }, theme);
    expect(out.config).toEqual(theme);
  });
});

describe('extractConfigFromSpec', () => {
  it('removes and returns the config block', () => {
    const spec = { mark: 'bar', config: { font: 'Georgia' } };
    const out = extractConfigFromSpec(spec);
    expect(out.config).toEqual({ font: 'Georgia' });
    expect(out.spec).toEqual({ mark: 'bar' });
    expect(spec).toHaveProperty('config'); // input untouched
  });

  it('returns null config when the spec has none', () => {
    const out = extractConfigFromSpec({ mark: 'bar' });
    expect(out.config).toBeNull();
    expect(out.spec).toEqual({ mark: 'bar' });
  });

  it('an empty or non-object config extracts as null but is still removed', () => {
    expect(extractConfigFromSpec({ mark: 'bar', config: {} })).toEqual({
      spec: { mark: 'bar' },
      config: null,
    });
    expect(extractConfigFromSpec({ mark: 'bar', config: 7 })).toEqual({
      spec: { mark: 'bar' },
      config: null,
    });
  });
});

describe('isJsonObject', () => {
  it('accepts plain objects only', () => {
    expect(isJsonObject({})).toBe(true);
    expect(isJsonObject([])).toBe(false);
    expect(isJsonObject(null)).toBe(false);
    expect(isJsonObject('x')).toBe(false);
  });
});
