import { createElement, type ReactNode } from 'react';
import { renderToString } from 'react-dom/server';
import { describe, expect, it, vi } from 'vitest';
import type { ObaRule, RuleSectionValues } from '../../types';
import YieldRuleEditor, { type YieldConfigData } from '../YieldRuleEditor';
import ObaEditor from './ObaEditor';

/**
 * L'éditeur OBA tel qu'il sort du rendu SERVEUR (`react-dom/server`,
 * environnement `node` : ni jsdom ni @testing-library dans le projet).
 *
 * `lib/oba-rules.test.ts` prouve la logique ; ce test prouve que le composant
 * l'APPLIQUE : la ligne « Dès 0 % » n'a ni champ de taux ni suppression,
 * l'avertissement dit ce que le serveur refusera, le champ multiplicateur
 * commence à ×1, et les gestes (ajout, tri quand le focus quitte la carte,
 * suppression) remontent la bonne valeur — gestionnaires appelés à la main.
 *
 * La carte d'une échéance (ObaWindowCard) est doublée : son test à elle dit
 * QUAND le focus la quitte ; ici, on appelle `onFocusLeave` au moment où le
 * navigateur le ferait. Les scénarios « geste après geste » passent par
 * `mount`, qui rend à nouveau après chaque changement, comme le parent.
 */
const { inputs, buttons, cards } = vi.hoisted(() => ({
    inputs: [] as Array<Record<string, unknown> & { onChange?: (e: { target: { value: string } }) => void; onBlur?: () => void }>,
    buttons: [] as Array<Record<string, unknown> & { onClick?: () => void; disabled?: boolean; children?: unknown }>,
    cards: [] as Array<{ onFocusLeave: () => void }>,
}));
vi.mock('@/components/ui/input', async () => {
    const { createElement } = await import('react');
    return {
        Input: (props: Record<string, unknown>) => {
            inputs.push(props);
            return createElement('input', props);
        },
    };
});
vi.mock('@/components/ui/button', async () => {
    const { createElement } = await import('react');
    return {
        // Seuls les attributs DOM que les tests lisent : variant / size ne sont pas des attributs HTML.
        Button: (props: Record<string, unknown> & { children?: ReactNode }) => {
            buttons.push(props);
            return createElement(
                'button',
                { type: 'button', disabled: props.disabled, title: props.title, 'aria-label': props['aria-label'] },
                props.children,
            );
        },
    };
});

vi.mock('./ObaWindowCard', async () => {
    const { createElement } = await import('react');
    return {
        default: (props: { onFocusLeave: () => void; className?: string; children?: ReactNode }) => {
            cards.push(props);
            return createElement('div', { className: props.className }, props.children);
        },
    };
});

vi.mock('@inertiajs/react', () => ({
    usePage: () => ({ props: { auth: { permissions: ['yield.view', 'yield.manage'] }, translations: {}, locale: 'fr' } }),
    useForm: <T>(initial: T) => ({ data: initial, setData: vi.fn(), put: vi.fn(), processing: false }),
    router: { post: vi.fn(), put: vi.fn(), visit: vi.fn() },
}));

const noop = () => undefined;

const VALID: ObaRule[] = [
    {
        window: [0, 15],
        thresholds: [
            { occ: 0, mult: 1 },
            { occ: 60, mult: 1 },
            { occ: 100, mult: 1.05 },
        ],
    },
];

function render(value: ObaRule[], onChange: (rules: ObaRule[]) => void = noop): string {
    inputs.length = 0;
    buttons.length = 0;
    cards.length = 0;
    return renderToString(createElement(ObaEditor, { value, onChange, presets: {} }));
}

/**
 * Un parent contrôlé : chaque changement est RENDU À NOUVEAU avant le geste
 * suivant, comme useForm / useState le font — les gestionnaires lus ensuite
 * sont ceux du nouveau rendu, là où le défaut se cachait.
 */
