import { describe, it, expect } from 'vitest';
import { buildTimelineGeometry, daysUntilLabel, shortDate, splitUpcoming } from './cashflowUtils';

const TIMELINE = [
  { date: '2026-06-10', balance: 500, bills: [] },
  { date: '2026-06-12', balance: 300, bills: [{ id: 1, name: 'Rent', amount: 200 }] },
  { date: '2026-06-15', balance: 300, bills: [], payday: true },
];

describe('buildTimelineGeometry', () => {
  it('returns null for empty or single-point timelines', () => {
    expect(buildTimelineGeometry([], 400, 96)).toBeNull();
    expect(buildTimelineGeometry([TIMELINE[0]], 400, 96)).toBeNull();
    expect(buildTimelineGeometry(null, 400, 96)).toBeNull();
  });

  it('positions points by actual day spacing, not even spacing', () => {
    const geo = buildTimelineGeometry(TIMELINE, 400, 96, 0);
    // 2026-06-12 is 2/5ths of the way from 06-10 to 06-15
    expect(geo.points[1].x).toBeCloseTo(400 * (2 / 5), 5);
    expect(geo.points[0].x).toBe(0);
    expect(geo.points[2].x).toBe(400);
  });

  it('maps balances down the Y axis and includes zero in the domain', () => {
    const geo = buildTimelineGeometry(TIMELINE, 400, 100, 0);
    // domain is [0, 500]: start sits at top, zero at bottom
    expect(geo.points[0].y).toBe(0);
    expect(geo.zeroY).toBe(100);
    expect(geo.points[1].y).toBeCloseTo(100 * (1 - 300 / 500), 5);
  });

  it('handles a projection that goes negative', () => {
    const geo = buildTimelineGeometry([
      { date: '2026-06-10', balance: 100, bills: [] },
      { date: '2026-06-12', balance: -50, bills: [{ id: 1, name: 'Big', amount: 150 }] },
      { date: '2026-06-15', balance: -50, bills: [], payday: true },
    ], 400, 100, 0);
    // zero line sits inside the chart, not on an edge
    expect(geo.zeroY).toBeGreaterThan(0);
    expect(geo.zeroY).toBeLessThan(100);
    // negative balance plots below the zero line
    expect(geo.points[1].y).toBeGreaterThan(geo.zeroY);
  });

  it('builds a step path (H then V segments) and a closed area', () => {
    const geo = buildTimelineGeometry(TIMELINE, 400, 96, 0);
    expect(geo.line).toMatch(/^M [\d.]+ [\d.]+( H [\d.]+ V [\d.-]+)+$/);
    expect(geo.area.endsWith('Z')).toBe(true);
    expect(geo.points[1].isDrop).toBe(true);
    expect(geo.points[2].isPayday).toBe(true);
  });
});

describe('daysUntilLabel / shortDate / splitUpcoming', () => {
  it('labels day counts naturally', () => {
    expect(daysUntilLabel(0)).toBe('today');
    expect(daysUntilLabel(1)).toBe('tomorrow');
    expect(daysUntilLabel(5)).toBe('5 days');
    expect(daysUntilLabel(undefined)).toBe('today');
  });

  it('formats dates without Date() timezone traps', () => {
    expect(shortDate('2026-07-01')).toBe('Jul 1');
    expect(shortDate('2026-12-15')).toBe('Dec 15');
    expect(shortDate('')).toBe('');
    expect(shortDate(null)).toBe('');
  });

  it('splits upcoming into visible + overflow', () => {
    const six = Array.from({ length: 6 }, (_, i) => ({ id: i }));
    expect(splitUpcoming(six, 4)).toEqual({ visible: six.slice(0, 4), overflow: 2 });
    expect(splitUpcoming([], 4)).toEqual({ visible: [], overflow: 0 });
    expect(splitUpcoming(null, 4)).toEqual({ visible: [], overflow: 0 });
  });
});
