import { describe, expect, it } from 'vitest';
import type { RuleSectionKey, RuleSectionValues } from '../types';
import {
    disableSection,
    editableWeekend,
    enableSection,
    ISO_DAYS,
    isSectionEnabled,
    normalizeFarOut,
    normalizeSeasonality,
    normalizeWeekend,
    pickSection,
    reattachSection,
    SECTION_KEYS,
    toggleWeekendDay,
    WEEKEND_DEFAULT_DAYS,
} from './rule-sections';

/** Même forme que YieldConfig::sectionDefaults() (saisons réduites à une). */
const DEFAULTS: RuleSectionValues = {
    seasonality: { base_pct: 0, seasons: [{ name: 'Très haute été', start_date: '2026-07-01', end_date: '2026-08-31', pct: 40 }] },
    last_minute_rules: [
        { days_before: 0, multiplier: 0.7, label: 'Même jour' },
        { days_before: 2, multiplier: 0.8, label: '2 jours' },
        { days_before: 7, multiplier: 0.9, label: '1 semaine' },
    ],
    far_out_premium: { threshold_days: 60, premium_pct: 0.1 },
    oba_rules: [{ window: [0, 15], thresholds: [{ occ: 10, mult: 0.85 }] }],
    // La SUGGESTION servie pour rallumer (YieldConfig::suggestedWeekendRule), pas le ×1 neutre.
    weekend_rule: { multiplier: 1.1, days: [5, 6] },
};

/** Même forme que YieldConfig::sectionDisabledValues(). */
const DISABLED: RuleSectionValues = {
    seasonality: { base_pct: 0, seasons: [] },
    last_minute_rules: [],
    far_out_premium: { threshold_days: 60, premium_pct: 0 },
    oba_rules: [],
    weekend_rule: { multiplier: 1, days: [5, 6] },
};

describe('isSectionEnabled', () => {
    it('voit les défauts comme actifs et les formes neutres comme coupées', () => {
        for (const ui of ['seasonality', 'lastminute', 'oba', 'weekend'] as const) {
            expect(isSectionEnabled(ui, DEFAULTS)).toBe(true);
            expect(isSectionEnabled(ui, DISABLED)).toBe(false);
        }
    });

    it('un socle de saison non nul suffit à rendre la section active', () => {
        expect(isSectionEnabled('seasonality', { ...DISABLED, seasonality: { base_pct: 5, seasons: [] } })).toBe(true);
    });

    it('une prime seule, sans palier, garde le last-minute actif', () => {
        expect(isSectionEnabled('lastminute', { ...DISABLED, far_out_premium: { threshold_days: 60, premium_pct: 0.1 } })).toBe(true);
    });
});

