import { afterEach, describe, expect, it, vi } from 'vitest';
import { createFrenchDictionary, resetFrenchDictionary, type FrenchDictionary } from './dictionary';
import {
    collectCheckableWords,
    collectSpellableWords,
    looksFrench,
    MIN_WORDS_FOR_LANGUAGE_GUESS,
    proofread,
    UNKNOWN_WORD_RATIO_LIMIT,
} from './index';
import { createWhitelist } from './whitelist';

/**
 * Faux dictionnaire : le vrai (1,4 Mo) n'est jamais chargé en test.
 * Tout mot absent de `connus` est considéré comme inconnu.
 *
 * `reparables` liste les mots que les couches françaises sauraient réparer (élision
 * reconstruite, accent restauré) : la sonde de langue les compte comme du français mal tapé,
 * pas comme des mots étrangers.
 */
function fauxDictionnaire(connus: string[], suggestions: Record<string, string[]> = {}, reparables: string[] = []): FrenchDictionary {
    const table = new Set(connus.map((mot) => mot.toLocaleLowerCase('fr')));
    const reparable = new Set(reparables.map((mot) => mot.toLocaleLowerCase('fr')));

    return {
        check: (mot) => table.has(mot.toLocaleLowerCase('fr')),
        suggest: (mot, max = 3) => (suggestions[mot.toLocaleLowerCase('fr')] ?? []).slice(0, max),
        isRepairableFrench: (mot) => reparable.has(mot.toLocaleLowerCase('fr')),
    };
}

/** Vocabulaire minimal du faux dictionnaire : uniquement les mots employés dans ce fichier. */
const MOTS_FRANCAIS = [
    'accueil',
    'adresse',
    'arrivée',
    'avec',
    'beaucoup',
    'bien',
    'bonjour',
    'chambre',
    'clé',
    'commence',
    'déjà',
    'demain',
    'est',
    'être',
    'matin',
    'merci',
    'nous',
    'porte',
    'pour',
    'prête',
    'ravis',
    'référence',
    'séjour',
    'sommes',
    'tout',
    'très',
    'via',
    'voir',
    'votre',
];

describe('proofread — couche typographie', () => {
    it('applique la typographie dans « corrected » et la signale comme appliquée', async () => {
        const resultat = await proofread('bonjour,comment ça va', { dictionary: null });

        expect(resultat.corrected).toBe('Bonjour, comment ça va');
        expect(resultat.hasChanges).toBe(true);
        expect(resultat.issues.every((probleme) => probleme.kind === 'typographie' && probleme.applied)).toBe(true);
    });

    it('ne signale rien sur un message déjà propre', async () => {
        const resultat = await proofread('Bonjour, comment ça va ?', { dictionary: null });

        expect(resultat.hasChanges).toBe(false);
        expect(resultat.issues).toHaveLength(0);
    });

    it('est idempotente : repasser le texte corrigé ne change plus rien', async () => {
        const brouillon = 'bonjour,sil vous plait confirmez le le rendez vous   avant 18h00.merci tres  , beaucoup';
        const premier = await proofread(brouillon, { dictionary: null });
        const second = await proofread(premier.corrected, { dictionary: null });

        expect(second.corrected).toBe(premier.corrected);
        expect(second.hasChanges).toBe(false);
    });

    it('reste idempotente quand le dictionnaire est branché', async () => {
        const dictionary = fauxDictionnaire(MOTS_FRANCAIS);
        const premier = await proofread('bonjour,votre sejour commence demain', { dictionary });
        const second = await proofread(premier.corrected, { dictionary });

        expect(second.corrected).toBe(premier.corrected);
        expect(second.hasChanges).toBe(false);
    });
});

