import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { describe, expect, it } from 'vitest';
import { addDays, projectTemplate, rangesOverlap, type SeasonTemplate } from '../../logements/season-range-utils';
import type { MovableHolidays, YieldSeason, YieldSeasonality } from '../types';
import {
    flattenSeasonSegments,
    formatDayMonthYear,
    isCompleteSeason,
    nextOccurrence,
    occupiedRanges,
    offsetWindowLabel,
    resolveSeason,
    seasonMatches,
    seasonOccurrences,
    seasonStatus,
    templateChipResolver,
    templateEvent,
    withoutBackgroundOverlapping,
    type SeasonInput,
} from './season-calendar';

/**
 * La copie écran du moteur des saisons. Le fichier de cas
 * tests/Fixtures/yield/season-calendar-cases.json est parcouru ICI et par
 * SeasonCalendarTest (PHPUnit) : les deux côtés rendent les mêmes nuits, aux
 * mêmes bords (31/12, 29/02, fêtes absentes de la table, entrées illisibles).
 */

const REPO_ROOT = resolve(__dirname, '../../../../..');
const FIXTURES = resolve(REPO_ROOT, 'tests/Fixtures/yield');

type Cases = {
    holidays: MovableHolidays;
    resolve: Array<{ case: string; seasonality: unknown; nights: Record<string, [number, string | null]> }>;
    occurrences: Array<{ case: string; season: SeasonInput; from: string; to: string; expected: Array<[string, string]> }>;
};

const CASES = JSON.parse(readFileSync(resolve(FIXTURES, 'season-calendar-cases.json'), 'utf8')) as Cases;

/** Les populations A et B de production, telles que la reprise les écrit (fixtures partagées avec PHPUnit). */
const population = (letter: 'a' | 'b') =>
    JSON.parse(readFileSync(resolve(FIXTURES, `prod-seasonality-population-${letter}.json`), 'utf8')) as {
        stored_raw: string;
        converted: YieldSeasonality;
    };
const POPULATION_A = population('a');
const POPULATION_B = population('b');

/** La table livrée (config/yield-holidays.php), pour les dates de 2027. */
const HOLIDAYS: MovableHolidays = {
    eid_al_fitr: { dates: ['2026-03-20', '2027-03-10', '2028-02-26', '2029-02-14', '2030-02-04', '2031-01-24'], confirmed: [] },
    eid_al_adha: { dates: ['2026-05-27', '2027-05-17', '2028-05-05', '2029-04-24', '2030-04-13', '2031-04-02'], confirmed: [] },
    easter: { dates: ['2027-03-28'], confirmed: ['2027-03-28'] },
};

/** Les gabarits de config/season-templates.php (le test porte sa propre copie, comme season-range-utils.test.ts). */
const TEMPLATES: SeasonTemplate[] = [
    { name: 'Moyenne', start: '04-16', end: '04-27', intensity: 'mid' },
    { name: 'Vac. Pâques', start: '04-28', end: '05-04', intensity: 'event' },
    { name: 'Aïd Adha', start: '05-14', end: '05-17', intensity: 'event', holiday: 'eid_al_adha', offsets: [-2, 1] },
    { name: 'Pic juin', start: '06-15', end: '06-30', intensity: 'high' },
    { name: 'Très haute été', start: '07-01', end: '08-31', intensity: 'peak' },
    { name: 'Haute sept', start: '09-01', end: '09-15', intensity: 'high' },
    { name: 'Basse', start: '11-01', end: '12-20', intensity: 'low' },
    { name: 'Fêtes / Diaspora', start: '12-21', end: '01-06', intensity: 'peak' },
    { name: 'Basse hiver', start: '01-07', end: '03-19', intensity: 'low' },
    { name: 'Aïd Fitr', start: '03-05', end: '03-10', intensity: 'event', holiday: 'eid_al_fitr', offsets: [-2, 1] },
];

const BASSE_HIVER: YieldSeason = { name: 'Basse hiver', start_date: '2027-01-07', end_date: '2027-03-19', pct: -15, repeat: 'yearly' };
const FITR: YieldSeason = {
    name: 'Aïd Fitr',
    start_date: '2027-03-08',
    end_date: '2027-03-11',
    pct: 30,
    layer: 'event',
    holiday: 'eid_al_fitr',
    offset_start: -2,
    offset_end: 1,
};

