import cronModule, { cronInstance, __test__ } from '../../shims/cron';

const { matchesCron, shouldRunInWindow } = __test__;

// Anchor date: 2026-04-17T10:00:00Z (a Friday, minute=0, hour=10)
const at = (hh: number, mm: number, dayOffset = 0) =>
  new Date(Date.UTC(2026, 3, 17 + dayOffset, hh, mm, 0));

describe('matchesCron (reference correctness, not modified by this fix)', () => {
  it.each([
    ['0 * * * * *', 10, 0, true],
    ['0 5 * * * *', 10, 5, true],
    ['0 5 * * * *', 10, 4, false],
    ['0 */5 * * * *', 10, 0, true],
    ['0 */5 * * * *', 10, 5, true],
    ['0 */5 * * * *', 10, 4, false],
    ['0 */5 * * * *', 10, 7, false],
    ['0 */10 * * * *', 10, 0, true],
    ['0 */10 * * * *', 10, 5, false],
    ['0 */10 * * * *', 10, 10, true],
    ['0 */30 * * * *', 10, 0, true],
    ['0 */30 * * * *', 10, 15, false],
    ['0 */30 * * * *', 10, 30, true],
    ['0 5 */6 * * *', 6, 5, true],
    ['0 5 */6 * * *', 6, 6, false],
    ['0 5 */6 * * *', 7, 5, false],
    ['0 5 */6 * * *', 12, 5, true],
    ['0 0 0 * * *', 0, 0, true],
    ['0 0 0 * * *', 0, 1, false],
    ['0 0 10 * * *', 10, 0, true],
    ['0 0 10 * * *', 10, 1, false],
  ])('matchesCron(%s) at %d:%d -> %s', (expr, h, m, expected) => {
    expect(matchesCron(expr, at(h, m))).toBe(expected);
  });
});

// These tests define the POST-FIX behavior: 1-minute window, not 5-minute.
// Before fix: shouldRunInWindow("0 */5 * * * *", h:m) is true for any m in [0..59].
// After  fix: shouldRunInWindow is true only at the exact matching minute.
describe('shouldRunInWindow — 1 minute window (post-fix behavior)', () => {
  describe('every-5-minute cron "0 */5 * * * *"', () => {
    it('matches exactly at minute 0, 5, 10, ..., 55 within an hour (12 times)', () => {
      const matches: number[] = [];
      for (let m = 0; m < 60; m += 1) {
        if (shouldRunInWindow('0 */5 * * * *', at(10, m))) matches.push(m);
      }
      expect(matches).toEqual([0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55]);
      expect(matches).toHaveLength(12);
    });

    it('does NOT match at minutes 1-4, 6-9, 11-14, ... (the bug we are fixing)', () => {
      for (const m of [1, 2, 3, 4, 6, 7, 8, 9, 11, 12, 13, 14]) {
        expect(shouldRunInWindow('0 */5 * * * *', at(10, m))).toBe(false);
      }
    });
  });

  describe('every-10-minute cron "0 */10 * * * *"', () => {
    it('matches exactly 6 times per hour (minute 0, 10, 20, 30, 40, 50)', () => {
      const matches: number[] = [];
      for (let m = 0; m < 60; m += 1) {
        if (shouldRunInWindow('0 */10 * * * *', at(10, m))) matches.push(m);
      }
      expect(matches).toEqual([0, 10, 20, 30, 40, 50]);
    });
  });

  describe('every-30-minute cron "0 */30 * * * *"', () => {
    it('matches exactly 2 times per hour', () => {
      const matches: number[] = [];
      for (let m = 0; m < 60; m += 1) {
        if (shouldRunInWindow('0 */30 * * * *', at(10, m))) matches.push(m);
      }
      expect(matches).toEqual([0, 30]);
    });
  });

  describe('every-minute cron "* * * * *"', () => {
    it('matches every minute', () => {
      for (let m = 0; m < 60; m += 1) {
        expect(shouldRunInWindow('* * * * *', at(10, m))).toBe(true);
      }
    });
  });

  describe('single-minute cron "0 5 */6 * * *" (every 6 hours at minute 5)', () => {
    it('matches exactly 4 times in a 24-hour window (hours 0, 6, 12, 18)', () => {
      const matches: string[] = [];
      for (let h = 0; h < 24; h += 1) {
        for (let m = 0; m < 60; m += 1) {
          if (shouldRunInWindow('0 5 */6 * * *', at(h, m))) matches.push(`${h}:${m}`);
        }
      }
      expect(matches).toEqual(['0:5', '6:5', '12:5', '18:5']);
    });
  });

  describe('daily cron "0 1 0 * * *" (00:01 every day)', () => {
    it('matches exactly once per day', () => {
      const matches: string[] = [];
      for (let h = 0; h < 24; h += 1) {
        for (let m = 0; m < 60; m += 1) {
          if (shouldRunInWindow('0 1 0 * * *', at(h, m))) matches.push(`${h}:${m}`);
        }
      }
      expect(matches).toEqual(['0:1']);
    });
  });

  describe('5-field cron "*/5 * * * *" (no seconds column)', () => {
    it('also supported — matches 12 times per hour', () => {
      const matches: number[] = [];
      for (let m = 0; m < 60; m += 1) {
        if (shouldRunInWindow('*/5 * * * *', at(10, m))) matches.push(m);
      }
      expect(matches).toHaveLength(12);
    });
  });
});