describe('disableSection / enableSection', () => {
    it('couper le last-minute coupe AUSSI la prime long terme, et rien d’autre', () => {
        const next = disableSection('lastminute', DEFAULTS, DISABLED);
        expect(next.last_minute_rules).toEqual([]);
        expect(next.far_out_premium).toEqual({ threshold_days: 60, premium_pct: 0 });
        expect(next.seasonality).toEqual(DEFAULTS.seasonality);
        expect(next.oba_rules).toEqual(DEFAULTS.oba_rules);
    });

    it('couper les saisons ou l’OBA écrit leur forme neutre', () => {
        expect(disableSection('seasonality', DEFAULTS, DISABLED).seasonality).toEqual({ base_pct: 0, seasons: [] });
        expect(disableSection('oba', DEFAULTS, DISABLED).oba_rules).toEqual([]);
    });

    it('rallumer restaure la valeur mémorisée à la coupure', () => {
        const custom: RuleSectionValues = { ...DEFAULTS, oba_rules: [{ window: [0, 15], thresholds: [{ occ: 20, mult: 1.2 }] }] };
        const remembered = pickSection('oba', custom);
        const off = disableSection('oba', custom, DISABLED);

        expect(enableSection('oba', off, remembered, DEFAULTS).oba_rules).toEqual(custom.oba_rules);
    });

    it('rallumer après avoir VIDÉ l’onglet repart des défauts, pas de la forme vide', () => {
        // Toutes les saisons supprimées à la main (l'onglet reste actif), puis coupé.
        const emptied: RuleSectionValues = { ...DEFAULTS, seasonality: { base_pct: 0, seasons: [] } };
        const remembered = pickSection('seasonality', emptied);
        const off = disableSection('seasonality', emptied, DISABLED);

        const next = enableSection('seasonality', off, remembered, DEFAULTS);
        expect(next.seasonality).toEqual(DEFAULTS.seasonality);
        expect(isSectionEnabled('seasonality', next)).toBe(true);
    });

    it('une valeur mémorisée qui a un effet par une seule de ses colonnes reste rendue', () => {
        // Aucun palier mais une prime : le last-minute était actif.
        const primeOnly: RuleSectionValues = { ...DEFAULTS, last_minute_rules: [], far_out_premium: { threshold_days: 45, premium_pct: 0.2 } };
        const next = enableSection('lastminute', disableSection('lastminute', primeOnly, DISABLED), pickSection('lastminute', primeOnly), DEFAULTS);
        expect(next.last_minute_rules).toEqual([]);
        expect(next.far_out_premium).toEqual({ threshold_days: 45, premium_pct: 0.2 });
    });

    it('rallumer sans valeur mémorisée repart des défauts du serveur', () => {
        const next = enableSection('lastminute', DISABLED, undefined, DEFAULTS);
        expect(next.last_minute_rules).toEqual(DEFAULTS.last_minute_rules);
        expect(next.far_out_premium).toEqual({ threshold_days: 60, premium_pct: 0.1 });
        expect(next.seasonality).toEqual(DISABLED.seasonality);
    });

    it('ne partage jamais un objet avec les valeurs du serveur', () => {
        const next = enableSection('seasonality', DISABLED, undefined, DEFAULTS);
        expect(next.seasonality).toEqual(DEFAULTS.seasonality);
        expect(next.seasonality).not.toBe(DEFAULTS.seasonality);
    });
});

describe('normalizeFarOut', () => {
    it('ramène le [] de PHP et les clés manquantes à la forme neutre', () => {
        expect(normalizeFarOut([])).toEqual({ threshold_days: 60, premium_pct: 0 });
        expect(normalizeFarOut(undefined)).toEqual({ threshold_days: 60, premium_pct: 0 });
        expect(normalizeFarOut({})).toEqual({ threshold_days: 60, premium_pct: 0 });
        // Sans seuil, le calculateur n'applique aucune prime : neutre aussi.
        expect(normalizeFarOut({ premium_pct: 0.1 })).toEqual({ threshold_days: 60, premium_pct: 0 });
    });

    it('garde une prime complète telle quelle, et complète une prime sans montant', () => {
        expect(normalizeFarOut({ threshold_days: 45, premium_pct: 0.2 })).toEqual({ threshold_days: 45, premium_pct: 0.2 });
        expect(normalizeFarOut({ threshold_days: 45 })).toEqual({ threshold_days: 45, premium_pct: 0 });
    });
});

