import { describe, expect, it } from 'vitest';
import {
  unwrapSingleton,
  wrapInConcat,
  wrapInFacet,
  wrapInLayer,
  wrapInRepeat,
} from './spec-transforms';

const unit = () => ({
  $schema: 'https://vega.github.io/schema/vega-lite/v6.json',
  data: { name: 'weather' },
  title: 'Rainfall',
  width: 400,
  height: 300,
  mark: 'bar',
  encoding: {
    x: { field: 'date', type: 'temporal' },
    y: { field: 'precipitation', type: 'quantitative' },
  },
});

describe('wrapInLayer', () => {
  it('moves the view into layer[0] and adds an empty layer', () => {
    const out = wrapInLayer(unit());
    expect(Array.isArray(out.layer)).toBe(true);
    const layers = out.layer as Record<string, unknown>[];
    expect(layers).toHaveLength(2);
    expect(layers[0].mark).toBe('bar');
    expect(layers[0].encoding).toBeDefined();
    expect(layers[1]).toEqual({ mark: 'point', encoding: {} });
  });

  it('keeps data, title, and the shared plotting size on the wrapper', () => {
    const out = wrapInLayer(unit());
    expect(out.data).toEqual({ name: 'weather' });
    expect(out.title).toBe('Rainfall');
    expect(out.width).toBe(400); // layers share one plotting area
    expect(out.height).toBe(300);
    const layers = out.layer as Record<string, unknown>[];
    expect(layers[0]).not.toHaveProperty('width');
  });

  it('does not mutate the input', () => {
    const spec = unit();
    wrapInLayer(spec);
    expect(spec).not.toHaveProperty('layer');
    expect(spec.mark).toBe('bar');
  });
});

describe('wrapInConcat', () => {
  it('wraps into hconcat/vconcat with an empty sibling view', () => {
    expect(wrapInConcat(unit(), 'h')).toHaveProperty('hconcat');
    const out = wrapInConcat(unit(), 'v');
    const views = out.vconcat as Record<string, unknown>[];
    expect(views).toHaveLength(2);
    expect(views[1]).toEqual({ mark: 'point', encoding: {} });
  });

  it('pushes the size down to each concat view (they size independently)', () => {
    const out = wrapInConcat(unit(), 'h');
    expect(out).not.toHaveProperty('width');
    const views = out.hconcat as Record<string, unknown>[];
    expect(views[0].width).toBe(400);
    expect(out.data).toEqual({ name: 'weather' }); // data still shared on top
  });
});

describe('wrapInFacet', () => {
  it('lifts data to the wrapper and holds the view as spec', () => {
    const out = wrapInFacet(unit(), 'weather', 'nominal');
    expect(out.facet).toEqual({ field: 'weather', type: 'nominal' });
    expect(out.data).toEqual({ name: 'weather' });
    const child = out.spec as Record<string, unknown>;
    expect(child.mark).toBe('bar');
    expect(child).not.toHaveProperty('data');
  });
});

describe('wrapInRepeat', () => {
  it('rewires the named channel to the repeat reference', () => {
    const out = wrapInRepeat(unit(), ['precipitation', 'wind'], 'y');
    expect(out.repeat).toEqual(['precipitation', 'wind']);
    const child = out.spec as { encoding: Record<string, Record<string, unknown>> };
    expect(child.encoding.y.field).toEqual({ repeat: 'repeat' });
    expect(child.encoding.x.field).toBe('date'); // other channels untouched
  });

  it('wraps as-is when the channel is absent or unspecified', () => {
    const out = wrapInRepeat(unit(), ['a', 'b'], null);
    const child = out.spec as { encoding: Record<string, Record<string, unknown>> };
    expect(child.encoding.y.field).toBe('precipitation');
  });
});

describe('unwrapSingleton', () => {
  it('collapses only a single-child array composition', () => {
    expect(unwrapSingleton({ layer: [{ mark: 'bar' }] })).toEqual({ mark: 'bar' });
    expect(unwrapSingleton({ hconcat: [{ mark: 'bar' }] })).toEqual({ mark: 'bar' });
    expect(unwrapSingleton({ layer: [{ mark: 'bar' }, { mark: 'line' }] })).toBeNull();
    expect(unwrapSingleton(unit())).toBeNull();
  });

  it('collapses the lone child up, child keys winning', () => {
    const wrapped = {
      data: { name: 'weather' },
      layer: [{ data: { name: 'other' }, mark: 'bar' }],
    };
    const out = unwrapSingleton(wrapped);
    expect(out).toEqual({ data: { name: 'other' }, mark: 'bar' });
    expect(out).not.toHaveProperty('layer');
  });

  it('round-trips a freshly wrapped-then-trimmed layer back to the unit', () => {
    const wrapped = wrapInLayer(unit());
    (wrapped.layer as unknown[]).pop(); // user deletes the placeholder layer
    const out = unwrapSingleton(wrapped)!;
    expect(out.mark).toBe('bar');
    expect(out.width).toBe(400);
    expect(out).not.toHaveProperty('layer');
  });

  it('returns null when there is nothing to collapse', () => {
    expect(unwrapSingleton(unit())).toBeNull();
  });
});