// Invariant check: non-5-aligned cron expressions must still be reachable when
// shim uses 1-minute window (post-fix). This guards against the review finding
// that if CF trigger is "*/5" while shim uses 1-min matching, jobs at minute 1
// (e.g. expiredSessionCleanupCronTime="0 1 * * * *") would never fire.
describe('non-5-aligned cron expressions (review P1-1 regression guard)', () => {
  it.each([
    ['0 1 * * * *', 'expiredSessionCleanupCronTime (minute 1 every hour)', [1]],
    ['0 1 0 * * *', 'paymentStatCronTime (00:01 daily)', [1]],
    ['0 7 * * * *', 'hypothetical minute 7', [7]],
    ['0 13 * * * *', 'hypothetical minute 13', [13]],
  ])('matches %s (%s) at the expected minute', (expr, _label, expectedMinutes) => {
    const matches: number[] = [];
    for (let m = 0; m < 60; m += 1) {
      // Check at hour 0 so the hour field "0" in '0 1 0 * * *' also aligns.
      if (shouldRunInWindow(expr, at(0, m))) matches.push(m);
    }
    expect(matches).toEqual(expectedMinutes);
  });
});

describe('regression guard — bug condition from 2026-04-17 incident', () => {
  it('each minute in an hour produces the EXPECTED number of matches for payment-kit cron expressions', () => {
    const expressions: Record<string, number> = {
      '0 */5 * * * *': 12, // depositVault, revokeStake
      '0 */10 * * * *': 6, // creditConsume, vendorStatusCheck, vendorReturnScan
      '0 */20 * * * *': 3, // stripePayment
      '0 */30 * * * *': 2, // subscription, stripeInvoice
    };

    for (const [expr, expected] of Object.entries(expressions)) {
      let count = 0;
      for (let m = 0; m < 60; m += 1) {
        if (shouldRunInWindow(expr, at(10, m))) count += 1;
      }
      expect({ expr, count }).toEqual({ expr, count: expected });
    }
  });
});

// runAll(date) integration test — verifies the shim uses the passed date
// (scheduledTime from CF) rather than wall-clock at execution time. This
// addresses PR #1341 review P2-1: a scheduled event intended for 00:01 but
// delivered late at 00:02 must still match "0 1 * * * *" jobs.
describe('cronInstance.runAll(date) — scheduledTime integration (review P2-1)', () => {
  afterEach(() => {
    // cronModule.init({...}) mutates the singleton registry; reset between cases
    cronModule.init({ jobs: [] });
  });

  it('passes the provided date to shouldRunInWindow, not wall clock', async () => {
    const calls: string[] = [];
    cronModule.init({
      jobs: [
        {
          name: 'test.minute-1',
          time: '0 1 * * * *',
          fn: async () => { calls.push('minute-1'); },
        },
        {
          name: 'test.minute-2',
          time: '0 2 * * * *',
          fn: async () => { calls.push('minute-2'); },
        },
      ],
    });

    // Scheduled intended at 00:01 but delivered late so wall-clock is in minute 2.
    // Caller (worker.ts) should pass the intended time (new Date(event.scheduledTime)).
    const intendedMinute1 = new Date(Date.UTC(2026, 3, 17, 0, 1, 0));
    await cronInstance.runAll(intendedMinute1);

    expect(calls).toEqual(['minute-1']);
  });

  it('defaults to new Date() when no arg is passed', async () => {
    // Smoke check that the default-arg path still works (e.g. from manual dev endpoints).
    let ran = false;
    cronModule.init({
      jobs: [
        {
          name: 'test.always',
          time: '* * * * *',
          fn: async () => { ran = true; },
        },
      ],
    });
    await cronInstance.runAll();
    expect(ran).toBe(true);
  });
});
