import { describe, expect, it } from 'vitest';
import { appliesToDate, daysInMonth, firstMatchingIndex, type RecurrenceLimits } from './playoutTemplateCalendar';
import type { DayOfWeek } from '../api';

const ALL_DAYS: DayOfWeek[] = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
const ALL_DOM = Array.from({ length: 31 }, (_, index) => index + 1);
const ALL_MONTHS = Array.from({ length: 12 }, (_, index) => index + 1);

function rec(overrides: Partial<RecurrenceLimits> = {}): RecurrenceLimits {
  return {
    daysOfWeek: [...ALL_DAYS],
    daysOfMonth: [...ALL_DOM],
    monthsOfYear: [...ALL_MONTHS],
    limitToDateRange: false,
    startMonth: 1,
    startDay: 1,
    startYear: null,
    endMonth: 12,
    endDay: 31,
    endYear: null,
    ...overrides
  };
}

describe('daysInMonth', () => {
  it('returns the length of each month, respecting leap years', () => {
    expect(daysInMonth(2026, 1)).toBe(31);
    expect(daysInMonth(2026, 2)).toBe(28);
    expect(daysInMonth(2024, 2)).toBe(29); // leap year
    expect(daysInMonth(2026, 4)).toBe(30);
  });
});

describe('appliesToDate', () => {
  it('applies to any date with defaults and no date-range limit', () => {
    // 2026-01-06 is a Tuesday.
    expect(appliesToDate(rec(), new Date(2026, 0, 6))).toBe(true);
  });

  it('filters by day of the week', () => {
    const mondayOnly = rec({ daysOfWeek: ['Monday'] });
    expect(appliesToDate(mondayOnly, new Date(2026, 0, 5))).toBe(true); // Monday
    expect(appliesToDate(mondayOnly, new Date(2026, 0, 6))).toBe(false); // Tuesday
  });

  it('filters by day of the month', () => {
    const firstOnly = rec({ daysOfMonth: [1] });
    expect(appliesToDate(firstOnly, new Date(2026, 5, 1))).toBe(true);
    expect(appliesToDate(firstOnly, new Date(2026, 5, 2))).toBe(false);
  });

  it('filters by month of the year', () => {
    const juneOnly = rec({ monthsOfYear: [6] });
    expect(appliesToDate(juneOnly, new Date(2026, 5, 15))).toBe(true);
    expect(appliesToDate(juneOnly, new Date(2026, 6, 15))).toBe(false);
  });

  it('limits to an inclusive date range on both ends', () => {
    const march = rec({ limitToDateRange: true, startMonth: 3, startDay: 1, endMonth: 3, endDay: 31 });
    expect(appliesToDate(march, new Date(2026, 2, 1))).toBe(true); // Mar 1 (start boundary)
    expect(appliesToDate(march, new Date(2026, 2, 31))).toBe(true); // Mar 31 (end boundary)
    expect(appliesToDate(march, new Date(2026, 1, 28))).toBe(false); // Feb 28
    expect(appliesToDate(march, new Date(2026, 3, 1))).toBe(false); // Apr 1
  });

  it('wraps the year boundary when the range is reversed', () => {
    const winter = rec({ limitToDateRange: true, startMonth: 11, startDay: 1, endMonth: 2, endDay: 1 });
    expect(appliesToDate(winter, new Date(2026, 11, 15))).toBe(true); // Dec 15
    expect(appliesToDate(winter, new Date(2026, 0, 10))).toBe(true); // Jan 10
    expect(appliesToDate(winter, new Date(2026, 10, 1))).toBe(true); // Nov 1 (boundary, inclusive)
    expect(appliesToDate(winter, new Date(2026, 1, 1))).toBe(true); // Feb 1 (boundary, inclusive)
    expect(appliesToDate(winter, new Date(2026, 5, 1))).toBe(false); // Jun 1 (excluded middle)
  });

  it('uses explicit years and disables wrap-around when both years are set', () => {
    // start month/day (Nov 1) is after end month/day (Feb 1), which would normally reverse — but with
    // explicit years reverse is forced off, so this is a straight 2025-11-01 .. 2026-02-01 window.
    const withYears = rec({
      limitToDateRange: true,
      startMonth: 11,
      startDay: 1,
      startYear: 2025,
      endMonth: 2,
      endDay: 1,
      endYear: 2026
    });
    expect(appliesToDate(withYears, new Date(2025, 11, 15))).toBe(true); // Dec 15 2025 (in window)
    expect(appliesToDate(withYears, new Date(2026, 5, 1))).toBe(false); // Jun 1 2026 (after end)
    expect(appliesToDate(withYears, new Date(2025, 5, 1))).toBe(false); // Jun 1 2025 (before start)
  });

  it('rolls a too-large start day forward to the 1st of the next month', () => {
    // April has 30 days; startDay 31 rolls the window start forward to May 1.
    const draft = rec({ limitToDateRange: true, startMonth: 4, startDay: 31, endMonth: 12, endDay: 31 });
    expect(appliesToDate(draft, new Date(2026, 3, 30))).toBe(false); // Apr 30 (before rolled start)
    expect(appliesToDate(draft, new Date(2026, 4, 1))).toBe(true); // May 1 (rolled start)
  });

  it('clamps a too-large end day to the last day of the month (incl. leap years)', () => {
    const nonLeap = rec({ limitToDateRange: true, startMonth: 1, startDay: 1, endMonth: 2, endDay: 31 });
    expect(appliesToDate(nonLeap, new Date(2026, 1, 28))).toBe(true); // Feb 28 2026 (clamped end)
    expect(appliesToDate(nonLeap, new Date(2026, 2, 1))).toBe(false); // Mar 1 2026
    expect(appliesToDate(nonLeap, new Date(2024, 1, 29))).toBe(true); // Feb 29 2024 (leap year)
  });
});

describe('firstMatchingIndex', () => {
  it('returns the first matching template in priority order', () => {
    const items = [rec({ daysOfWeek: ['Monday'] }), rec()];
    expect(firstMatchingIndex(items, new Date(2026, 0, 5))).toBe(0); // Monday → first row wins
    expect(firstMatchingIndex(items, new Date(2026, 0, 6))).toBe(1); // Tuesday → falls through to row 1
  });

  it('returns -1 when no template applies', () => {
    const items = [rec({ daysOfWeek: ['Monday'] }), rec({ daysOfWeek: ['Wednesday'] })];
    expect(firstMatchingIndex(items, new Date(2026, 0, 6))).toBe(-1); // Tuesday matches neither
  });
});
