import { resolveDueDate } from './due-date.resolver';
import type { DueIntent } from './voice.types';

// 2025-10-11 is a Saturday — the first day of the Iranian week.
const SATURDAY = '2025-10-11';
const THURSDAY = '2025-10-16';

describe('resolveDueDate', () => {
  describe('weekday intents', () => {
    it('resolves "this <weekday>" to the coming occurrence in the same week', () => {
      const result = resolveDueDate(
        { kind: 'weekday', weekday: 'thursday', which: 'this' },
        SATURDAY,
      );
      expect(result.dueDate).toBe(THURSDAY); // Sat -> Thu is 5 days in a Saturday-start week
    });

    it('resolves "next <weekday>" to the following week', () => {
      const result = resolveDueDate(
        { kind: 'weekday', weekday: 'thursday', which: 'next' },
        SATURDAY,
      );
      expect(result.dueDate).toBe('2025-10-23');
    });

    it('reads "by Thursday" said on a Thursday as the next one, not today', () => {
      // A deadline of today is almost never what was meant.
      const result = resolveDueDate(
        { kind: 'weekday', weekday: 'thursday', which: 'this' },
        THURSDAY,
      );
      expect(result.dueDate).toBe('2025-10-23');
    });

    it('resolves the Saturday that starts the next week', () => {
      const result = resolveDueDate(
        { kind: 'weekday', weekday: 'saturday', which: 'this' },
        SATURDAY,
      );
      expect(result.dueDate).toBe('2025-10-18');
    });

    it('rejects an unknown weekday or qualifier', () => {
      expect(
        resolveDueDate(
          {
            kind: 'weekday',
            weekday: 'caturday',
            which: 'this',
          } as unknown as DueIntent,
          SATURDAY,
        ).dueDate,
      ).toBeNull();
      expect(
        resolveDueDate(
          {
            kind: 'weekday',
            weekday: 'thursday',
            which: 'soon',
          } as unknown as DueIntent,
          SATURDAY,
        ).dueDate,
      ).toBeNull();
    });
  });

  describe('offset intents', () => {
    it('adds days, weeks and months', () => {
      expect(
        resolveDueDate({ kind: 'offset', unit: 'day', amount: 1 }, SATURDAY)
          .dueDate,
      ).toBe('2025-10-12');
      expect(
        resolveDueDate({ kind: 'offset', unit: 'week', amount: 1 }, SATURDAY)
          .dueDate,
      ).toBe('2025-10-18');
      expect(
        resolveDueDate({ kind: 'offset', unit: 'month', amount: 1 }, SATURDAY)
          .dueDate,
      ).toBe('2025-11-11');
    });

    it('clamps to the end of a shorter month', () => {
      expect(
        resolveDueDate(
          { kind: 'offset', unit: 'month', amount: 1 },
          '2025-01-31',
        ).dueDate,
      ).toBe('2025-02-28');
      expect(
        resolveDueDate(
          { kind: 'offset', unit: 'month', amount: 1 },
          '2024-01-31',
        ).dueDate,
      ).toBe('2024-02-29');
    });

    it('crosses a year boundary', () => {
      expect(
        resolveDueDate(
          { kind: 'offset', unit: 'month', amount: 3 },
          '2025-11-15',
        ).dueDate,
      ).toBe('2026-02-15');
      expect(
        resolveDueDate(
          { kind: 'offset', unit: 'day', amount: 30 },
          '2025-12-20',
        ).dueDate,
      ).toBe('2026-01-19');
    });

    it('rejects negative, fractional and absurd amounts', () => {
      for (const amount of [-1, 1.5, 10_000, Number.NaN]) {
        expect(
          resolveDueDate({ kind: 'offset', unit: 'day', amount }, SATURDAY)
            .dueDate,
        ).toBeNull();
      }
    });
  });

  describe('jalali intents', () => {
    it('converts by arithmetic, not inference', () => {
      expect(
        resolveDueDate({ kind: 'jalali', jy: 1404, jm: 7, jd: 25 }, SATURDAY)
          .dueDate,
      ).toBe('2025-10-17');
    });

    it('handles the leap-year Esfand 30', () => {
      expect(
        resolveDueDate(
          { kind: 'jalali', jy: 1403, jm: 12, jd: 30 },
          '2025-03-01',
        ).dueDate,
      ).toBe('2025-03-20');
    });

    it('rejects Esfand 30 in a non-leap year', () => {
      const result = resolveDueDate(
        { kind: 'jalali', jy: 1404, jm: 12, jd: 30 },
        SATURDAY,
      );
      expect(result.dueDate).toBeNull();
      expect(result.unresolved?.reason).toBe('invalid_date');
    });
  });

  describe('gregorian intents', () => {
    it('accepts a real date and rejects an impossible one', () => {
      expect(
        resolveDueDate({ kind: 'gregorian', y: 2025, m: 10, d: 17 }, SATURDAY)
          .dueDate,
      ).toBe('2025-10-17');
      expect(
        resolveDueDate({ kind: 'gregorian', y: 2025, m: 2, d: 30 }, SATURDAY)
          .dueDate,
      ).toBeNull();
      expect(
        resolveDueDate({ kind: 'gregorian', y: 2025, m: 13, d: 1 }, SATURDAY)
          .dueDate,
      ).toBeNull();
    });
  });

  describe('guard rails', () => {
    it('treats a past date as unresolved', () => {
      const result = resolveDueDate(
        { kind: 'gregorian', y: 2020, m: 1, d: 1 },
        SATURDAY,
      );
      expect(result.dueDate).toBeNull();
      expect(result.unresolved?.reason).toBe('invalid_date');
    });

    it('treats a date decades away as unresolved', () => {
      expect(
        resolveDueDate({ kind: 'gregorian', y: 2099, m: 1, d: 1 }, SATURDAY)
          .dueDate,
      ).toBeNull();
    });

    it('accepts today itself via a zero-day offset', () => {
      expect(
        resolveDueDate({ kind: 'offset', unit: 'day', amount: 0 }, SATURDAY)
          .dueDate,
      ).toBe(SATURDAY);
    });

    it('reports no due date at all when the model said nothing, without flagging it', () => {
      expect(resolveDueDate(null, SATURDAY)).toEqual({
        dueDate: null,
        unresolved: null,
      });
      expect(resolveDueDate(undefined, SATURDAY)).toEqual({
        dueDate: null,
        unresolved: null,
      });
    });

    it('degrades rather than throwing on a malformed today or intent', () => {
      expect(
        resolveDueDate({ kind: 'offset', unit: 'day', amount: 1 }, 'not-a-date')
          .dueDate,
      ).toBeNull();
      expect(
        resolveDueDate({ kind: 'nope' } as unknown as DueIntent, SATURDAY)
          .dueDate,
      ).toBeNull();
    });

    it('echoes what was heard so the review sheet can show it', () => {
      const result = resolveDueDate(
        { kind: 'jalali', jy: 1404, jm: 12, jd: 30 },
        SATURDAY,
      );
      expect(result.unresolved?.spoken).toBe('1404/12/30');
    });
  });
});