describe('proofread — couche dictionnaire', () => {
    it('suggère sans jamais appliquer', async () => {
        const dictionary = fauxDictionnaire(MOTS_FRANCAIS, { chmabre: ['chambre', 'chambres'] });
        const resultat = await proofread('Bonjour, votre chmabre est prête pour demain matin avec la clé', { dictionary });
        const orthographe = resultat.issues.filter((probleme) => probleme.kind === 'orthographe');

        expect(orthographe.map((probleme) => probleme.from)).toContain('chmabre');
        expect(orthographe.every((probleme) => probleme.applied)).toBe(false);
        expect(resultat.corrected).toContain('chmabre');
    });

    it('remonte les suggestions et la première d’entre elles dans « to »', async () => {
        const dictionary = fauxDictionnaire(MOTS_FRANCAIS, { chmabre: ['chambre', 'chambres', 'chambré', 'cambre'] });
        const resultat = await proofread('Bonjour, votre chmabre est prête pour demain matin avec la clé', {
            dictionary,
            maxSuggestions: 2,
        });
        const probleme = resultat.issues.find((issue) => issue.from === 'chmabre');

        expect(probleme?.suggestions).toEqual(['chambre', 'chambres']);
        expect(probleme?.to).toBe('chambre');
        expect(probleme?.rule).toBe('mot-inconnu');
    });

    it('ne signale ni les canaux, ni les villes, ni les mots dynamiques de l’appelant', async () => {
        const dictionary = fauxDictionnaire(MOTS_FRANCAIS);
        const resultat = await proofread('Bonjour, votre séjour à Hammamet via Airbnb commence demain à la Villa Ennour', {
            dictionary,
            extraWords: ['Villa Ennour'],
        });

        expect(resultat.issues.filter((probleme) => probleme.kind === 'orthographe')).toHaveLength(0);
    });

    it('ne signale ni les URLs, ni les e-mails, ni les références chiffrées', async () => {
        const dictionary = fauxDictionnaire(MOTS_FRANCAIS);
        const resultat = await proofread(
            'Bonjour, merci beaucoup pour tout : https://thelandlord.tn, contact@thelandlord.tn et la référence RES4521',
            { dictionary },
        );

        expect(resultat.issues.filter((probleme) => probleme.kind === 'orthographe')).toHaveLength(0);
    });

    it('positionne les signalements d’orthographe sur le texte corrigé', async () => {
        const dictionary = fauxDictionnaire(MOTS_FRANCAIS, { chmabre: ['chambre'] });
        const resultat = await proofread('bonjour,votre chmabre est prête pour demain matin', { dictionary });
        const probleme = resultat.issues.find((issue) => issue.from === 'chmabre');

        expect(resultat.corrected.slice(probleme?.start, probleme?.end)).toBe('chmabre');
    });
});