describe('week-end', () => {
    it('chaque colonne de règle est pilotée par exactement un onglet', () => {
        const all: RuleSectionKey[] = ['seasonality', 'last_minute_rules', 'far_out_premium', 'oba_rules', 'weekend_rule'];
        const piloted = Object.values(SECTION_KEYS).flat();
        expect([...piloted].sort()).toEqual([...all].sort());
        expect(SECTION_KEYS.weekend).toEqual(['weekend_rule']);
    });

    it('n’est actif qu’avec un multiplicateur ≠ 1 ET au moins une nuit', () => {
        expect(isSectionEnabled('weekend', { ...DISABLED, weekend_rule: { multiplier: 1.1, days: [5, 6] } })).toBe(true);
        expect(isSectionEnabled('weekend', { ...DISABLED, weekend_rule: { multiplier: 0.9, days: [7] } })).toBe(true);
        expect(isSectionEnabled('weekend', { ...DISABLED, weekend_rule: { multiplier: 1, days: [5, 6] } })).toBe(false);
        expect(isSectionEnabled('weekend', { ...DISABLED, weekend_rule: { multiplier: 1.3, days: [] } })).toBe(false);
    });

    it('une colonne NULL côté serveur (clé absente) se lit comme le défaut : ×1, donc coupé', () => {
        const legacy = { ...DISABLED, weekend_rule: undefined } as unknown as typeof DISABLED;
        expect(isSectionEnabled('weekend', legacy)).toBe(false);
    });

    it('couper écrit la forme neutre du serveur, et ne touche à rien d’autre', () => {
        const custom = { ...DEFAULTS, weekend_rule: { multiplier: 1.25, days: [5, 6, 7] } };
        const next = disableSection('weekend', custom, DISABLED);
        expect(next.weekend_rule).toEqual({ multiplier: 1, days: [5, 6] });
        expect(next.oba_rules).toEqual(DEFAULTS.oba_rules);
        expect(next.last_minute_rules).toEqual(DEFAULTS.last_minute_rules);
    });

    it('rallumer sans mémoire repart de la suggestion ×1,10 — jamais du ×1 qui aurait l’air actif sans effet', () => {
        const next = enableSection('weekend', DISABLED, undefined, DEFAULTS);
        expect(next.weekend_rule).toEqual({ multiplier: 1.1, days: [5, 6] });
        expect(isSectionEnabled('weekend', next)).toBe(true);
    });

    it('rallumer rend la valeur mémorisée si elle avait un effet, sinon la suggestion', () => {
        const custom = { ...DEFAULTS, weekend_rule: { multiplier: 0.9, days: [7] } };
        const back = enableSection('weekend', disableSection('weekend', custom, DISABLED), pickSection('weekend', custom), DEFAULTS);
        expect(back.weekend_rule).toEqual({ multiplier: 0.9, days: [7] });

        // Une valeur mémorisée mal rangée (doublon, désordre) ressort normalisée :
        // l'éditeur ne doit jamais afficher deux fois la même case cochée.
        const messy = { ...DEFAULTS, weekend_rule: { multiplier: 1.2, days: [6, 5, 5] } };
        const tidy = enableSection('weekend', disableSection('weekend', messy, DISABLED), pickSection('weekend', messy), DEFAULTS);
        expect(tidy.weekend_rule).toEqual({ multiplier: 1.2, days: [5, 6] });

        const neutral = { ...DEFAULTS, weekend_rule: { multiplier: 1, days: [5] } };
        const fromNeutral = enableSection('weekend', disableSection('weekend', neutral, DISABLED), pickSection('weekend', neutral), DEFAULTS);
        expect(fromNeutral.weekend_rule).toEqual({ multiplier: 1.1, days: [5, 6] });
    });
});

describe('normalizeWeekend', () => {
    it('rien du tout = le défaut du serveur (×1 vendredi-samedi)', () => {
        expect(normalizeWeekend(undefined)).toEqual({ multiplier: 1, days: [5, 6] });
        expect(normalizeWeekend(null)).toEqual({ multiplier: 1, days: [5, 6] });
        expect(WEEKEND_DEFAULT_DAYS).toEqual([5, 6]);
    });

    it('une forme illisible ne produit aucun effet — jamais des nuits devinées', () => {
        expect(normalizeWeekend([])).toEqual({ multiplier: 1, days: [] });
        expect(normalizeWeekend('abc')).toEqual({ multiplier: 1, days: [] });
        expect(normalizeWeekend({ multiplier: 1.3 })).toEqual({ multiplier: 1.3, days: [] });
        expect(normalizeWeekend({ multiplier: 'beaucoup', days: [5] })).toEqual({ multiplier: 1, days: [5] });
        expect(normalizeWeekend({ multiplier: -2, days: [5] })).toEqual({ multiplier: 1, days: [5] });
    });

    it('garde les jours entiers de 1 à 7, sans doublon, triés', () => {
        expect(normalizeWeekend({ multiplier: '1.2', days: [6, 5, '5', 9, 0, 5.5, 1] })).toEqual({ multiplier: 1.2, days: [1, 5, 6] });
    });
});