function mount(initial: ObaRule[]) {
    const state = { value: initial, changes: [] as ObaRule[][] };
    const rerender = (): void => {
        render(state.value, (rules) => {
            state.value = rules;
            state.changes.push(rules);
            rerender();
        });
    };
    rerender();
    return state;
}

/**
 * Preset recommandé, échéance 0-15, dans sa forme LONGUE d'avant la fusion des
 * lignes ×1 sans effet : celle où le défaut a été vu à l'écran. Le preset du
 * code n'en garde que la ligne 0 % ; la table sert ici pour ses six lignes.
 */
const RECOMMENDED_0_15: ObaRule[] = [
    {
        window: [0, 15],
        thresholds: [0, 10, 30, 50, 60, 100].map((occ) => ({ occ, mult: 1 })),
    },
];

/** Champs de taux (« ≥ … % ») : min 0, max 100, sans pas décimal. */
const occInputs = () => inputs.filter((p) => p.max === 100);
/** Champs multiplicateur : pas de 0,01. */
const multInputs = () => inputs.filter((p) => p.step === '0.01');
/** Boutons dont le libellé accessible est donné. */
const buttonsLabelled = (label: RegExp) => buttons.filter((b) => typeof b['aria-label'] === 'string' && label.test(b['aria-label'] as string));
/** Bouton dont le texte contient `text`. */
const buttonWithText = (text: string) =>
    buttons.find((b) => (Array.isArray(b.children) ? b.children : [b.children]).some((child) => typeof child === 'string' && child.includes(text)));

