import {
    clampDayToMonth,
    generateMonthlyDueDates,
    generateQuarterlyDueDates,
    buildQuarterLabels,
} from './fees.installment-dates';

const d = (iso: string) => new Date(iso);

describe('clampDayToMonth — edge cases', () => {
    it('day=0 clamps up to 1', () => {
        expect(clampDayToMonth(2027, 1, 0)).toBe(1);
    });
    it('day=-5 clamps up to 1', () => {
        expect(clampDayToMonth(2027, 1, -5)).toBe(1);
    });
    it('day=99 clamps down to 31 for January', () => {
        expect(clampDayToMonth(2027, 1, 99)).toBe(31);
    });
    it('day=30 clamps to 28 for Feb non-leap', () => {
        expect(clampDayToMonth(2027, 2, 30)).toBe(28);
    });
    it('day=30 clamps to 29 for Feb leap', () => {
        expect(clampDayToMonth(2028, 2, 30)).toBe(29);
    });
    it('centennial non-leap (2100): Feb has 28 days', () => {
        expect(clampDayToMonth(2100, 2, 31)).toBe(28);
    });
    it('quadricentennial leap (2000, 2400): Feb has 29 days', () => {
        expect(clampDayToMonth(2000, 2, 31)).toBe(29);
        expect(clampDayToMonth(2400, 2, 31)).toBe(29);
    });
    it('out-of-range month: 13 wraps to Jan next year (31 days) — not validated but does not throw', () => {
        // Date.UTC(year, 13, 0) = day 0 of Feb = Jan 31
        // Caller should pass month1 ∈ [1,12]; this documents current behavior
        expect(clampDayToMonth(2027, 13, 31)).toBe(31);
    });
    it('out-of-range month: 0 wraps to Dec prev year (31 days)', () => {
        // Date.UTC(year, 0, 0) = day 0 of Jan = Dec 31 prev year
        expect(clampDayToMonth(2027, 0, 31)).toBe(31);
    });
});

describe('generateMonthlyDueDates — BACKWARD RANGE BUG probe', () => {
    it('start > end (backward range): emits 0 dates via early-exit (no runaway loop)', () => {
        // Defense-in-depth: the service layer also rejects backward ranges via
        // resolveEffectiveRange's BadRequestException, but if a backward range
        // ever reaches the helper directly, it now emits 0 dates rather than
        // walking up to the guard=60 cap with bogus forward-year dates.
        const dates = generateMonthlyDueDates(d('2027-02-01T00:00:00Z'), d('2027-01-31T00:00:00Z'), 10);
        expect(dates).toHaveLength(0);
    });

    it('same month range: emits exactly 1 installment', () => {
        const dates = generateMonthlyDueDates(d('2027-06-05T00:00:00Z'), d('2027-06-20T00:00:00Z'), 15);
        expect(dates).toHaveLength(1);
        expect(dates[0].toISOString().slice(0, 10)).toBe('2027-06-15');
    });

    it('2-year range (24 months) respected', () => {
        const dates = generateMonthlyDueDates(d('2027-01-01T00:00:00Z'), d('2028-12-31T00:00:00Z'), 10);
        expect(dates).toHaveLength(24);
        expect(dates[0].toISOString().slice(0, 7)).toBe('2027-01');
        expect(dates[23].toISOString().slice(0, 7)).toBe('2028-12');
    });

    it('5-year range hits guard of 60 exactly', () => {
        const dates = generateMonthlyDueDates(d('2027-01-01T00:00:00Z'), d('2031-12-31T00:00:00Z'), 1);
        expect(dates).toHaveLength(60); // guard cap
    });

    it('6-year range is truncated at 60 (no further months emitted)', () => {
        const dates = generateMonthlyDueDates(d('2027-01-01T00:00:00Z'), d('2033-12-31T00:00:00Z'), 1);
        expect(dates).toHaveLength(60);
        // Last emitted is end of the 60-month window from Jan 2027 = Dec 2031
        expect(dates[59].toISOString().slice(0, 7)).toBe('2031-12');
    });

    it('dueDay=31 across full year clamps each short month correctly', () => {
        const dates = generateMonthlyDueDates(d('2028-01-01T00:00:00Z'), d('2028-12-31T00:00:00Z'), 31);
        const expected = [
            '2028-01-31', '2028-02-29', '2028-03-31', '2028-04-30',
            '2028-05-31', '2028-06-30', '2028-07-31', '2028-08-31',
            '2028-09-30', '2028-10-31', '2028-11-30', '2028-12-31',
        ];
        expect(dates.map(x => x.toISOString().slice(0, 10))).toEqual(expected);
    });

    it('dueDay=1 every month lands on the 1st', () => {
        const dates = generateMonthlyDueDates(d('2027-01-15T00:00:00Z'), d('2027-12-15T00:00:00Z'), 1);
        expect(dates.every(x => x.getUTCDate() === 1)).toBe(true);
    });

    it('year crossing: Dec → Jan increments year correctly', () => {
        const dates = generateMonthlyDueDates(d('2027-12-01T00:00:00Z'), d('2028-01-31T00:00:00Z'), 10);
        expect(dates).toHaveLength(2);
        expect(dates[0].toISOString().slice(0, 10)).toBe('2027-12-10');
        expect(dates[1].toISOString().slice(0, 10)).toBe('2028-01-10');
    });
});