describe('toggleWeekendDay', () => {
    const rule = { multiplier: 1.1, days: [5, 6] };

    it('ajoute une nuit en gardant la liste triée', () => {
        expect(toggleWeekendDay(rule, 1, true)).toEqual({ multiplier: 1.1, days: [1, 5, 6] });
    });

    it('retire une nuit, et ne duplique jamais', () => {
        expect(toggleWeekendDay(rule, 5, false)).toEqual({ multiplier: 1.1, days: [6] });
        expect(toggleWeekendDay(rule, 6, true)).toEqual(rule);
        expect(toggleWeekendDay(rule, 3, false)).toEqual(rule);
    });

    it('ne mute pas la règle reçue', () => {
        toggleWeekendDay(rule, 7, true);
        expect(rule.days).toEqual([5, 6]);
    });

    it('propose les sept nuits, lundi d’abord', () => {
        expect([...ISO_DAYS]).toEqual([1, 2, 3, 4, 5, 6, 7]);
    });
});

describe('editableWeekend (la règle telle que l’éditeur la tient)', () => {
    it('garde le multiplicateur TEL QUEL — 0, négatif, NaN d’un champ vidé — au lieu de le ramener à 1', () => {
        expect(editableWeekend({ multiplier: 0, days: [5, 6] })).toEqual({ multiplier: 0, days: [5, 6] });
        expect(editableWeekend({ multiplier: -1, days: [5] })).toEqual({ multiplier: -1, days: [5] });
        expect(editableWeekend({ multiplier: Number.NaN, days: [6] }).multiplier).toBeNaN();
        // normalizeWeekend, elle, lit une valeur STOCKÉE : ×1 plutôt qu'un effet deviné.
        expect(normalizeWeekend({ multiplier: 0, days: [5, 6] }).multiplier).toBe(1);
    });

    it('normalise les jours comme normalizeWeekend', () => {
        expect(editableWeekend({ multiplier: 0.9, days: [6, 5, '5', 9] })).toEqual({ multiplier: 0.9, days: [5, 6] });
        expect(editableWeekend({ multiplier: 0.9, days: { a: 5 } })).toEqual({ multiplier: 0.9, days: [] });
    });

    it('sans valeur, ou avec un multiplicateur non numérique, rend la lecture normalisée', () => {
        expect(editableWeekend(undefined)).toEqual({ multiplier: 1, days: [5, 6] });
        expect(editableWeekend({ multiplier: '1.2', days: [5] })).toEqual({ multiplier: 1.2, days: [5] });
        expect(editableWeekend('abc')).toEqual({ multiplier: 1, days: [] });
    });

    it('cocher une nuit ne touche pas au multiplicateur en cours de saisie', () => {
        expect(toggleWeekendDay(editableWeekend({ multiplier: 0, days: [5] }), 6, true)).toEqual({ multiplier: 0, days: [5, 6] });
    });
});

describe('normalizeSeasonality', () => {
    it('garde la forme par dates et complète les champs manquants', () => {
        expect(normalizeSeasonality({ base_pct: 5, seasons: [{ name: 'Été', start_date: '2026-07-01', end_date: '2026-08-31' }] })).toEqual({
            base_pct: 5,
            seasons: [{ name: 'Été', start_date: '2026-07-01', end_date: '2026-08-31', pct: 0 }],
        });
    });

    it('rend une saisonnalité vide pour l’ancienne forme par mois', () => {
        expect(normalizeSeasonality({ '01': { label: 'Janvier', multiplier: 0.9 } })).toEqual({ base_pct: 0, seasons: [] });
        expect(normalizeSeasonality(null)).toEqual({ base_pct: 0, seasons: [] });
    });

    /*
     * Récurrence, priorité, fête mobile : sans ces clés, ouvrir puis
     * enregistrer une configuration ferait perdre aux saisons leur récurrence
     * et à l'Aïd sa date mobile (l'aperçu et l'enregistrement partent de cette
     * valeur). toStrictEqual : une clé invalide doit être ABSENTE, pas undefined.
     */
    it('garde repeat, layer, holiday et les décalages quand ils sont valides', () => {
        const fitr = {
            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,
        };
        const basse = { name: 'Basse', start_date: '2026-11-01', end_date: '2026-12-20', pct: -15, repeat: 'yearly', layer: 'season' };

        expect(normalizeSeasonality({ base_pct: 0, seasons: [basse, fitr] })).toStrictEqual({ base_pct: 0, seasons: [basse, fitr] });
    });

    it('retire une clé facultative invalide, sans toucher au reste', () => {
        const base = { name: 'X', start_date: '2027-01-01', end_date: '2027-01-10', pct: 10 };
        const invalid = { ...base, repeat: 'monthly', layer: 'x', holiday: 'noel', offset_start: 1.5, offset_end: '2' };

        expect(normalizeSeasonality({ base_pct: 0, seasons: [invalid] })).toStrictEqual({ base_pct: 0, seasons: [base] });
        // Chaque clé valide passe SEULE (une règle de copie oubliée se verrait ici).
        expect(normalizeSeasonality({ base_pct: 0, seasons: [{ ...base, holiday: 'easter' }] }).seasons[0]).toStrictEqual({
            ...base,
            holiday: 'easter',
        });
        expect(normalizeSeasonality({ base_pct: 0, seasons: [{ ...base, offset_start: 0 }] }).seasons[0]).toStrictEqual({ ...base, offset_start: 0 });
        expect(normalizeSeasonality({ base_pct: 0, seasons: [{ ...base, offset_end: -3 }] }).seasons[0]).toStrictEqual({ ...base, offset_end: -3 });
    });
});

