import {
  getBatchWindowForTimestamp,
  listBatchWindowsForDate,
  listBatchWindowsForBookings,
  BATCH_WINDOW_START_HOURS,
  boardWindowForTimestamp,
  listBoardWindowsForRange,
  groupBookingsIntoBoardWindows,
} from './batch-window.util';

describe('batch-window.util', () => {
  it('maps 20:15 EAT to the 19:00–22:00 window', () => {
    // 20:15 EAT = 17:15 UTC on 11 Jun 2026
    const ts = new Date('2026-06-11T17:15:00.000Z');
    const window = getBatchWindowForTimestamp(ts);

    expect(window.label).toContain('19:00');
    expect(window.label).toContain('22:00');
    expect(window.label).toContain('11 Jun 2026');
  });

  it('maps 08:30 EAT to the 07:00–10:00 window', () => {
    const ts = new Date('2026-06-11T05:30:00.000Z'); // 08:30 EAT
    const window = getBatchWindowForTimestamp(ts);
    expect(window.label).toContain('07:00');
    expect(window.label).toContain('10:00');
  });

  it('maps 02:00 EAT to the previous day 22:00–07:00 window', () => {
    const ts = new Date('2026-06-11T23:00:00.000Z'); // 02:00 EAT on 12 Jun
    const window = getBatchWindowForTimestamp(ts);
    expect(window.label).toContain('22:00');
    expect(window.label).toContain('07:00');
    expect(window.label).toContain('11 Jun 2026');
  });

  it('lists six windows for a calendar day', () => {
    const ref = new Date('2026-06-11T12:00:00.000Z');
    const windows = listBatchWindowsForDate(ref);
    expect(windows).toHaveLength(BATCH_WINDOW_START_HOURS.length);
    expect(windows[0].label).toContain('07:00');
    expect(windows[windows.length - 1].label).toContain('22:00');
  });

  it('includes cross-day overnight window when booking signed at 00:02 EAT', () => {
    // 21:02 UTC = 00:02 EAT on 12 Jun → belongs to 11 Jun 22:00–07:00 window
    const fullyExecutedAt = new Date('2026-06-11T21:02:05.153Z');
    const scheduleDate = new Date('2026-06-12T06:00:00.000Z');
    const windows = listBatchWindowsForBookings([fullyExecutedAt], scheduleDate);
    const overnight = windows.find((w) => w.label.includes('22:00') && w.label.includes('07:00'));
    expect(overnight).toBeDefined();
    expect(overnight!.label).toContain('11 Jun 2026');
    expect(getBatchWindowForTimestamp(fullyExecutedAt).key).toBe(overnight!.key);
  });
});

describe('batch-window board windows (midnight-based 3h slots)', () => {
  it('maps 04:00 EAT to the 03:00–06:00 slot', () => {
    // 01:00 UTC = 04:00 EAT on 11 Jun
    const w = boardWindowForTimestamp(new Date('2026-06-11T01:00:00.000Z'));
    expect(w.label).toContain('03:00');
    expect(w.label).toContain('06:00');
    expect(w.date).toBe('2026-06-11');
    expect(w.dateLabel).toContain('11 Jun');
  });

  it('maps 00:30 EAT to the 00:00–03:00 slot of that EAT day', () => {
    // 21:30 UTC on 10 Jun = 00:30 EAT on 11 Jun
    const w = boardWindowForTimestamp(new Date('2026-06-10T21:30:00.000Z'));
    expect(w.label).toContain('00:00');
    expect(w.label).toContain('03:00');
    expect(w.date).toBe('2026-06-11');
  });

  it('maps 23:00 EAT to the final 21:00–24:00 slot', () => {
    // 20:00 UTC = 23:00 EAT on 11 Jun
    const w = boardWindowForTimestamp(new Date('2026-06-11T20:00:00.000Z'));
    expect(w.label).toContain('21:00');
    expect(w.label).toContain('24:00');
    expect(w.date).toBe('2026-06-11');
  });

  it('lists a continuous range open→departure clamped at both ends', () => {
    // open 05 Jun 08:00 EAT (05:00 UTC) → departs 08 Jun 14:00 EAT (11:00 UTC)
    const open = new Date('2026-06-05T05:00:00.000Z');
    const departure = new Date('2026-06-08T11:00:00.000Z');
    const windows = listBoardWindowsForRange(open, departure);

    // Day 5: 06,09,12,15,18,21 = 6 ; Days 6,7: 8 each ; Day 8: 00,03,06,09,12 = 5
    expect(windows).toHaveLength(6 + 8 + 8 + 5);
    expect(windows[0].date).toBe('2026-06-05');
    expect(windows[0].label).toContain('06:00');
    expect(windows[0].label).toContain('09:00');
    const last = windows[windows.length - 1];
    expect(last.date).toBe('2026-06-08');
    expect(last.label).toContain('12:00');
    expect(last.label).toContain('15:00');
    // chronological + unique keys
    const keys = windows.map((w) => w.key);
    expect(new Set(keys).size).toBe(keys.length);
  });

  it('handles a same-day open→departure range', () => {
    const open = new Date('2026-06-05T05:00:00.000Z'); // 08:00 EAT (06–09 slot)
    const departure = new Date('2026-06-05T11:00:00.000Z'); // 14:00 EAT (12–15 slot)
    const windows = listBoardWindowsForRange(open, departure);
    // 06,09,12 = 3 slots
    expect(windows).toHaveLength(3);
    expect(windows.every((w) => w.date === '2026-06-05')).toBe(true);
  });

  it('buckets bookings by fullyExecutedAt and keeps empty + pending windows', () => {
    const open = new Date('2026-06-05T05:00:00.000Z');
    const departure = new Date('2026-06-06T11:00:00.000Z');
    const items = [
      { id: 'a', ts: new Date('2026-06-05T05:30:00.000Z') }, // 08:30 EAT → 06–09 on 5th
      { id: 'b', ts: null }, // pending
    ];
    const map = groupBookingsIntoBoardWindows(
      items,
      (i) => i.ts,
      open,
      departure,
      'pending-contract',
    );
    const pending = map.get('pending-contract');
    expect(pending?.items.map((i) => i.id)).toEqual(['b']);
    const withA = [...map.values()].find((b) => b.items.some((i) => i.id === 'a'));
    expect(withA?.window?.date).toBe('2026-06-05');
    // empty slots are retained for the UI
    const emptyCount = [...map.values()].filter(
      (b) => b.window && b.items.length === 0,
    ).length;
    expect(emptyCount).toBeGreaterThan(0);
  });
});