describe('generateQuarterlyDueDates — edge cases', () => {
    it('startMonth=11 (Nov): emits Nov, Feb+1, May+1, Aug+1 next year', () => {
        const dates = generateQuarterlyDueDates(d('2027-11-01T00:00:00Z'), d('2028-10-31T00:00:00Z'), 15);
        expect(dates.map(x => x.toISOString().slice(0, 7))).toEqual([
            '2027-11', '2028-02', '2028-05', '2028-08',
        ]);
    });

    it('startMonth=12 (Dec): wraps year for all subsequent quarters', () => {
        const dates = generateQuarterlyDueDates(d('2027-12-01T00:00:00Z'), d('2028-11-30T00:00:00Z'), 15);
        expect(dates.map(x => x.toISOString().slice(0, 7))).toEqual([
            '2027-12', '2028-03', '2028-06', '2028-09',
        ]);
    });

    it('quarterly dueDay=31 clamps per month (Feb → 28/29)', () => {
        const dates = generateQuarterlyDueDates(d('2027-02-01T00:00:00Z'), d('2027-01-31T00:00:00Z'), 31);
        expect(dates[0].getUTCDate()).toBe(28); // Feb 2027 non-leap
        expect(dates[1].getUTCDate()).toBe(31); // May
        expect(dates[2].getUTCDate()).toBe(31); // Aug
        expect(dates[3].getUTCDate()).toBe(30); // Nov
    });

    it('ignores end param — always 4 installments regardless', () => {
        const dates = generateQuarterlyDueDates(
            d('2027-01-01T00:00:00Z'),
            d('2027-03-31T00:00:00Z'), // 3-month end window
            10,
        );
        expect(dates).toHaveLength(4);
        expect(dates[3].toISOString().slice(0, 7)).toBe('2027-10'); // last quarter crosses the "end" window
    });
});

describe('UTC invariance', () => {
    it('input with any UTC time-of-day resolves to midnight-UTC of the target day', () => {
        const dates = generateMonthlyDueDates(
            d('2027-01-01T23:59:59Z'),
            d('2027-02-28T00:00:01Z'),
            15,
        );
        expect(dates).toHaveLength(2);
        expect(dates[0].toISOString()).toBe('2027-01-15T00:00:00.000Z');
        expect(dates[1].toISOString()).toBe('2027-02-15T00:00:00.000Z');
    });

    it('DST-transition months (Mar, Nov) produce no drift — all midnight UTC', () => {
        const dates = generateMonthlyDueDates(d('2027-03-01T00:00:00Z'), d('2027-11-30T00:00:00Z'), 15);
        for (const x of dates) {
            expect(x.getUTCHours()).toBe(0);
            expect(x.getUTCMinutes()).toBe(0);
            expect(x.getUTCSeconds()).toBe(0);
        }
    });
});