describe('fichier de cas partagé avec PHPUnit', () => {
    it.each(CASES.resolve.map((c) => [c.case, c] as const))('résolution — %s', (_label, testCase) => {
        for (const [night, [multiplier, name]] of Object.entries(testCase.nights)) {
            const got = resolveSeason(testCase.seasonality, night, CASES.holidays);
            expect(got.multiplier, `${testCase.case} — ${night}`).toBeCloseTo(multiplier, 9);
            expect(got.name, `${testCase.case} — ${night}`).toBe(name);
        }
    });

    it.each(CASES.occurrences.map((c) => [c.case, c] as const))('occurrences — %s', (_label, testCase) => {
        const got = seasonOccurrences(testCase.season, testCase.from, testCase.to, CASES.holidays).map((r) => [r.start_date, r.end_date]);
        expect(got).toEqual(testCase.expected);

        // Et, jour par jour, seasonMatches() répond vrai exactement sur ces plages
        // (rognées à [from, to] : une plage qui finit le 31/12/9999 ne boucle
        // pas sur l'an 10000).
        const covered = new Set<string>();
        for (const [from, to] of testCase.expected) {
            const first = from > testCase.from ? from : testCase.from;
            const last = to < testCase.to ? to : testCase.to;
            for (let day = first; day <= last; day = addDays(day, 1)) covered.add(day);
        }
        for (let day = testCase.from; day <= testCase.to; day = addDays(day, 1)) {
            expect(seasonMatches(testCase.season, day, CASES.holidays), `${testCase.case} — ${day}`).toBe(covered.has(day));
        }
    });

    it('le fichier couvre les bords exigés : 31/12, 29/02, événement, fête absente, entrées illisibles', () => {
        const labels = [...CASES.resolve, ...CASES.occurrences].map((c) => c.case).join(' | ');
        for (const edge of ['31/12', '29/02', 'événement', 'absente de la table', 'illisibles']) {
            expect(labels).toContain(edge);
        }
    });
});

describe('flattenSeasonSegments (frise)', () => {
    it('un événement dans une saison de fond donne trois segments DISJOINTS qui couvrent la fenêtre', () => {
        const segments = flattenSeasonSegments([BASSE_HIVER, FITR], '2027-03-01', '2027-03-19', HOLIDAYS);

        expect(segments.map((s) => [s.start_date, s.end_date, s.season?.name ?? null])).toEqual([
            ['2027-03-01', '2027-03-07', 'Basse hiver'],
            ['2027-03-08', '2027-03-11', 'Aïd Fitr'],
            ['2027-03-12', '2027-03-19', 'Basse hiver'],
        ]);
        const days = segments.reduce((sum, s) => sum + (Date.parse(s.end_date) - Date.parse(s.start_date)) / 86_400_000 + 1, 0);
        expect(days).toBe(19);
    });

    it('une saison annuelle qui passe le 1er janvier est découpée à ses vraies bornes, le socle ailleurs', () => {
        const fetes: YieldSeason = { name: 'Fêtes', start_date: '2026-12-21', end_date: '2027-01-06', pct: 40, repeat: 'yearly' };
        const segments = flattenSeasonSegments([fetes], '2027-12-15', '2028-01-10', HOLIDAYS);

        expect(segments.map((s) => [s.start_date, s.end_date, s.index])).toEqual([
            ['2027-12-15', '2027-12-20', null],
            ['2027-12-21', '2028-01-06', 0],
            ['2028-01-07', '2028-01-10', null],
        ]);
    });

    it('la population A reprise, sur un an : chaque nuit a la saison que le moteur lui donne', () => {
        const seasons = POPULATION_A.converted.seasons;
        const segments = flattenSeasonSegments(seasons, '2026-09-25', '2027-09-25', HOLIDAYS);
        for (const segment of segments) {
            for (let day = segment.start_date; day <= segment.end_date; day = addDays(day, 1)) {
                expect(resolveSeason(POPULATION_A.converted, day, HOLIDAYS).name, day).toBe(segment.season?.name ?? null);
            }
        }
        // Les nuits de l'Aïd el-Fitr 2027 sortent de « Basse hiver ».
        expect(segments.find((s) => s.season?.name === 'Aïd Fitr')).toMatchObject({ start_date: '2027-03-08', end_date: '2027-03-11' });
    });
});

/*
 * La galerie de gabarits raisonne sur les OCCURRENCES : une saison annuelle
 * garde les dates de sa première occurrence (« Haute sept » 2026-09-01 →
 * 09-15 pour la population A). Comparée aux dates brutes, la puce « Haute
 * sept », projetée sur 2027, redevenait cliquable — et son ajout répondait
 * 422 (deux saisons de fond sur les mêmes nuits).
 */
