import { readFileSync } from 'node:fs';
import nspell from 'nspell';
import { beforeAll, describe, expect, it } from 'vitest';
import {
    collectCheckableWords,
    createFrenchDictionary,
    createWhitelist,
    diffWords,
    MIN_WORDS_FOR_LANGUAGE_GUESS,
    proofread,
    type FrenchDictionary,
} from './index';

// Fumée : le VRAI dictionnaire, tel qu'il est servi depuis public/.
let dictionary: FrenchDictionary;
const extraWords = ['Villa des Artistes', 'Sarah Ben Salah'];

beforeAll(() => {
    // Lus en UTF-8, exactement comme le navigateur les reçoit via `response.text()`
    // (`SET UTF-8` dans index.aff). Les passer en Buffer marcherait aussi à
    // l'exécution, mais ne reproduirait pas le chemin réel.
    const speller = nspell(readFileSync('public/dictionaries/fr/index.aff', 'utf8'), readFileSync('public/dictionaries/fr/index.dic', 'utf8'));
    dictionary = createFrenchDictionary(speller);
});

describe('relecture sur des messages réels', () => {
    it('redresse un message français mal tapé', async () => {
        const entree = 'bonjour,votre sejour a la villa commence demain .merci de confirmer votre heure d arrivee';
        const r = await proofread(entree, { dictionary, extraWords });
        console.log('FR  →', JSON.stringify(r.corrected), '| fr =', r.isFrench);
        console.log(
            '     mots signalés :',
            r.issues.filter((i) => i.kind === 'orthographe').map((i) => `${i.from}→[${i.suggestions.join(',')}]`),
        );
        expect(r.dictionaryAvailable).toBe(true);
        expect(r.isFrench).toBe(true);
        expect(r.corrected).toContain('Bonjour, votre');
        expect(r.corrected).toContain('séjour');
        expect(r.corrected).toContain('arrivée');
        expect(diffWords(entree, r.corrected).some((s) => s.type !== 'same')).toBe(true);
    });

    it('laisse un message anglais intact', async () => {
        const entree = 'Hello, could you please confirm your arrival time? Thanks a lot.';
        const r = await proofread(entree, { dictionary, extraWords });
        console.log('EN  →', JSON.stringify(r.corrected), '| fr =', r.isFrench);
        expect(r.isFrench).toBe(false);
        expect(r.corrected).toBe(entree);
    });

    it('ne signale ni le logement ni le voyageur', async () => {
        const r = await proofread('Bonjour Sarah Ben Salah, bienvenue à la Villa des Artistes.', { dictionary, extraWords });
        console.log(
            'NOMS→',
            r.issues.filter((i) => i.kind === 'orthographe').map((i) => i.from),
        );
        expect(r.issues.filter((i) => i.kind === 'orthographe')).toHaveLength(0);
    });

    it("n'abîme pas une URL ni une heure", async () => {
        const entree = 'Le lien est https://thelandlord.tn/checkin et l’arrivée est à 14:30.';
        const r = await proofread(entree, { dictionary, extraWords });
        console.log('URL →', JSON.stringify(r.corrected));
        expect(r.corrected).toContain('https://thelandlord.tn/checkin');
        expect(r.corrected).toContain('14:30');
    });

    it('est idempotent sur le vrai dictionnaire', async () => {
        const r1 = await proofread('bonjour,merci de confirmer votre heure d arrivee svp', { dictionary, extraWords });
        const r2 = await proofread(r1.corrected, { dictionary, extraWords });
        expect(r2.corrected).toBe(r1.corrected);
        expect(r2.hasChanges).toBe(false);
    });
});