describe('buildQuarterLabels', () => {
    it('June-start: Q1 (Jun-Aug), Q2 (Sep-Nov), Q3 (Dec-Feb), Q4 (Mar-May)', () => {
        expect(buildQuarterLabels(6)).toEqual([
            'Q1 (Jun-Aug)', 'Q2 (Sep-Nov)', 'Q3 (Dec-Feb)', 'Q4 (Mar-May)',
        ]);
    });
    it('Calendar (Jan-start): Q1 (Jan-Mar), Q2 (Apr-Jun), Q3 (Jul-Sep), Q4 (Oct-Dec)', () => {
        expect(buildQuarterLabels(1)).toEqual([
            'Q1 (Jan-Mar)', 'Q2 (Apr-Jun)', 'Q3 (Jul-Sep)', 'Q4 (Oct-Dec)',
        ]);
    });
    it('April-start: Q1 (Apr-Jun), Q2 (Jul-Sep), Q3 (Oct-Dec), Q4 (Jan-Mar)', () => {
        expect(buildQuarterLabels(4)).toEqual([
            'Q1 (Apr-Jun)', 'Q2 (Jul-Sep)', 'Q3 (Oct-Dec)', 'Q4 (Jan-Mar)',
        ]);
    });
    it('December-start: Q1 (Dec-Feb), Q2 (Mar-May), Q3 (Jun-Aug), Q4 (Sep-Nov)', () => {
        expect(buildQuarterLabels(12)).toEqual([
            'Q1 (Dec-Feb)', 'Q2 (Mar-May)', 'Q3 (Jun-Aug)', 'Q4 (Sep-Nov)',
        ]);
    });
    it('November-start (wrap-heavy): Q1 (Nov-Jan), Q2 (Feb-Apr), Q3 (May-Jul), Q4 (Aug-Oct)', () => {
        expect(buildQuarterLabels(11)).toEqual([
            'Q1 (Nov-Jan)', 'Q2 (Feb-Apr)', 'Q3 (May-Jul)', 'Q4 (Aug-Oct)',
        ]);
    });
});

describe('generateMonthlyDueDates — skip first month when dueDay < startDay', () => {
    it('start Jun 15 + dueDay=10 skips Jun 10, emits Jul 10 first', () => {
        const dates = generateMonthlyDueDates(d('2026-06-15T00:00:00Z'), d('2026-08-31T00:00:00Z'), 10);
        expect(dates.map((x) => x.toISOString().slice(0, 10))).toEqual([
            '2026-07-10', '2026-08-10',
        ]);
    });

    it('start Jun 15 + dueDay=15 keeps Jun 15 as first', () => {
        const dates = generateMonthlyDueDates(d('2026-06-15T00:00:00Z'), d('2026-08-15T00:00:00Z'), 15);
        expect(dates.map((x) => x.toISOString().slice(0, 10))).toEqual([
            '2026-06-15', '2026-07-15', '2026-08-15',
        ]);
    });

    it('start Jun 15 + dueDay=20 keeps Jun 20 as first (dueDay > startDay)', () => {
        const dates = generateMonthlyDueDates(d('2026-06-15T00:00:00Z'), d('2026-08-20T00:00:00Z'), 20);
        expect(dates.map((x) => x.toISOString().slice(0, 10))).toEqual([
            '2026-06-20', '2026-07-20', '2026-08-20',
        ]);
    });

    it('start Dec 20 + dueDay=5 skips Dec, emits Jan 5 next year as first', () => {
        const dates = generateMonthlyDueDates(d('2026-12-20T00:00:00Z'), d('2027-03-05T00:00:00Z'), 5);
        expect(dates.map((x) => x.toISOString().slice(0, 10))).toEqual([
            '2027-01-05', '2027-02-05', '2027-03-05',
        ]);
    });

    it('start day=1 + dueDay=1 → no skip', () => {
        const dates = generateMonthlyDueDates(d('2026-06-01T00:00:00Z'), d('2026-08-01T00:00:00Z'), 1);
        expect(dates).toHaveLength(3);
    });

    it('window collapses to zero if skip pushes past end', () => {
        // start=Jun 15, end=Jun 30, dueDay=10 → skip Jun, but end is also Jun → 0 emissions
        const dates = generateMonthlyDueDates(d('2026-06-15T00:00:00Z'), d('2026-06-30T00:00:00Z'), 10);
        expect(dates).toHaveLength(0);
    });
});

describe('generateQuarterlyDueDates — caller should clip past endDate', () => {
    it('4-month window (Sep→Dec) yields 2 quarterly dates in-window', () => {
        const start = d('2025-09-01T00:00:00Z');
        const end = d('2025-12-31T00:00:00Z');
        const all = generateQuarterlyDueDates(start, end, 10);
        // Helper returns all 4; caller (buildAssignmentDocs) filters.
        expect(all).toHaveLength(4);
        const endMs = end.getTime();
        const filtered = all.filter((x) => x.getTime() <= endMs);
        expect(filtered.map((x) => x.toISOString().slice(0, 7))).toEqual([
            '2025-09', '2025-12',
        ]);
    });

    it('7-month window (Sep→Mar) yields 3 quarterly dates in-window (Sep, Dec, Mar)', () => {
        const start = d('2025-09-01T00:00:00Z');
        const end = d('2026-03-31T00:00:00Z');
        const all = generateQuarterlyDueDates(start, end, 10);
        const endMs = end.getTime();
        const filtered = all.filter((x) => x.getTime() <= endMs);
        expect(filtered.map((x) => x.toISOString().slice(0, 7))).toEqual([
            '2025-09', '2025-12', '2026-03',
        ]);
    });

    it('2-month window (Sep→Oct) yields 1 quarterly date (Sep only)', () => {
        const start = d('2025-09-01T00:00:00Z');
        const end = d('2025-10-31T00:00:00Z');
        const all = generateQuarterlyDueDates(start, end, 10);
        const endMs = end.getTime();
        const filtered = all.filter((x) => x.getTime() <= endMs);
        expect(filtered.map((x) => x.toISOString().slice(0, 7))).toEqual(['2025-09']);
    });

    it('full 12-month window yields all 4 quarterly dates (no filtering needed)', () => {
        const start = d('2026-04-01T00:00:00Z');
        const end = d('2027-03-31T00:00:00Z');
        const all = generateQuarterlyDueDates(start, end, 10);
        const endMs = end.getTime();
        const filtered = all.filter((x) => x.getTime() <= endMs);
        expect(filtered).toHaveLength(4);
    });
});