describe('reattachSection (« Revenir au groupe »)', () => {
    type Form = RuleSectionValues & { rule_overrides: RuleSectionKey[]; base_price: number };

    const form: Form = {
        ...DEFAULTS,
        weekend_rule: { multiplier: 1.4, days: [5] },
        rule_overrides: ['weekend_rule', 'oba_rules'],
        base_price: 80,
    };
    const group = {
        seasonality: null,
        last_minute_rules: null,
        far_out_premium: null,
        oba_rules: [{ window: [0, 30] as [number, number], thresholds: [{ occ: 50, mult: 1.2 }] }],
        weekend_rule: { multiplier: 1.3, days: [6, 5] },
    };

    it('week-end : reprend la valeur du GROUPE (normalisée) et sort de rule_overrides', () => {
        const next = reattachSection('weekend', form, group);

        expect(next.weekend_rule).toEqual({ multiplier: 1.3, days: [5, 6] });
        expect(next.rule_overrides).toEqual(['oba_rules']);
        // Les autres sections et le reste du formulaire ne bougent pas.
        expect(next.oba_rules).toBe(form.oba_rules);
        expect(next.base_price).toBe(80);
    });

    it('ne mute ni le formulaire ni le groupe', () => {
        const next = reattachSection('weekend', form, group);
        next.weekend_rule.days.push(7);

        expect(form.weekend_rule).toEqual({ multiplier: 1.4, days: [5] });
        expect(form.rule_overrides).toEqual(['weekend_rule', 'oba_rules']);
        expect(group.weekend_rule).toEqual({ multiplier: 1.3, days: [6, 5] });
    });

    it('une section que le groupe ne porte pas garde la valeur du formulaire', () => {
        const next = reattachSection('weekend', form, { ...group, weekend_rule: null });
        expect(next.weekend_rule).toEqual({ multiplier: 1.4, days: [5] });
        expect(next.rule_overrides).toEqual(['oba_rules']);
    });

    it('le last-minute reprend paliers ET prime du groupe ; la prime sans clés devient neutre', () => {
        const next = reattachSection(
            'lastminute',
            { ...form, rule_overrides: ['last_minute_rules', 'far_out_premium'] },
            { ...group, last_minute_rules: [], far_out_premium: [] as unknown as RuleSectionValues['far_out_premium'] },
        );
        expect(next.last_minute_rules).toEqual([]);
        expect(next.far_out_premium).toEqual({ threshold_days: 60, premium_pct: 0 });
        expect(next.rule_overrides).toEqual([]);
    });

    it('sans groupe, ne fait que retirer la section des personnalisations', () => {
        const next = reattachSection('oba', form, null);
        expect(next.oba_rules).toBe(form.oba_rules);
        expect(next.rule_overrides).toEqual(['weekend_rule']);
    });
});