describe('élision oubliée — le cas signalé par le gérant', () => {
    // Il a tapé « test dorthographe jenvois ». Les deux mots étaient bien signalés, mais les
    // suggestions du vrai dictionnaire étaient « orthographe » et « envois » : la lettre élidée
    // SUPPRIMÉE. Accepter les deux donnait « Test orthographe envois », du français faux.
    it('propose de remettre l’apostrophe, pas de supprimer la lettre élidée', async () => {
        const r = await proofread('test dorthographe jenvois', { dictionary, extraWords });
        const orthographe = r.issues.filter((i) => i.kind === 'orthographe');
        console.log(
            'ÉLIS→',
            orthographe.map((i) => `${i.from}→[${i.suggestions.join(',')}]`),
        );

        expect(orthographe.map((i) => i.from)).toEqual(['dorthographe', 'jenvois']);
        expect(orthographe[0].suggestions[0]).toBe("d'orthographe");
        expect(orthographe[1].suggestions[0]).toBe("j'envois");
        expect(orthographe.every((i) => i.applied === false)).toBe(true); // suggéré, jamais appliqué
    });

    it('reconstruit les élisions que les gérants tapent collées', () => {
        const attendu: Record<string, string> = {
            jai: "j'ai",
            jaime: "j'aime",
            cest: "c'est",
            dun: "d'un",
            quil: "qu'il",
            quon: "qu'on",
            nest: "n'est",
            ladresse: "l'adresse",
            lappartement: "l'appartement",
            dautres: "d'autres",
            daccord: "d'accord",
            lorsquil: "lorsqu'il",
            puisquil: "puisqu'il",
            lheure: "l'heure",
            laccueil: "l'accueil",
            dabord: "d'abord",
            // Le reste n'est PAS connu tel quel : c'est la suggestion de nspell qui sert, et
            // seulement parce qu'elle a exactement les mêmes lettres, aux accents près.
            lhotel: "l'hôtel",
            jespere: "j'espère",
            nhesitez: "n'hésitez",
            lequipe: "l'équipe",
            larrivee: "l'arrivée",
            darrivee: "d'arrivée",
        };

        for (const [colle, elide] of Object.entries(attendu)) {
            expect(dictionary.check(colle)).toBe(false); // le mot est bien signalé
            expect(dictionary.suggest(colle)[0]).toBe(elide); // et la reconstruction vient en tête
        }
    });

    it('n’invente aucune apostrophe sur un mot capitalisé, même en tête de phrase', () => {
        // Les quatre prénoms fautifs du corpus OUVRAIENT leur phrase (« Sabri vous attend
        // devant le portail. ») : ne garder que le milieu de phrase déplaçait le défaut au
        // lieu de le réparer. Les élisions capitalisées fréquentes sont recollées en amont
        // par la couche typographie — voir le test « les élisions fréquentes sont recollées ».
        for (const nom of ['Sabri', 'Saber', 'Jaber', 'Dorra']) {
            expect(dictionary.suggest(nom, 3, { sentenceStart: true }).filter((s) => s.includes("'"))).toEqual([]);
            expect(dictionary.suggest(nom, 3, { sentenceStart: false }).filter((s) => s.includes("'"))).toEqual([]);
        }
    });

    it('ne réintroduit aucune des reconstructions fausses mesurées', () => {
        // Toutes sortaient d'un passage aveugle par suggest(reste). Le discriminant « mêmes
        // lettres, accents en plus » les élimine, sans sacrifier les cas justes ci-dessus.
        expect(dictionary.suggest('larrivee')).not.toContain("l'arrive");
        expect(dictionary.suggest('darrivee')).not.toContain("d'arrive");
        expect(dictionary.suggest('cava')).toEqual([]);
        expect(dictionary.suggest('lete')).not.toContain("l'eue");
        expect(dictionary.suggest('jusqua')).not.toContain("j'musqua");
        expect(dictionary.suggest('jusquà')).not.toContain("j'asque");
    });

    it('recolle « jusqu’ » d’office, et le résultat ne se fait pas resignaler', async () => {
        const r = await proofread('Nous restons jusquau 12 mai, jusquaux vacances, jusqua demain', { dictionary, extraWords });
        console.log('JUSQU→', JSON.stringify(r.corrected));

        expect(r.corrected).toContain("jusqu'au 12 mai");
        expect(r.corrected).toContain("jusqu'aux vacances");
        expect(r.corrected).toContain("jusqu'à demain");
        expect(r.issues.filter((i) => i.kind === 'orthographe')).toHaveLength(0);
    });
});