describe('ObaEditor (rendu)', () => {
    it('la ligne « Dès 0 % » n’a pas de champ de taux, et ne se supprime pas', () => {
        const html = render(VALID);

        expect(html).toContain('Dès 0 %');
        // Trois seuils, deux champs de taux : la ligne 0 % n'en a pas.
        expect(multInputs()).toHaveLength(3);
        expect(occInputs()).toHaveLength(2);
        expect(occInputs().map((p) => p.value)).toEqual([60, 100]);

        const removable = buttonsLabelled(/^Supprimer le seuil$/);
        const locked = buttonsLabelled(/toujours 0 %/);
        expect(removable).toHaveLength(2);
        expect(locked).toHaveLength(1);
        expect(locked[0].disabled).toBe(true);
    });

    it('le multiplicateur commence à ×1 (indice de saisie aligné sur le serveur)', () => {
        render(VALID);
        for (const field of multInputs()) {
            expect(field.min).toBe(1);
            expect(field.max).toBe(2);
        }
    });

    it('dit que le remplissage ne fait que monter les prix, et ce qu’est une nuit vendable', () => {
        const html = render(VALID);
        expect(html).toContain('le remplissage ne fait que monter les prix');
        expect(html).toContain('les baisses viennent de la règle dernière minute');
        expect(html).toContain('nuits vendables');
    });

    it('avertit, avant l’enregistrement, d’une courbe qui baisse — et pas d’une courbe valide', () => {
        expect(render(VALID)).not.toContain('sera refusée');

        const down = render([
            {
                window: [0, 15],
                thresholds: [
                    { occ: 0, mult: 1.2 },
                    { occ: 50, mult: 1.1 },
                ],
            },
        ]);
        expect(down).toContain('Le multiplicateur baisse quand le remplissage monte');
    });

    it('avertit d’une ancienne table : ligne 0 % manquante et remise sous ×1', () => {
        const html = render([
            {
                window: [0, 15],
                thresholds: [
                    { occ: 10, mult: 0.85 },
                    { occ: 60, mult: 1 },
                ],
            },
        ]);
        expect(html).toContain('doit commencer par une ligne « Dès 0 % »');
        expect(html).toContain('Un multiplicateur est sous ×1');
        // Sans ligne 0 %, rien n'est verrouillé : chaque ligne garde son champ.
        expect(occInputs()).toHaveLength(2);
    });

    it('« Ajouter un seuil » remonte la table triée, le seuil à sa place, au même multiplicateur', () => {
        const changes: ObaRule[][] = [];
        render(VALID, (rules) => changes.push(rules));

        buttonWithText('Ajouter un seuil')?.onClick?.();

        expect(changes).toHaveLength(1);
        expect(changes[0][0].thresholds).toEqual([
            { occ: 0, mult: 1 },
            { occ: 60, mult: 1 },
            { occ: 70, mult: 1 },
            { occ: 100, mult: 1.05 },
        ]);
    });

    it('« Ajouter une échéance » part de [0 % → ×1], à la suite de la dernière', () => {
        const changes: ObaRule[][] = [];
        render(VALID, (rules) => changes.push(rules));

        buttonWithText('Ajouter une échéance')?.onClick?.();

        expect(changes[0][1]).toEqual({ window: [16, 30], thresholds: [{ occ: 0, mult: 1 }] });
    });

    it('taper un taux ne trie pas ; quitter la CARTE trie la valeur — et seulement si une ligne bouge', () => {
        const changes: ObaRule[][] = [];
        // La dernière ligne vient d'être tapée « 20 » : l'état que la sortie de la carte trouve.
        const typed: ObaRule[] = [
            {
                window: [0, 15],
                thresholds: [
                    { occ: 0, mult: 1 },
                    { occ: 60, mult: 1.1 },
                    { occ: 20, mult: 1.05 },
                ],
            },
        ];
        render(typed, (rules) => changes.push(rules));

        // Taper ne trie pas : la ligne reste sous le curseur (« 2 », début de « 20 »).
        occInputs()[1].onChange?.({ target: { value: '2' } });
        expect(changes[0][0].thresholds.map((t) => t.occ)).toEqual([0, 60, 2]);

        // Le champ « taux » ne trie plus en perdant le focus : c'est la carte qui décide.
        for (const field of occInputs()) {
            expect(field.onBlur).toBeUndefined();
        }

        // Le focus quitte la carte : la valeur est triée, chaque seuil garde son multiplicateur.
        cards[0].onFocusLeave();
        expect(changes[1][0].thresholds).toEqual([
            { occ: 0, mult: 1 },
            { occ: 20, mult: 1.05 },
            { occ: 60, mult: 1.1 },
        ]);

        // Déjà triée : quitter la carte ne remonte rien.
        changes.length = 0;
        render(VALID, (rules) => changes.push(rules));
        cards[0].onFocusLeave();
        expect(changes).toHaveLength(0);
    });

    it('taux retapé, puis Tab vers le multiplicateur de la MÊME ligne : c’est ce seuil-là qui change', () => {
        const form = mount(RECOMMENDED_0_15);

        // Le 60 % (4e champ de taux) devient 5 %.
        occInputs()[3].onChange?.({ target: { value: '5' } });
        expect(form.value[0].thresholds.map((t) => t.occ)).toEqual([0, 10, 30, 50, 5, 100]);

        // Tab : le navigateur retire le focus du champ « taux » (son blur, s'il en a un),
        // mais il reste dans la carte — ObaWindowCard ne signale rien, rien n'est trié.
        occInputs()[3].onBlur?.();
        // Le multiplicateur de cette ligne (5e champ ×, la ligne 0 % comprise) reçoit 1.2.
        multInputs()[4].onChange?.({ target: { value: '1.2' } });
        const edited = form.value[0].thresholds;
        expect(edited.find((t) => t.occ === 5)?.mult).toBe(1.2);
        expect(edited.find((t) => t.occ === 50)?.mult).toBe(1);

        // Puis le focus quitte l'échéance : trié, le ×1,2 suit SON seuil.
        cards[0].onFocusLeave();
        expect(form.value[0].thresholds).toEqual([
            { occ: 0, mult: 1 },
            { occ: 5, mult: 1.2 },
            { occ: 10, mult: 1 },
            { occ: 30, mult: 1 },
            { occ: 50, mult: 1 },
            { occ: 100, mult: 1 },
        ]);
    });

    it('taux retapé, puis corbeille de la MÊME ligne : c’est cette ligne qui part, le 50 % reste', () => {
        const form = mount(RECOMMENDED_0_15);

        occInputs()[3].onChange?.({ target: { value: '5' } });
        // Clic sur la corbeille de la ligne retapée : le mousedown retire le focus du champ
        // « taux » (son blur, s'il en a un) AVANT le clic — sans sortie de la carte.
        occInputs()[3].onBlur?.();
        buttonsLabelled(/^Supprimer le seuil$/)[3].onClick?.();

        // La suppression termine le geste : le reste revient trié.
        expect(form.value[0].thresholds.map((t) => t.occ)).toEqual([0, 10, 30, 50, 100]);
    });

    it('vider un champ de taux le laisse vide — pas un second « 0 % » signalé en doublon', () => {
        const form = mount(VALID);

        occInputs()[0].onChange?.({ target: { value: '' } });

        expect(form.value[0].thresholds[1].occ).toBeNaN();
        expect(occInputs()[0].value).toBe('');
        expect(render(form.value)).not.toContain('Deux seuils portent le même taux');
    });

    it('les bornes d’une échéance s’arrêtent au jour 365, et « Ajouter une échéance » se grise quand il n’y a plus de place', () => {
        render(VALID);
        const windowFields = inputs.filter((p) => p.max === 365);
        expect(windowFields).toHaveLength(2);
        expect(buttonWithText('Ajouter une échéance')?.disabled).toBe(false);

        const changes: ObaRule[][] = [];
        render([{ window: [0, 365], thresholds: [{ occ: 0, mult: 1 }] }], (rules) => changes.push(rules));
        expect(buttonWithText('Ajouter une échéance')?.disabled).toBe(true);
        buttonWithText('Ajouter une échéance')?.onClick?.();
        expect(changes).toHaveLength(0);
    });

    it('supprimer une ligne retire CELLE-LÀ ; la ligne 0 % reste', () => {
        const changes: ObaRule[][] = [];
        render(VALID, (rules) => changes.push(rules));

        buttonsLabelled(/^Supprimer le seuil$/)[0].onClick?.();
        expect(changes[0][0].thresholds).toEqual([
            { occ: 0, mult: 1 },
            { occ: 100, mult: 1.05 },
        ]);

        buttonsLabelled(/toujours 0 %/)[0].onClick?.();
        expect(changes[1][0].thresholds).toEqual(VALID[0].thresholds);
    });
});