describe('occupiedRanges (galerie de gabarits)', () => {
    const TODAY = '2026-09-25';
    const blockedChips = (existing: ReturnType<typeof occupiedRanges>) =>
        TEMPLATES.filter((tpl) => {
            const range = projectTemplate(tpl, TODAY);
            return existing.some((r) => rangesOverlap(range, r));
        }).map((tpl) => tpl.name);

    it('au 2026-09-25, la puce « Haute sept » est bloquée pour la population A reprise', () => {
        const existing = occupiedRanges(POPULATION_A.converted.seasons, TODAY, HOLIDAYS);
        expect(blockedChips(existing)).toContain('Haute sept');

        // Avec les dates brutes, elle ne l'était pas : c'est le défaut corrigé.
        const raw = POPULATION_A.converted.seasons.map((s) => ({ start_date: s.start_date, end_date: s.end_date }));
        expect(TEMPLATES.filter((tpl) => tpl.name === 'Haute sept' && raw.some((r) => rangesOverlap(projectTemplate(tpl, TODAY), r)))).toEqual([]);
    });

    it.each([
        ['A', POPULATION_A],
        ['B', POPULATION_B],
    ] as const)('population %s reprise : TOUTES les puces sont grisées, les deux Aïd compris — elle a déjà chaque saison', (_label, pop) => {
        const chipFor = templateChipResolver(pop.converted.seasons, TODAY, HOLIDAYS);
        const free = TEMPLATES.filter((tpl) => chipFor(tpl).conflict === undefined).map((tpl) => tpl.name);
        expect(free).toEqual([]);

        // La puce « Aïd Adha » affiche la fenêtre mobile (table : 17/05/2027),
        // pas ses MM-JJ fixes, et c'est l'Aïd Adha déjà présent qui la grise.
        const adha = chipFor(TEMPLATES.find((tpl) => tpl.name === 'Aïd Adha')!);
        expect(adha.range).toEqual({ start_date: '2027-05-15', end_date: '2027-05-18' });
        expect(adha.conflict).toMatchObject({ name: 'Aïd Adha', start_date: '2027-05-15', end_date: '2027-05-18' });
    });

    it('une puce à fête n’est pas grisée par la saison de fond qui la contient : elle ajoute l’événement mobile', () => {
        const chipFor = templateChipResolver([BASSE_HIVER], TODAY, HOLIDAYS);
        const fitrTpl = TEMPLATES.find((tpl) => tpl.name === 'Aïd Fitr')!;

        expect(chipFor(fitrTpl)).toEqual({ range: { start_date: '2027-03-08', end_date: '2027-03-11' } });
        expect(templateEvent(fitrTpl, 30, TODAY, HOLIDAYS)).toEqual(FITR);
        // La puce de fond, elle, reste grisée par « Basse hiver ».
        expect(chipFor(TEMPLATES.find((tpl) => tpl.name === 'Basse hiver')!).conflict).toMatchObject({ name: 'Basse hiver' });
    });

    it('une puce à fête est grisée par un événement qui occupe une de ses fenêtres À VENIR, pas seulement la prochaine', () => {
        // Événement fixe du 10 au 16/02 : l'Aïd el-Fitr n'y tombe qu'en 2029 (14/02 → 12 → 15/02).
        const salon: YieldSeason = { name: 'Salon', start_date: '2027-02-10', end_date: '2027-02-16', pct: 10, layer: 'event', repeat: 'yearly' };
        const chip = templateChipResolver([salon], TODAY, HOLIDAYS)(TEMPLATES.find((tpl) => tpl.name === 'Aïd Fitr')!);

        expect(chip.range).toEqual({ start_date: '2027-03-08', end_date: '2027-03-11' });
        expect(chip.conflict).toEqual({ name: 'Salon', start_date: '2029-02-10', end_date: '2029-02-16' });
    });

    it('templateEvent : table épuisée, l’occurrence du gabarit, début NON ramené à aujourd’hui', () => {
        const adhaTpl = TEMPLATES.find((tpl) => tpl.name === 'Aïd Adha')!;

        expect(templateEvent(adhaTpl, 30, '2027-05-15', {})).toEqual({
            name: 'Aïd Adha',
            start_date: '2027-05-14',
            end_date: '2027-05-17',
            pct: 30,
            layer: 'event',
            holiday: 'eid_al_adha',
            offset_start: -2,
            offset_end: 1,
        });
        expect(templateEvent(TEMPLATES.find((tpl) => tpl.name === 'Haute sept')!, 20, TODAY, HOLIDAYS)).toBeNull();
        expect(templateEvent({ ...adhaTpl, holiday: 'noel' }, 30, TODAY, HOLIDAYS)).toBeNull();
    });

    it('les événements n’occupent rien : une saison de fond a le droit de les contenir', () => {
        expect(occupiedRanges([FITR], TODAY, HOLIDAYS)).toEqual([]);
        expect(withoutBackgroundOverlapping([BASSE_HIVER, FITR], { start_date: '2027-03-01', end_date: '2027-03-31' }, HOLIDAYS)).toEqual([FITR]);
        expect(withoutBackgroundOverlapping([BASSE_HIVER, FITR], { start_date: '2027-06-01', end_date: '2027-06-30' }, HOLIDAYS)).toEqual([
            BASSE_HIVER,
            FITR,
        ]);
    });

    it('une ligne en cours de saisie (pas encore de date de début) n’est jamais retirée par une puce', () => {
        const draft: YieldSeason = { name: 'Promo', start_date: '', end_date: '2027-12-31', pct: 15 };
        const ete: YieldSeason = { name: 'Été', start_date: '2027-07-01', end_date: '2027-08-31', pct: 40 };

        expect(withoutBackgroundOverlapping([draft, ete], { start_date: '2027-06-15', end_date: '2027-06-30' }, HOLIDAYS)).toEqual([draft, ete]);
        // Une ligne complète qui touche la plage, elle, est retirée.
        expect(withoutBackgroundOverlapping([draft, ete], { start_date: '2027-08-15', end_date: '2027-09-15' }, HOLIDAYS)).toEqual([draft]);
    });
});

