/**
 * Scheduler tests — the LIVE / cold / dead-hours / weekend-night cases from
 * Handoff 03's methodology table. Pure logic, no DB: runs without the registry.
 *
 * All instants are built in America/New_York summer (EDT, UTC-4), so a local
 * wall-clock hour H on June day D is `Date.UTC(2026, 5, D, H+4)`. June 2026:
 * 17th=Wed, 19th=Fri, 20th=Sat, 21st=Sun.
 */

import { describe, expect, test } from 'bun:test';

import {
  schedule,
  derivePriority,
  dayPartForHour,
  DEFAULT_SCHEDULER_CONFIG as CFG,
} from './scheduler';
import { timezoneForHandle, DEFAULT_TIMEZONE } from './timezone';

const HOUR = 60 * 60 * 1000;
const MIN = 60 * 1000;
/** Epoch ms for an ET wall-clock time in June 2026 (EDT = UTC-4). */
const et = (day: number, hour: number, min = 0) => Date.UTC(2026, 5, day, hour + 4, min);
const mid = () => 0.5; // deterministic jitter at range midpoint

describe('derivePriority', () => {
  test('recency buckets', () => {
    expect(derivePriority(10 * MIN)).toBe('HOT');
    expect(derivePriority(3 * HOUR)).toBe('HIGH');
    expect(derivePriority(8 * HOUR)).toBe('NORMAL');
    expect(derivePriority(36 * HOUR)).toBe('LOW');
    expect(derivePriority(5 * 24 * HOUR)).toBe('STALE');
  });
});

describe('dayPartForHour', () => {
  test('boundaries', () => {
    expect(dayPartForHour(5)).toBe('DEAD');
    expect(dayPartForHour(10)).toBe('MORNING');
    expect(dayPartForHour(14)).toBe('AFTERNOON');
    expect(dayPartForHour(19)).toBe('EVENING');
    expect(dayPartForHour(23)).toBe('LATE');
    expect(dayPartForHour(0)).toBe('LATE');
  });
});

describe('schedule — LIVE/HOT', () => {
  test('<60m sends promptly with undo-window delay, ignoring day-part', () => {
    const now = et(17, 14); // Wed 14:00
    const r = schedule({ recvAt: now - 10 * MIN, now, rand: mid });
    expect(r.priority).toBe('HOT');
    expect(r.notBefore).toBe(now + 120 * 1000); // midpoint of 60–180s
    expect(r.reason).toContain('LIVE');
  });

  test('HOT overrides dead-hours (still sends now, not deferred)', () => {
    const now = et(17, 5); // Wed 05:00 (DEAD)
    const r = schedule({ recvAt: now - 10 * MIN, now, rand: mid });
    expect(r.priority).toBe('HOT');
    expect(r.notBefore).toBe(now + 120 * 1000);
  });
});

describe('schedule — cold, in send-friendly window', () => {
  test('weekday afternoon → paced send now', () => {
    const now = et(17, 14); // Wed 14:00
    const r = schedule({ recvAt: now - 8 * HOUR, now, rand: mid });
    expect(r.priority).toBe('NORMAL');
    expect(r.notBefore).toBe(now + 55 * 1000); // midpoint of 20–90s
    expect(r.reason).toContain('send-friendly');
    expect(r.reason).not.toContain('weekend');
  });
});

describe('schedule — cold, deferred (rhythm bias)', () => {
  test('dead-hours, evening texter → defer to 17:00 today', () => {
    const now = et(17, 5); // Wed 05:00 (DEAD)
    const recvAt = now - 8 * HOUR; // 21:00 Tue → EVENING
    const r = schedule({ recvAt, now, rand: mid });
    expect(r.priority).toBe('NORMAL');
    expect(r.recvDayPart).toBe('EVENING');
    expect(r.notBefore).toBe(et(17, 17)); // 17:00 Wed today
    expect(r.reason).toContain('EVENING');
  });

  test('dead-hours, lunchtime texter → defer to 12:00 today (different rhythm)', () => {
    const now = et(17, 6); // Wed 06:00 (DEAD)
    const recvAt = now - 18 * HOUR; // 12:00 Tue → AFTERNOON
    const r = schedule({ recvAt, now, rand: mid });
    expect(r.recvDayPart).toBe('AFTERNOON');
    expect(r.notBefore).toBe(et(17, 12)); // 12:00 Wed, not 17:00
  });

  test('deferred target already passed today → rolls to tomorrow', () => {
    // 02:00 Wed, lead messaged 00:30 Wed (LATE → prefers 22:00). 2 < 22 so today.
    // Use a morning texter at a late local hour to force tomorrow instead:
    const now = et(17, 2); // Wed 02:00 — outside window (weekday)
    const recvAt = et(16, 10); // Tue 10:00 → MORNING → prefers max(10, 9)=10
    const r = schedule({ recvAt, now, rand: mid });
    expect(r.recvDayPart).toBe('MORNING');
    // now.hour 2 < 10 → today 10:00 Wed
    expect(r.notBefore).toBe(et(17, 10));
  });
});

describe('schedule — weekend-night extension', () => {
  test('Saturday 02:00 is in-window (weekend extended) → sends now', () => {
    const now = et(20, 2); // Sat 02:00
    const r = schedule({ recvAt: now - 90 * MIN, now, rand: mid }); // 1.5h → HIGH
    expect(r.priority).toBe('HIGH');
    expect(r.notBefore).toBe(now + 55 * 1000);
    expect(r.reason).toContain('weekend-night');
  });

  test('Wednesday 02:00 is NOT in-window → deferred (proves weekend matters)', () => {
    const now = et(17, 2); // Wed 02:00
    const recvAt = now - 90 * MIN; // 00:30 Wed → LATE
    const r = schedule({ recvAt, now, rand: mid });
    expect(r.recvDayPart).toBe('LATE');
    expect(r.notBefore).toBe(et(17, 22)); // LATE rhythm → 22:00 today
    expect(r.reason).toContain('deferred');
  });
});

describe('timezoneForHandle', () => {
  test('area-code → zone, default Eastern', () => {
    expect(timezoneForHandle('+13105551234')).toBe('America/Los_Angeles');
    expect(timezoneForHandle('+13125551234')).toBe('America/Chicago');
    expect(timezoneForHandle('+13035551234')).toBe('America/Denver');
    expect(timezoneForHandle('+12125551234')).toBe('America/New_York');
    expect(timezoneForHandle('+19995551234')).toBe(DEFAULT_TIMEZONE); // unknown
    expect(timezoneForHandle('garbage')).toBe(DEFAULT_TIMEZONE);
  });

  test('config default matches', () => {
    expect(CFG.defaultTimezone).toBe(DEFAULT_TIMEZONE);
  });
});