/** Mêmes formes que YieldConfig::sectionDefaults() / sectionDisabledValues(), réduites. */
const SECTIONS: 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] },
};

const CONFIG: YieldConfigData = {
    id: 1,
    property_id: '100',
    internal_id: 100,
    property_name: 'Villa',
    is_active: true,
    min_price: 50,
    base_price: 80,
    max_price: 150,
    seasonality: { base_pct: 0, seasons: [] },
    last_minute_rules: [],
    oba_rules: VALID,
    far_out_premium: { threshold_days: 60, premium_pct: 0 },
    weekend_rule: { multiplier: 1, days: [5, 6] },
    rule_group_id: null,
    rule_overrides: [],
    rule_group: null,
    last_pushed_at: null,
    last_push_status: null,
};

describe('YieldRuleEditor — onglet OBA (rendu)', () => {
    it('l’onglet « OBA » porte l’ancre de la visite de yield/edit, sur son DÉCLENCHEUR', () => {
        const html = renderToString(
            createElement(YieldRuleEditor, {
                config: CONFIG,
                obaPresets: {},
                exchangeRate: 2.9,
                sectionDefaults: SECTIONS,
                sectionDisabled: SECTIONS,
            }),
        );

        expect(html).toMatch(/<button[^>]*role="tab"[^>]*data-tour="yield-edit-oba"[^>]*>(?:(?!<\/button>).)*OBA/);
    });
});