describe('ce que la ligne dit d’elle-même (seasonStatus)', () => {
    it('annuelle : la prochaine occurrence, jamais « estimée »', () => {
        const haute: YieldSeason = { name: 'Haute sept', start_date: '2026-09-01', end_date: '2026-09-15', pct: 20, repeat: 'yearly' };
        expect(seasonStatus(haute, '2026-09-25', HOLIDAYS)).toEqual({
            next: { start_date: '2027-09-01', end_date: '2027-09-15' },
            estimated: false,
            missing: false,
        });
    });

    it('fête : la prochaine fenêtre d’après la table, estimée tant que la date n’est pas confirmée', () => {
        expect(seasonStatus(FITR, '2026-09-25', HOLIDAYS)).toEqual({
            next: { start_date: '2027-03-08', end_date: '2027-03-11' },
            estimated: true,
            missing: false,
        });
        const confirmed = { ...HOLIDAYS, eid_al_fitr: { dates: ['2027-03-10'], confirmed: ['2027-03-10'] } };
        expect(seasonStatus(FITR, '2026-09-25', confirmed).estimated).toBe(false);
    });

    it('fête sans date dans les 12 prochains mois : signalée, sans prochaine occurrence', () => {
        expect(seasonStatus(FITR, '2031-02-01', HOLIDAYS)).toEqual({ next: null, estimated: false, missing: true });
        // Une date connue mais à plus d’un an : signalée aussi.
        const far = { eid_al_fitr: { dates: ['2028-02-26'] } };
        expect(seasonStatus(FITR, '2026-09-25', far).missing).toBe(true);
        expect(seasonStatus(FITR, '2027-03-01', far).missing).toBe(false);
    });

    it('ponctuelle : ses dates si elles ne sont pas passées', () => {
        const promo: YieldSeason = { name: 'Promo', start_date: '2026-10-01', end_date: '2026-10-10', pct: -10 };
        expect(nextOccurrence(promo, '2026-09-25', HOLIDAYS)).toEqual({ start_date: '2026-10-01', end_date: '2026-10-10' });
        expect(nextOccurrence(promo, '2026-10-11', HOLIDAYS)).toBeNull();
    });
});

describe('petits outils', () => {
    it('offsetWindowLabel écrit la fenêtre comme le badge', () => {
        expect(offsetWindowLabel([-2, 1], 'J')).toBe('J-2 → J+1');
        expect(offsetWindowLabel([0, 3], 'D')).toBe('D → D+3');
    });

    it('formatDayMonthYear', () => {
        expect(formatDayMonthYear('2027-03-08')).toBe('08/03/2027');
    });

    it('isCompleteSeason : une fête, ou deux dates valides dans l’ordre', () => {
        expect(isCompleteSeason(FITR)).toBe(true);
        expect(isCompleteSeason(BASSE_HIVER)).toBe(true);
        expect(isCompleteSeason({ ...BASSE_HIVER, start_date: '' })).toBe(false);
        expect(isCompleteSeason({ ...BASSE_HIVER, start_date: '2027-04-01' })).toBe(false);
        expect(isCompleteSeason({ ...BASSE_HIVER, end_date: '2027-02-30' })).toBe(false);
    });
});
