import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { describe, expect, it } from 'vitest';
import type { RuleSectionKey } from '../types';
import { initialPreviewPayload, PREVIEW_FIELDS, previewKey, previewPayload, ruleGroupPayload, type RuleGroupForm } from './rule-payloads';

/** Les cinq sections de règles — celles des groupes et de rule_overrides (YieldRuleGroup::SECTIONS). */
const SECTIONS: RuleSectionKey[] = ['seasonality', 'last_minute_rules', 'far_out_premium', 'oba_rules', 'weekend_rule'];

/** Le formulaire de l'éditeur, réduit : les champs postés et ceux qui ne doivent PAS l'être. */
const form = {
    min_price: 50,
    base_price: 80,
    max_price: 150,
    seasonality: { base_pct: 0, seasons: [] },
    last_minute_rules: [{ days_before: 2, multiplier: 0.8, label: '2 j' }],
    oba_rules: [],
    far_out_premium: { threshold_days: 60, premium_pct: 0.1 },
    weekend_rule: { multiplier: 1.1, days: [5, 6] },
    rule_overrides: ['weekend_rule'],
};

describe('corps de l’aperçu (POST yield.preview)', () => {
    it('porte la bande de prix et les CINQ sections — week-end compris', () => {
        expect([...PREVIEW_FIELDS].sort()).toEqual(['base_price', 'max_price', 'min_price', ...SECTIONS].sort());

        const body = previewPayload(form);
        expect(body.weekend_rule).toEqual({ multiplier: 1.1, days: [5, 6] });
        expect(Object.keys(body).sort()).toEqual([...PREVIEW_FIELDS].sort());
        expect(body).not.toHaveProperty('rule_overrides');
    });

    it('poste le multiplicateur tel que saisi : le serveur refuse 0, l’aperçu le dit', () => {
        const body = previewPayload({ ...form, weekend_rule: { multiplier: 0, days: [5, 6] } });
        expect(body.weekend_rule).toEqual({ multiplier: 0, days: [5, 6] });
    });
});

describe('dépendance de l’aperçu débouncé (previewKey)', () => {
    it('change quand SEULE la règle week-end change — multiplicateur ou nuits', () => {
        const before = previewKey(form);
        expect(previewKey({ ...form, weekend_rule: { multiplier: 1.2, days: [5, 6] } })).not.toBe(before);
        expect(previewKey({ ...form, weekend_rule: { multiplier: 1.1, days: [5] } })).not.toBe(before);
        // Champ vidé (NaN) : la clé change aussi, l'aperçu part et revient en refus.
        expect(previewKey({ ...form, weekend_rule: { multiplier: Number.NaN, days: [5, 6] } })).not.toBe(before);
    });

    it('change pour chaque champ posté, et pour aucun autre', () => {
        const before = previewKey(form);
        for (const field of PREVIEW_FIELDS) {
            expect(previewKey({ ...form, [field]: 'changé' }), field).not.toBe(before);
        }
        const otherOverrides = { ...form, rule_overrides: [] };
        expect(previewKey(otherOverrides)).toBe(before);
        // Mêmes valeurs dans un nouvel objet : pas de recalcul inutile.
        expect(previewKey({ ...form, weekend_rule: { multiplier: 1.1, days: [5, 6] } })).toBe(before);
    });
});

describe('premier aperçu du panneau latéral (initialPreviewPayload)', () => {
    const served = { ...form, far_out_premium: [] as unknown, weekend_rule: { multiplier: 1.2, days: [6, 5] } };

    it('poste la règle week-end servie, normalisée comme à l’ouverture de l’éditeur', () => {
        const body = initialPreviewPayload(served);
        expect(body.weekend_rule).toEqual({ multiplier: 1.2, days: [5, 6] });
        expect(body.far_out_premium).toEqual({ threshold_days: 60, premium_pct: 0 });
        expect(Object.keys(body).sort()).toEqual([...PREVIEW_FIELDS].sort());
    });

    it('un serveur qui ne sert pas encore weekend_rule donne ×1 (aucun effet), pas un corps sans la clé', () => {
        const body = initialPreviewPayload({ ...served, weekend_rule: undefined });
        expect(body.weekend_rule).toEqual({ multiplier: 1, days: [5, 6] });
    });
});

