import { describe, expect, it } from 'vitest';
import { computeNumberLineLayout, type NumberLinePoint } from './NumberLine';

const fmt = (v: number) => v.toFixed(0);

const points: NumberLinePoint[] = [
  { kind: 'area', label: 'This area', value: 50 },
  { kind: 'national', label: 'National', value: 30 },
  { kind: 'outcode', label: 'SE3', value: 49 },
  { kind: 'sector', label: 'SE3 9', value: 51 },
];

describe('computeNumberLineLayout', () => {
  it('returns null when there is nothing to draw', () => {
    expect(computeNumberLineLayout(points, 0, fmt)).toBeNull();
    expect(computeNumberLineLayout([], 300, fmt)).toBeNull();
  });

  it('places ticks ordered by value', () => {
    const layout = computeNumberLineLayout(points, 300, fmt)!;
    expect(layout).not.toBeNull();
    // Items are sorted by tick position, which mirrors value order.
    const ticks = layout.items.map((i) => i.tickX);
    for (let i = 1; i < ticks.length; i++) {
      expect(ticks[i]).toBeGreaterThanOrEqual(ticks[i - 1]);
    }
    // The smallest value (national, 30) sits left of the largest (sector, 51).
    const national = layout.items.find((i) => i.kind === 'national')!;
    const sector = layout.items.find((i) => i.kind === 'sector')!;
    expect(national.tickX).toBeLessThan(sector.tickX);
  });

  it('maps the lowest value to the left edge and the highest to the right', () => {
    const layout = computeNumberLineLayout(
      [
        { kind: 'national', label: 'N', value: 30 },
        { kind: 'area', label: 'A', value: 50 },
        { kind: 'sector', label: 'S', value: 45 },
      ],
      300,
      fmt
    )!;
    const low = layout.items.find((i) => i.kind === 'national')!; // 30: lowest
    const high = layout.items.find((i) => i.kind === 'area')!; // 50: highest
    expect(low.tickX).toBeCloseTo(layout.plotLeft, 5);
    expect(high.tickX).toBeCloseTo(layout.plotRight, 5);
  });

  it('centres ticks when all values are equal', () => {
    const layout = computeNumberLineLayout(
      [
        { kind: 'area', label: 'A', value: 7 },
        { kind: 'national', label: 'N', value: 7 },
      ],
      300,
      fmt
    )!;
    const centre = (layout.plotLeft + layout.plotRight) / 2;
    for (const item of layout.items) {
      expect(item.tickX).toBeCloseTo(centre, 5);
    }
  });

  it('spreads clustered labels so their boxes never overlap', () => {
    const layout = computeNumberLineLayout(points, 300, fmt)!;
    for (let i = 1; i < layout.items.length; i++) {
      const prev = layout.items[i - 1];
      const cur = layout.items[i];
      // Boxes [labelX ± halfWidth] must not overlap (GAP only adds more margin).
      expect(cur.labelX - cur.halfWidth).toBeGreaterThanOrEqual(
        prev.labelX + prev.halfWidth - 1e-6
      );
    }
  });

  it('keeps labels within the plot bounds', () => {
    const layout = computeNumberLineLayout(points, 300, fmt)!;
    for (const item of layout.items) {
      expect(item.labelX - item.halfWidth).toBeGreaterThanOrEqual(layout.plotLeft - 1e-6);
      expect(item.labelX + item.halfWidth).toBeLessThanOrEqual(layout.plotRight + 1e-6);
    }
  });
});