describe('Cross-year custom windows (user-specified Sept-Mar, etc.)', () => {
    it('Sept 2025 → March 2026 monthly, dueDay=10: 7 installments Sep 2025..Mar 2026', () => {
        const dates = generateMonthlyDueDates(
            d('2025-09-01T00:00:00Z'),
            d('2026-03-31T00:00:00Z'),
            10,
        );
        expect(dates).toHaveLength(7);
        expect(dates.map(x => x.toISOString().slice(0, 10))).toEqual([
            '2025-09-10', '2025-10-10', '2025-11-10', '2025-12-10',
            '2026-01-10', '2026-02-10', '2026-03-10',
        ]);
    });

    it('Sept 2025 → March 2026 quarterly: Sep 2025, Dec 2025, Mar 2026, Jun 2026', () => {
        const dates = generateQuarterlyDueDates(
            d('2025-09-01T00:00:00Z'),
            d('2026-03-31T00:00:00Z'),
            10,
        );
        expect(dates.map(x => x.toISOString().slice(0, 7))).toEqual([
            '2025-09', '2025-12', '2026-03', '2026-06',
        ]);
    });

    it('Jul 2026 → Feb 2027 monthly with dueDay=31 clamps each short month', () => {
        const dates = generateMonthlyDueDates(
            d('2026-07-01T00:00:00Z'),
            d('2027-02-28T00:00:00Z'),
            31,
        );
        expect(dates.map(x => x.toISOString().slice(0, 10))).toEqual([
            '2026-07-31', '2026-08-31', '2026-09-30', '2026-10-31',
            '2026-11-30', '2026-12-31', '2027-01-31', '2027-02-28',
        ]);
    });

    it('custom start inside AY but end after AY is valid (spans across AY boundary)', () => {
        // Admin sets startDate=Sep 2025, endDate=Mar 2026 on a fee in "AY 2025-2026"
        // even though AY runs Jun 2025 – May 2026. The custom window is narrower
        // on one side and crosses the year boundary. Helper treats it the same.
        const dates = generateMonthlyDueDates(
            d('2025-09-15T00:00:00Z'),
            d('2026-03-15T00:00:00Z'),
            15,
        );
        expect(dates).toHaveLength(7);
    });
});

describe('Regression: original buggy scenarios from screenshots', () => {
    it('Image #2: calendar AY 2027 Jan 1 → Dec 31, dueDay=10 → all 12 dates inside 2027', () => {
        const dates = generateMonthlyDueDates(d('2027-01-01T00:00:00Z'), d('2027-12-31T00:00:00Z'), 10);
        expect(dates).toHaveLength(12);
        for (const x of dates) expect(x.getUTCFullYear()).toBe(2027);
    });

    it('Image #1: April-start AY 2029-04-01 → 2030-03-31, dueDay=1 → Apr 2029 first, Mar 2030 last', () => {
        const dates = generateMonthlyDueDates(d('2029-04-01T00:00:00Z'), d('2030-03-31T00:00:00Z'), 1);
        expect(dates[0].toISOString().slice(0, 10)).toBe('2029-04-01');
        expect(dates[11].toISOString().slice(0, 10)).toBe('2030-03-01');
        // Verify no installment slips outside the AY window
        const firstMs = d('2029-04-01T00:00:00Z').getTime();
        const lastMs = d('2030-03-31T23:59:59Z').getTime();
        for (const x of dates) {
            expect(x.getTime()).toBeGreaterThanOrEqual(firstMs);
            expect(x.getTime()).toBeLessThanOrEqual(lastMs);
        }
    });
});