describe('proofread — mots suspects', () => {
    // « cava » est accepté par le vrai dictionnaire (verbe caver, « il cava ») : sans
    // contournement, il traversait la relecture sans un mot, alors que le gérant voulait
    // écrire « ça va ». Le faux dictionnaire reproduit cette acceptation.
    it('signale un mot que le dictionnaire accepte, sans jamais l’appliquer', async () => {
        const dictionary = fauxDictionnaire([...MOTS_FRANCAIS, 'cava']);
        const resultat = await proofread('Bonjour, cava bien pour demain matin, merci beaucoup', { dictionary });
        const suspects = resultat.issues.filter((probleme) => probleme.rule === 'mot-suspect');

        expect(suspects.map((probleme) => probleme.from)).toEqual(['cava']);
        expect(suspects[0].suggestions).toEqual(['ça va']);
        expect(suspects[0].to).toBe('ça va');
        expect(suspects[0].kind).toBe('orthographe');
        expect(suspects[0].applied).toBe(false);
        expect(resultat.corrected).toContain('cava');
    });

    it('pointe le mot dans le texte corrigé', async () => {
        const dictionary = fauxDictionnaire([...MOTS_FRANCAIS, 'cava']);
        const resultat = await proofread('bonjour,cava bien pour demain matin, merci beaucoup', { dictionary });
        const probleme = resultat.issues.find((issue) => issue.rule === 'mot-suspect');

        expect(resultat.corrected.slice(probleme?.start, probleme?.end)).toBe('cava');
    });

    it('laisse la liste blanche gagner', async () => {
        const dictionary = fauxDictionnaire([...MOTS_FRANCAIS, 'cava']);
        const resultat = await proofread('Bonjour, cava bien pour demain matin, merci beaucoup', {
            dictionary,
            extraWords: ['Cava'], // un logement pourrait s'appeler ainsi
        });

        expect(resultat.issues.filter((probleme) => probleme.kind === 'orthographe')).toHaveLength(0);
    });

    it('ne signale pas les mots ambigus laissés hors de la table', async () => {
        const dictionary = fauxDictionnaire([...MOTS_FRANCAIS, 'quelle', 'belle', 'vue', 'sur', 'dune']);
        const resultat = await proofread('Bonjour, quelle belle vue sur la dune pour votre séjour demain', { dictionary });

        expect(resultat.issues.filter((probleme) => probleme.kind === 'orthographe')).toHaveLength(0);
    });

    it('n’est pas rejoué sur un message qui n’est pas en français', async () => {
        const dictionary = fauxDictionnaire(MOTS_FRANCAIS);
        const resultat = await proofread('Hello, cava is a nice italian white wine for tomorrow morning', { dictionary });

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

describe('proofread — détection « pas du français »', () => {
    it('ne garde que la typographie sur un message anglais', async () => {
        const dictionary = fauxDictionnaire(MOTS_FRANCAIS);
        const resultat = await proofread('Hello, your reservation at the apartment is confirmed for tomorrow morning', { dictionary });

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

    it('ne massacre pas un message en arabe', async () => {
        const arabe = 'مرحبا، الحجز مؤكد لغد صباحا في الشقة';
        const resultat = await proofread(arabe, { dictionary: fauxDictionnaire(MOTS_FRANCAIS) });

        expect(resultat.isFrench).toBe(false);
        expect(resultat.corrected).toBe(arabe);
        expect(resultat.issues).toHaveLength(0);
    });

    it('garde la couche orthographe sur un message français avec une faute', async () => {
        const dictionary = fauxDictionnaire(MOTS_FRANCAIS, { chmabre: ['chambre'] });
        const resultat = await proofread('Bonjour, votre chmabre est prête pour demain matin, merci beaucoup pour tout', { dictionary });

        expect(resultat.isFrench).toBe(true);
        expect(resultat.issues.filter((probleme) => probleme.kind === 'orthographe')).toHaveLength(1);
    });

    it('n’essaie pas de trancher sur un message trop court', () => {
        expect(MIN_WORDS_FOR_LANGUAGE_GUESS).toBe(6);
        expect(looksFrench(3, 3)).toBe(true);
        expect(looksFrench(5, 5)).toBe(true);
    });

    it('bascule au-delà de 40 % de mots inconnus, pas en dessous', () => {
        expect(UNKNOWN_WORD_RATIO_LIMIT).toBe(0.4);
        expect(looksFrench(10, 4)).toBe(true);
        expect(looksFrench(10, 5)).toBe(false);
    });
});

describe('proofread — la sonde de langue ne s’éteint plus sur sa propre cible', () => {
    // Les élisions oubliées comptaient comme des mots inconnus : plus le gérant en oubliait,
    // plus le ratio grimpait, plus `looksFrench` basculait — et TOUTE la couche française
    // était coupée, sur le message même qu'elle devait réparer.
    // Volontairement des élisions que la couche typographie NE recolle PAS d'office
    // (`ladresse`, `lheure`, `darrivee`) : les formes ultra-fréquentes comme « jai » sont
    // corrigées en amont et n'atteindraient jamais la sonde, ce qui ne prouverait rien.
    const ELISIONS = 'Bonjour, merci pour ladresse et lheure de darrivee';

    it('comptait le message pour de l’étranger quand rien ne réparait les élisions', async () => {
        const dictionary = fauxDictionnaire(MOTS_FRANCAIS);

        // 6 mots testables, 3 inconnus : 0,50 pour un seuil à 0,40.
        expect((await proofread(ELISIONS, { dictionary })).isFrench).toBe(false);
    });

    it('le compte pour du français dès que les couches françaises savent le réparer', async () => {
        const dictionary = fauxDictionnaire(MOTS_FRANCAIS, { ladresse: ["l'adresse"] }, ['ladresse', 'lheure', 'darrivee']);
        const resultat = await proofread(ELISIONS, { dictionary });

        expect(resultat.isFrench).toBe(true);
        expect(resultat.issues.filter((probleme) => probleme.kind === 'orthographe').map((probleme) => probleme.from)).toEqual([
            'ladresse',
            'lheure',
            'darrivee',
        ]);
    });

    it('n’allège pas pour autant ce qui est signalé : la sonde ne tranche que la langue', async () => {
        const dictionary = fauxDictionnaire(MOTS_FRANCAIS, { ladresse: ["l'adresse"] }, ['ladresse', 'lheure', 'darrivee']);
        const resultat = await proofread(ELISIONS, { dictionary });
        const ladresse = resultat.issues.find((probleme) => probleme.from === 'ladresse');

        expect(ladresse?.suggestions).toEqual(["l'adresse"]);
        expect(ladresse?.applied).toBe(false);
    });

    it('laisse un vrai message anglais hors du français', async () => {
        const dictionary = fauxDictionnaire(MOTS_FRANCAIS, {}, ['ladresse', 'lheure', 'darrivee']);
        const resultat = await proofread('Hello, could you please confirm your arrival time tomorrow morning at the apartment', { dictionary });

        expect(resultat.isFrench).toBe(false);
    });
});

describe('proofread — le contexte de phrase vient du texte', () => {
    it('dit à la couche dictionnaire si le mot ouvre une phrase', async () => {
        const vus: Record<string, boolean | undefined> = {};
        const dictionary: FrenchDictionary = {
            check: (mot) => MOTS_FRANCAIS.includes(mot.toLocaleLowerCase('fr')),
            suggest: (mot, _max, context) => {
                vus[mot] = context?.sentenceStart;

                return [];
            },
            isRepairableFrench: () => false,
        };

        await proofread('Bonjour, la chmabre est prête. Chmabre demain matin avec la clé', { dictionary });

        // Même mot, deux positions : c'est ce qui sépare « Jai bien reçu » de « le chauffeur Sabri ».
        expect(vus).toEqual({ chmabre: false, Chmabre: true });
    });
});

describe('proofread — coût de la sonde de langue', () => {
    it('n’appelle aucune suggestion pour écarter un message anglais', async () => {
        // Mesuré : `suggest()` coûte 558 ms sur « tomorrow » et plusieurs secondes sur un mot
        // arabe. Une sonde qui l'appellerait sur chaque mot inconnu ferait passer la relecture
        // d'un message anglais de 2 ms à 1,6 s, et celle d'un message arabe à 6,9 s.
        let appels = 0;
        const dictionary = createFrenchDictionary({
            correct: (mot) => MOTS_FRANCAIS.includes(mot.toLocaleLowerCase('fr')),
            suggest: () => {
                appels += 1;

                return [];
            },
        });

        const resultat = await proofread('Hello, could you please confirm your arrival time tomorrow morning at the apartment', { dictionary });

        expect(resultat.isFrench).toBe(false);
        expect(appels).toBe(0);
    });
});

describe('proofread — dégradation douce', () => {
    afterEach(() => {
        resetFrenchDictionary();
        vi.unstubAllGlobals();
    });

    it('se réduit à la typographie quand le dictionnaire ne charge pas, sans jamais lever', async () => {
        vi.stubGlobal(
            'fetch',
            vi.fn(() => Promise.reject(new Error('réseau coupé'))),
        );

        const resultat = await proofread('bonjour,comment ça va');

        expect(resultat.dictionaryAvailable).toBe(false);
        expect(resultat.corrected).toBe('Bonjour, comment ça va');
        expect(resultat.issues.every((probleme) => probleme.kind === 'typographie')).toBe(true);
    });

    it('permet de couper explicitement la couche dictionnaire', async () => {
        const resultat = await proofread('Bonjour, votre chmabre est prête', { dictionary: null });

        expect(resultat.dictionaryAvailable).toBe(false);
        expect(resultat.issues.filter((probleme) => probleme.kind === 'orthographe')).toHaveLength(0);
    });
});

describe('collectCheckableWords', () => {
    const liste = createWhitelist();

    it('ne retient que les mots dignes d’être vérifiés', () => {
        const mots = collectCheckableWords('Le lit 12 est OK, voir https://thelandlord.tn ou contact@thelandlord.tn', liste);

        expect(mots.map((jeton) => jeton.text)).toEqual(['lit', 'est', 'voir']);
    });

    it('garde les élisions entières — c’est le dictionnaire qui les découpe', () => {
        const mots = collectCheckableWords("Voici l'hôtel et aujourd'hui", liste);

        expect(mots.map((jeton) => jeton.text)).toEqual(['Voici', "l'hôtel", "aujourd'hui"]);
    });

    it('garde les mots d’une autre écriture : la sonde de langue en a besoin pour classer', () => {
        const mots = collectCheckableWords('مرحبا، الحجز مؤكد لغد صباحا في الشقة', liste);

        expect(mots.length).toBeGreaterThan(0);
    });
});

describe('collectSpellableWords', () => {
    const liste = createWhitelist();

    it('écarte les mots sans aucune lettre latine — un dictionnaire français n’en dira rien', () => {
        expect(collectSpellableWords('شكرا لك على الرسالة', liste)).toEqual([]);
    });

    it('garde les mots latins d’un message mêlé', () => {
        const mots = collectSpellableWords('Merci شكرا beaucoup', liste);

        expect(mots.map((jeton) => jeton.text)).toEqual(['Merci', 'beaucoup']);
    });
});