describe('mots suspects sur le vrai dictionnaire', () => {
    it('signale « cava », que le dictionnaire accepte pourtant (verbe caver)', async () => {
        const r = await proofread('Bonjour Sarah, cava bien pour votre arrivée de demain ?', { dictionary, extraWords });
        const suspects = r.issues.filter((i) => i.rule === 'mot-suspect');
        console.log(
            'SUSP→',
            suspects.map((i) => `${i.from}→[${i.suggestions.join(',')}]`),
        );

        expect(dictionary.check('cava')).toBe(true); // c'est bien un contournement du dictionnaire
        expect(suspects.map((i) => i.from)).toEqual(['cava']);
        expect(suspects[0].suggestions).toEqual(['ça va']);
        expect(suspects[0].applied).toBe(false);
        expect(r.corrected).toContain('cava'); // rien n'a été appliqué d'office
    });

    it('ne signale pas « dune », qui est un vrai mot pour une agence du bord de mer', async () => {
        const r = await proofread('Bonjour, quelle belle vue sur la dune depuis votre chambre', { dictionary, extraWords });

        expect(r.issues.filter((i) => i.kind === 'orthographe')).toHaveLength(0);
    });
});

describe('élision inventée — les quatre défauts mesurés sur le corpus', () => {
    /** Suggestions d'orthographe d'un message, dans l'ordre où le gérant les voit. */
    async function signalements(message: string): Promise<Record<string, string[]>> {
        const r = await proofread(message, { dictionary, extraWords });

        return Object.fromEntries(r.issues.filter((i) => i.kind === 'orthographe').map((i) => [i.from, i.suggestions]));
    }

    it('« depart » donne « départ », plus jamais « d’épart »', async () => {
        // « épart » est une pièce de charrette, donc un vrai mot : l'élision sortait EN TÊTE,
        // devant « départ ». Quatre occurrences sur les 55 messages du corpus.
        expect(dictionary.suggest('depart')[0]).toBe('départ');
        expect(dictionary.suggest('depart')).not.toContain("d'épart");

        const vus = await signalements('Jai bien reçu votre message concernant le jour du depart, merci beaucoup pour votre patience.');
        expect(vus.depart[0]).toBe('départ');
        expect(vus.depart.join(' ')).not.toContain("'");
    });

    it('ne coupe plus les prénoms tunisiens en deux', async () => {
        // Mesuré : « Sabri » → « S'abri », « Saber » → « S'aber », « Jaber » → « J'aber »,
        // « Dorra » → « D'orra » — 4 des 71 prénoms du corpus inconnus du dictionnaire.
        const vus = await signalements('Bonjour, le chauffeur Sabri vous attend à l’aéroport, et Dorra passera demain avec Jaber et Saber.');

        expect(Object.keys(vus)).toEqual(['Sabri', 'Dorra', 'Jaber', 'Saber']);
        expect(
            Object.values(vus)
                .flat()
                .filter((suggestion) => suggestion.includes("'")),
        ).toEqual([]);
    });

    it('les élisions fréquentes capitalisées sont recollées d’office, sans rien à cliquer', async () => {
        // Contrepartie assumée du garde-fou ci-dessus : « Jai » n'est plus une SUGGESTION, il
        // est corrigé directement par la couche typographie. C'est mieux pour le gérant (aucun
        // clic) et c'est sans risque : aucune de ces formes n'est un mot du dictionnaire.
        const resultat = await proofread('Jai bien reçu votre message. Cest note, nhesitez pas.', { dictionary });

        expect(resultat.corrected).toBe("J'ai bien reçu votre message. C'est note, n'hésitez pas.");
        expect(resultat.issues.filter((probleme) => probleme.kind === 'orthographe')).toEqual([]);
    });

    it('n’élide plus les restes mots-outils : ni « N’ou », ni « t’ou », ni « j’ou », ni « l’ou »', () => {
        for (const mot of ['Nou', 'tou', 'jou', 'lou']) {
            expect(dictionary.suggest(mot).filter((suggestion) => suggestion.includes("'"))).toEqual([]);
        }
    });

    it('laisse intactes les élisions utiles, y compris celles dont le reste fait deux lettres', () => {
        const attendu: Record<string, string> = {
            jai: "j'ai",
            dorthographe: "d'orthographe",
            jenvois: "j'envois",
            larrivee: "l'arrivée",
            cest: "c'est",
            quil: "qu'il",
            quon: "qu'on",
            dun: "d'un",
            lorsquil: "lorsqu'il",
            lheure: "l'heure",
            ladresse: "l'adresse",
            lappartement: "l'appartement",
        };

        for (const [colle, elide] of Object.entries(attendu)) {
            expect(dictionary.suggest(colle)[0]).toBe(elide);
        }
    });

    it('propose l’accent avant l’amputation, hors élision', () => {
        // nspell propose toujours d'abord la suggestion qui SUPPRIME une lettre.
        const attendu: Record<string, string> = {
            louee: 'louée',
            chauffee: 'chauffée',
            fermee: 'fermée',
            equipee: 'équipée',
            confirmee: 'confirmée',
            annulee: 'annulée',
        };

        for (const [tape, accentue] of Object.entries(attendu)) {
            expect(dictionary.suggest(tape)[0]).toBe(accentue);
        }
    });

    it('vaut aussi pour un mot qui porte déjà son apostrophe', () => {
        // « lentree » donnait bien « l'entrée », mais « l'entree » donnait « l'entre ».
        expect(dictionary.suggest("l'entree")[0]).toBe("l'entrée");
        expect(dictionary.suggest('lentree')[0]).toBe("l'entrée");
    });

    it('ne signale pas un message arabe court', async () => {
        // Moins de 6 mots : le message passe sous le plancher de la sonde et était relu comme
        // du français — trois mots soulignés, zéro suggestion, sur un arabe parfaitement écrit.
        const arabe = 'شكرا لك على الرسالة';
        const r = await proofread(arabe, { dictionary, extraWords });

        expect(r.issues.filter((i) => i.kind === 'orthographe')).toHaveLength(0);
        expect(r.corrected).toBe(arabe);
    });
});

