import { describe, expect, it } from 'vitest';
import { applyRangeToRows, isPastPeriod, serializeRows, subtractRange, toMinStayRows, type MinStayRow } from './min-stay-range-utils';

const row = (from: string, to: string, minStay: number): MinStayRow => ({ key: `k-${from}`, date_from: from, date_to: to, min_stay: minStay });

/** Every day covered by a table, with the value that applies to it. */
const dayMap = (rows: MinStayRow[]): Record<string, number[]> => {
    const map: Record<string, number[]> = {};
    for (const r of rows) {
        for (let d = new Date(`${r.date_from}T00:00:00Z`); d <= new Date(`${r.date_to}T00:00:00Z`); d.setUTCDate(d.getUTCDate() + 1)) {
            const key = d.toISOString().slice(0, 10);
            map[key] = [...(map[key] ?? []), r.min_stay];
        }
    }
    return map;
};

describe('subtractRange', () => {
    it('leaves a disjoint period untouched', () => {
        expect(subtractRange(row('2026-08-01', '2026-08-10', 3), '2026-09-01', '2026-09-10')).toEqual([
            expect.objectContaining({ date_from: '2026-08-01', date_to: '2026-08-10' }),
        ]);
    });

    it('treats a shared boundary day as an overlap, not adjacency', () => {
        // Bounds are inclusive: 08-10 belongs to both, so the period must yield it.
        const fragments = subtractRange(row('2026-08-01', '2026-08-10', 3), '2026-08-10', '2026-08-20');
        expect(fragments).toHaveLength(1);
        expect(fragments[0].date_to).toBe('2026-08-09');
    });

    it('clips the left side', () => {
        const fragments = subtractRange(row('2026-08-01', '2026-08-31', 3), '2026-07-15', '2026-08-10');
        expect(fragments).toHaveLength(1);
        expect(fragments[0]).toMatchObject({ date_from: '2026-08-11', date_to: '2026-08-31' });
    });

    it('splits a period covered in its middle into two fragments', () => {
        const fragments = subtractRange(row('2026-08-01', '2026-08-31', 3), '2026-08-10', '2026-08-20');
        expect(fragments).toHaveLength(2);
        expect(fragments[0]).toMatchObject({ date_from: '2026-08-01', date_to: '2026-08-09', min_stay: 3 });
        expect(fragments[1]).toMatchObject({ date_from: '2026-08-21', date_to: '2026-08-31', min_stay: 3 });
    });

    it('drops a period entirely swallowed by the range', () => {
        expect(subtractRange(row('2026-08-05', '2026-08-09', 3), '2026-08-01', '2026-08-31')).toEqual([]);
    });

    it('gives the fragments distinct keys so React never reuses a row', () => {
        const [left, right] = subtractRange(row('2026-08-01', '2026-08-31', 3), '2026-08-10', '2026-08-20');
        expect(left.key).not.toBe(right.key);
    });
});

describe('applyRangeToRows', () => {
    it('never leaves two different values on the same day', () => {
        // C'est l'invariant que la validation serveur exige : sans le rognage,
        // ce cas partirait en « périodes en conflit » au moment d'enregistrer.
        const result = applyRangeToRows([row('2026-08-01', '2026-08-31', 3)], '2026-08-10', '2026-08-20', 7);

        for (const values of Object.values(dayMap(result))) {
            expect(new Set(values).size).toBe(1);
        }
    });

    it('keeps the untouched days on their original value', () => {
        const map = dayMap(applyRangeToRows([row('2026-08-01', '2026-08-31', 3)], '2026-08-10', '2026-08-20', 7));

        expect(map['2026-08-09']).toEqual([3]);
        expect(map['2026-08-10']).toEqual([7]);
        expect(map['2026-08-20']).toEqual([7]);
        expect(map['2026-08-21']).toEqual([3]);
    });

    it('applies across several periods at once', () => {
        const result = applyRangeToRows(
            [row('2026-08-01', '2026-08-10', 2), row('2026-08-15', '2026-08-25', 4), row('2026-09-01', '2026-09-10', 6)],
            '2026-08-05',
            '2026-08-20',
            9,
        );
        const map = dayMap(result);

        expect(map['2026-08-04']).toEqual([2]);
        expect(map['2026-08-05']).toEqual([9]);
        // 08-12 était un trou entre deux périodes : la plage appliquée le couvre
        // comme n'importe quel autre jour de [from, to]. C'est voulu — l'action
        // « appliquer à une plage » porte sur la plage entière, pas seulement
        // sur les jours déjà restreints.
        expect(map['2026-08-12']).toEqual([9]);
        expect(map['2026-08-20']).toEqual([9]);
        expect(map['2026-08-21']).toEqual([4]);
        expect(map['2026-09-05']).toEqual([6]);
        // Le trou qui n'est PAS dans la plage appliquée reste un trou.
        expect(map['2026-08-28']).toBeUndefined();
    });

    it('adding a single day inside an existing period carves it instead of stacking', () => {
        // Le bouton « Ajouter une période » pose aujourd'hui..aujourd'hui : sans
        // rognage, le jour porterait deux valeurs et le serveur rejetterait tout.
        const result = applyRangeToRows([row('2026-08-01', '2026-08-31', 5)], '2026-08-15', '2026-08-15', 2);

        expect(dayMap(result)['2026-08-15']).toEqual([2]);
        expect(result).toHaveLength(3);
    });

    it('returns rows sorted by start date', () => {
        const result = applyRangeToRows([row('2026-09-01', '2026-09-10', 2)], '2026-08-01', '2026-08-10', 4);
        expect(result.map((r) => r.date_from)).toEqual(['2026-08-01', '2026-09-01']);
    });
});

describe('toMinStayRows / serializeRows', () => {
    it('sorts and coerces the server payload', () => {
        const rows = toMinStayRows([
            { date_from: '2026-09-01', date_to: '2026-09-10', min_stay: 4 },
            { date_from: '2026-08-01', date_to: '2026-08-10', min_stay: '3' as unknown as number },
        ]);

        expect(rows.map((r) => r.date_from)).toEqual(['2026-08-01', '2026-09-01']);
        expect(rows[0].min_stay).toBe(3);
    });

    it('falls back to one night on an unusable value', () => {
        // Une valeur 0/NaN venue de RU ne doit pas se propager : la spec impose > 0.
        expect(toMinStayRows([{ date_from: '2026-08-01', date_to: '2026-08-02', min_stay: 0 }])[0].min_stay).toBe(1);
    });

    it('fingerprints content only, ignoring row keys', () => {
        const a: MinStayRow = { key: 'a', date_from: '2026-08-01', date_to: '2026-08-10', min_stay: 3 };
        const b: MinStayRow = { key: 'b', date_from: '2026-08-01', date_to: '2026-08-10', min_stay: 3 };

        expect(serializeRows([a])).toBe(serializeRows([b]));
    });
});

describe('isPastPeriod', () => {
    it('is past only once the period has fully elapsed', () => {
        const today = '2026-08-03';

        expect(isPastPeriod({ date_from: '2026-07-01', date_to: '2026-08-02', min_stay: 3 }, today)).toBe(true);
        expect(isPastPeriod({ date_from: '2026-07-01', date_to: '2026-08-03', min_stay: 3 }, today)).toBe(false);
        expect(isPastPeriod({ date_from: '2026-07-01', date_to: '2026-09-01', min_stay: 3 }, today)).toBe(false);
    });
});