describe('corps de l’enregistrement d’un groupe (ruleGroupPayload)', () => {
    const groupForm: RuleGroupForm = {
        name: 'Studios',
        description: '',
        include: { seasonality: false, lastMinute: true, oba: false, weekend: true },
        seasonality: { base_pct: 0, seasons: [] },
        lastMinute: [{ days_before: 2, multiplier: 0.8, label: '2 j' }],
        farOut: { threshold_days: 60, premium_pct: 0.1 },
        oba: [],
        weekend: { multiplier: 1.15, days: [5, 6] },
    };

    it('section week-end cochée → la règle éditée', () => {
        expect(ruleGroupPayload(groupForm).weekend_rule).toEqual({ multiplier: 1.15, days: [5, 6] });
    });

    it('section décochée → null EXPLICITE : la clé reste, sinon la modification ne retirerait rien', () => {
        const payload = ruleGroupPayload({ ...groupForm, include: { ...groupForm.include, weekend: false } });
        expect(payload).toHaveProperty('weekend_rule', null);
        expect(Object.keys(payload).sort()).toEqual(['description', 'name', ...SECTIONS].sort());
    });

    it('le multiplicateur part tel que saisi : 0 est refusé par le serveur, jamais changé en ×1 (qui couperait la règle)', () => {
        expect(ruleGroupPayload({ ...groupForm, weekend: { multiplier: 0, days: [5] } }).weekend_rule).toEqual({ multiplier: 0, days: [5] });
    });

    it('le last-minute emporte la prime ; les autres sections coupées partent à null ; description vide → null', () => {
        const payload = ruleGroupPayload(groupForm);
        expect(payload.last_minute_rules).toEqual(groupForm.lastMinute);
        expect(payload.far_out_premium).toEqual(groupForm.farOut);
        expect(payload.seasonality).toBeNull();
        expect(payload.oba_rules).toBeNull();
        expect(payload.description).toBeNull();
        expect(payload.name).toBe('Studios');
    });
});

/*
 * Branchement : les composants appellent CES fonctions, et non une copie à la
 * main du corps. Sans DOM de test, c'est le seul moyen de s'assurer que la
 * liste testée ci-dessus est bien celle qui part (même méthode que
 * yield-tour.test.ts pour les ancres).
 */
describe('branchement dans les composants', () => {
    /**
     * La source SANS blancs ni virgules finales : le test porte sur l'appel, pas
     * sur la façon dont Prettier le coupe en lignes.
     */
    const compact = (text: string): string => text.replace(/\s+/g, '').replace(/,([)}\]])/g, '$1');
    const source = (path: string): string => compact(readFileSync(resolve(__dirname, '..', path), 'utf8'));

    it('l’éditeur poste previewPayload(data) et relance l’aperçu sur previewKey(data), seulement', () => {
        const editor = source('components/YieldRuleEditor.tsx');
        expect(editor).toContain(compact("postYieldJson<{ preview: PriceDay[] }>(route('yield.preview', config.id), previewPayload(data))"));
        expect(editor).toContain(compact('const previewDependency = previewKey(data);'));
        expect(editor).toMatch(/refreshPreview\(\),800\);[^;]*;\/\/eslint-disable-next-linereact-hooks\/exhaustive-deps\},\[previewDependency\]\);/);
    });

    it('« Revenir au groupe » passe par reattachSection', () => {
        expect(source('components/YieldRuleEditor.tsx')).toContain(compact('setData((prev) => reattachSection(ui, prev, group));'));
    });

    it('le panneau latéral amorce l’aperçu avec initialPreviewPayload', () => {
        expect(source('components/YieldRuleSheet.tsx')).toContain(
            compact("postYieldJson<{ preview: PriceDay[] }>(route('yield.preview', configId), initialPreviewPayload(result.config))"),
        );
    });

    it('la feuille de groupe construit son corps avec ruleGroupPayload, interrupteur week-end compris', () => {
        const sheet = source('rule-groups/RuleGroupSheet.tsx');
        expect(sheet).toMatch(/constpayload=ruleGroupPayload\(\{[^;]*include:\{[^}]*weekend:incWeekend[^}]*\}[^;]*weekend\}\)/);
        expect(sheet).toContain(compact("router.put(route('yield.rule-groups.update', group.id), payload, opts)"));
        expect(sheet).toContain(compact("router.post(route('yield.rule-groups.store'), payload, opts)"));
    });
});