describe('sonde de langue — les élisions oubliées ne coupent plus le correcteur', () => {
    // Chaque élision manquante comptait comme un mot inconnu : plus le gérant en oubliait,
    // plus le ratio grimpait, et toute la couche française s'éteignait. Mesuré avant le
    // correctif : ces six messages ressortaient tous en isFrench=false, donc SANS un seul
    // signalement — « jusquau » n'était même plus recollé.
    const MESSAGES = [
        'Bonjour, jai bien recu votre message. Jenvois ladresse de lappartement et lheure darrivee. Merci.',
        'Jai note lheure darrivee. Jenvois ladresse de lappartement et le code dentree, cest simple.',
        'Cest note. Lheure darrivee est libre, jenvois ladresse et le code dentree de lappartement.',
        'Jai bien recu lacompte. Jenvois lheure darrivee, ladresse de lappartement et le nom du gardien.',
        'Bonjour, jespere que lappartement vous plait. Jenvois lheure de depart et ladresse du parking, cest note.',
        'Jai prevenu lequipe de lheure darrivee. Ladresse de lappartement et le code dentree suivent, cest confirme.',
    ];

    it('les six messages redeviennent du français, et sont relus', async () => {
        for (const message of MESSAGES) {
            const r = await proofread(message, { dictionary, extraWords });

            expect(r.isFrench).toBe(true);
            expect(r.issues.filter((i) => i.kind === 'orthographe').length).toBeGreaterThan(0);
        }
    });

    it('un vrai message anglais reste hors du français', async () => {
        for (const message of [
            'Hello, could you please confirm your arrival time tomorrow morning at the apartment? Thanks a lot for your help.',
            'Hello, your reservation at the apartment is confirmed for tomorrow morning, the keys will be at the reception desk.',
            'Good evening, the cleaning team will come tomorrow before your departure, please leave the keys on the kitchen table.',
        ]) {
            const r = await proofread(message, { dictionary, extraWords });

            expect(r.isFrench).toBe(false);
            expect(r.issues.filter((i) => i.kind === 'orthographe')).toHaveLength(0);
        }
    });

    it('relit un message d’élisions LONG, bien au-dessus du plancher de langue', async () => {
        // Le test qui manquait : celui qui certifiait la fonctionnalité tenait en 3 mots,
        // donc sous MIN_WORDS_FOR_LANGUAGE_GUESS. Allongé, il ne signalait plus rien.
        const entree = 'Bonjour, jai bien recu votre message. Jenvois ladresse de lappartement et lheure darrivee. Merci.';
        const r = await proofread(entree, { dictionary, extraWords });
        const mots = collectCheckableWords(r.corrected, createWhitelist(extraWords));

        expect(mots.length).toBeGreaterThan(MIN_WORDS_FOR_LANGUAGE_GUESS);
        expect(r.isFrench).toBe(true);

        const orthographe = r.issues.filter((i) => i.kind === 'orthographe');

        // « jai » n'est plus signalé : la couche typographie l'a déjà recollé en « j'ai ».
        expect(r.corrected).toContain("j'ai");
        expect(r.corrected).toContain('reçu');

        expect(orthographe.map((i) => i.from)).toEqual(['Jenvois', 'ladresse', 'lappartement', 'lheure', 'darrivee']);
        // Coût assumé du garde-fou sur les capitales : « Jenvois », capitalisé, ne reçoit PAS
        // d'apostrophe reconstruite — c'est le prix à payer pour ne jamais écrire « S'abri »
        // sur le prénom d'un chauffeur. En minuscule, « jenvois » donne bien « j'envois ».
        expect(orthographe[0].suggestions.filter((suggestion) => suggestion.includes("'"))).toEqual([]);
        expect(orthographe.slice(1).map((i) => i.suggestions[0])).toEqual(["l'adresse", "l'appartement", "l'heure", "d'arrivée"]);
        expect(orthographe.every((i) => i.applied === false)).toBe(true);
        expect(dictionary.suggest('jenvois')[0]).toBe("j'envois");
    });

    it('recolle « jusquau » même dans un message truffé d’élisions', async () => {
        const r = await proofread('Jai reserve lappartement jusquau 12 mai, ladresse et lheure darrivee suivent.', { dictionary, extraWords });

        expect(r.isFrench).toBe(true);
        expect(r.corrected).toContain("jusqu'au 12 mai");
    });

    it('tient bon quand extraWords rétrécit le dénominateur', async () => {
        // L'usage documenté du correcteur — nommer le voyageur et le logement — retirait des
        // mots CONNUS du décompte et ne laissait que les inconnus : 5 sur 11, soit 0,45 pour un
        // seuil à 0,40. Plus l'appelant renseignait le contexte, moins il était aidé.
        const entree = 'Bonjour Sarah Ben Salah, jai bien recu votre message pour la Villa des Artistes. Jenvois ladresse et lheure darrivee.';
        const r = await proofread(entree, { dictionary, extraWords });

        expect(r.isFrench).toBe(true);
        // « jai » est recollé d'office par la typographie ; « Jenvois », capitalisé, n'a droit
        // à aucune apostrophe reconstruite (garde-fou sur les noms propres). Restent les trois
        // élisions en minuscules, qui sont bien proposées.
        expect(r.corrected).toContain("j'ai");
        expect(r.issues.filter((i) => i.kind === 'orthographe').map((i) => i.from)).toEqual(['Jenvois', 'ladresse', 'lheure', 'darrivee']);
        expect(
            r.issues
                .filter((i) => i.kind === 'orthographe')
                .slice(1)
                .map((i) => i.suggestions[0]),
        ).toEqual(["l'adresse", "l'heure", "d'arrivée"]);
    });
});
