import { describe, it, expect, beforeEach } from 'vitest';
import { parseCron, cronMatches, nextCronOccurrence } from './cron.js';
import { registerJob, getJobHandler, getRegisteredJobs, clearRegistry } from './registry.js';
import type { JobContext, JobResult } from './types.js';

// ── Cron Parser Tests ────────────────────────────────────────

describe('cron parser', () => {
  describe('parseCron', () => {
    it('should parse * * * * * (every minute)', () => {
      const fields = parseCron('* * * * *');
      expect(fields.minute.length).toBe(60);
      expect(fields.hour.length).toBe(24);
      expect(fields.dayOfMonth.length).toBe(31);
      expect(fields.month.length).toBe(12);
      expect(fields.dayOfWeek.length).toBe(7);
    });

    it('should parse exact values', () => {
      const fields = parseCron('30 14 1 6 3');
      expect(fields.minute).toEqual([30]);
      expect(fields.hour).toEqual([14]);
      expect(fields.dayOfMonth).toEqual([1]);
      expect(fields.month).toEqual([6]);
      expect(fields.dayOfWeek).toEqual([3]);
    });

    it('should parse step values (*/N)', () => {
      const fields = parseCron('*/15 */6 * * *');
      expect(fields.minute).toEqual([0, 15, 30, 45]);
      expect(fields.hour).toEqual([0, 6, 12, 18]);
    });

    it('should parse ranges (N-M)', () => {
      const fields = parseCron('0-5 9-17 * * *');
      expect(fields.minute).toEqual([0, 1, 2, 3, 4, 5]);
      expect(fields.hour).toEqual([9, 10, 11, 12, 13, 14, 15, 16, 17]);
    });

    it('should parse comma-separated values', () => {
      const fields = parseCron('0,15,30,45 * * * *');
      expect(fields.minute).toEqual([0, 15, 30, 45]);
    });

    it('should throw on invalid cron expression', () => {
      expect(() => parseCron('* * *')).toThrow('expected 5 fields');
      expect(() => parseCron('abc * * * *')).toThrow('Invalid value');
    });
  });

  describe('cronMatches', () => {
    it('should match every-minute cron', () => {
      const date = new Date('2026-03-01T12:30:00Z');
      expect(cronMatches('* * * * *', date)).toBe(true);
    });

    it('should match exact time', () => {
      const date = new Date('2026-03-01T14:30:00Z');
      expect(cronMatches('30 14 * * *', date)).toBe(true);
      expect(cronMatches('31 14 * * *', date)).toBe(false);
    });

    it('should match hourly at minute 0', () => {
      const date = new Date('2026-03-01T08:00:00Z');
      expect(cronMatches('0 * * * *', date)).toBe(true);
    });

    it('should match specific day of week', () => {
      // 2026-03-02 is Monday (day 1)
      const monday = new Date('2026-03-02T09:00:00Z');
      expect(cronMatches('0 9 * * 1', monday)).toBe(true);
      expect(cronMatches('0 9 * * 2', monday)).toBe(false);
    });

    it('should match midnight UTC daily', () => {
      const midnight = new Date('2026-03-01T00:00:00Z');
      expect(cronMatches('0 0 * * *', midnight)).toBe(true);
    });

    it('should match every 6 hours', () => {
      expect(cronMatches('0 */6 * * *', new Date('2026-03-01T00:00:00Z'))).toBe(true);
      expect(cronMatches('0 */6 * * *', new Date('2026-03-01T06:00:00Z'))).toBe(true);
      expect(cronMatches('0 */6 * * *', new Date('2026-03-01T03:00:00Z'))).toBe(false);
    });
  });

  describe('nextCronOccurrence', () => {
    it('should find next minute for * * * * *', () => {
      const after = new Date('2026-03-01T12:30:00Z');
      const next = nextCronOccurrence('* * * * *', after);
      expect(next).not.toBeNull();
      expect(next!.getUTCMinutes()).toBe(31);
    });

    it('should find next hourly occurrence', () => {
      const after = new Date('2026-03-01T12:30:00Z');
      const next = nextCronOccurrence('0 * * * *', after);
      expect(next).not.toBeNull();
      expect(next!.getUTCHours()).toBe(13);
      expect(next!.getUTCMinutes()).toBe(0);
    });

    it('should find next daily midnight', () => {
      const after = new Date('2026-03-01T12:00:00Z');
      const next = nextCronOccurrence('0 0 * * *', after);
      expect(next).not.toBeNull();
      expect(next!.getUTCDate()).toBe(2);
      expect(next!.getUTCHours()).toBe(0);
    });

    it('should skip to next week for weekly cron', () => {
      // Monday 2026-03-02 at 10am — next Monday 9am should be 2026-03-09
      const after = new Date('2026-03-02T10:00:00Z');
      const next = nextCronOccurrence('0 9 * * 1', after);
      expect(next).not.toBeNull();
      expect(next!.getUTCDate()).toBe(9);
    });
  });
});

// ── Job Registry Tests ───────────────────────────────────────

describe('job registry', () => {
  beforeEach(() => {
    clearRegistry();
  });

  it('should register and retrieve a job handler', () => {
    const handler = async (_ctx: JobContext): Promise<JobResult> => ({ success: true });
    registerJob('test-job', handler);
    expect(getJobHandler('test-job')).toBe(handler);
  });

  it('should return undefined for unregistered job', () => {
    expect(getJobHandler('nonexistent')).toBeUndefined();
  });

  it('should list registered job names', () => {
    registerJob('job-a', async () => ({ success: true }));
    registerJob('job-b', async () => ({ success: true }));
    const names = getRegisteredJobs();
    expect(names).toContain('job-a');
    expect(names).toContain('job-b');
    expect(names.length).toBe(2);
  });

  it('should throw on duplicate registration', () => {
    registerJob('dup', async () => ({ success: true }));
    expect(() => registerJob('dup', async () => ({ success: true }))).toThrow('already registered');
  });

  it('should clear all registered jobs', () => {
    registerJob('temp', async () => ({ success: true }));
    expect(getRegisteredJobs().length).toBe(1);
    clearRegistry();
    expect(getRegisteredJobs().length).toBe(0);
  });
});
