import type { ActivityEntry, ActivityEvent, ActivityFilters, ActivitySubject } from '@/types/activity';
import { describe, expect, it } from 'vitest';
import {
    activeFilterCount,
    CLEARED_FILTERS,
    dateToIsoDay,
    detectPreset,
    effectiveRange,
    eventTone,
    exportParams,
    extraFieldCount,
    FEED_ONLY_CLEARED,
    hasNoSubjectBlock,
    isActivityEvent,
    fill,
    formatCount,
    formatShortDay,
    groupByDay,
    isDefaultPeriod,
    isoDayToDate,
    PERIOD_PRESETS,
    presetRange,
    queryFromFilters,
    queryToParams,
    relatedCount,
    rowDetail,
    sortNewestFirst,
    sourceKind,
    splitChanges,
    subjectLabel,
} from './activity-format';

/** Une ligne de flux minimale ; les dates sont locales pour ne pas dépendre du fuseau de la CI. */
function entry(id: number, createdAt: Date, overrides: Partial<ActivityEntry> = {}): ActivityEntry {
    const subject: ActivitySubject = { type: 'Reservation', type_label: 'Réservation', type_phrase: 'la réservation', id: 10, label: null, url: null, deleted: false, parent: null };

    return {
        id,
        event: 'updated',
        module: 'reservations',
        actor: { id: 1, name: 'Amine', role: 'Admin', initials: 'AM', is_system: false, team: null },
        subject,
        summary_fields: [],
        summary_count: 0,
        preview: null,
        created_at: createdAt.toISOString(),
        source: 'web',
        context: { url: null, ip: null, device: null },
        records: [{ id, event: 'updated', module: 'reservations', subject, changes: [] }],
        ...overrides,
    };
}

const filters: ActivityFilters = {
    tab: 'feed',
    user: null,
    module: null,
    event: null,
    from: '2026-09-01',
    to: '2026-09-23',
    search: null,
    system: false,
    franchise: null,
    subject: null,
    sensitive: false,
};

describe('eventTone — la couleur porte un sens, et seulement trois', () => {
    const cas: Array<[ActivityEvent, string]> = [
        ['created', 'created'],
        ['deleted', 'deleted'],
        ['permissions_updated', 'sensitive'],
        ['login_failed', 'sensitive'],
        ['updated', 'neutral'],
        ['restored', 'neutral'],
        ['login', 'neutral'],
        ['logout', 'neutral'],
        // L'export se trace, mais n'est pas un signal : neutre.
        ['exported', 'neutral'],
    ];

    it.each(cas)('%s → %s', (event, tone) => {
        expect(eventTone(event)).toBe(tone);
    });
});

describe('sourceKind — jamais le nom d’un prestataire', () => {
    it('reconnaît les quatre origines connues', () => {
        expect(sourceKind('web')).toBe('web');
        expect(sourceKind('webhook')).toBe('webhook');
        expect(sourceKind('sync')).toBe('sync');
        expect(sourceKind('console')).toBe('console');
    });

    it('réunit toutes les variantes admin-* sous « outil d’administration »', () => {
        expect(sourceKind('admin-report')).toBe('admin');
        expect(sourceKind('admin-restore')).toBe('admin');
        expect(sourceKind('admin-force')).toBe('admin');
    });

    it('classe une origine inconnue ou absente à part, sans planter', () => {
        expect(sourceKind('nouvelle-origine')).toBe('other');
        expect(sourceKind(null)).toBe('other');
    });
});

describe('groupByDay', () => {
    it('regroupe par jour local en gardant l’ordre du serveur', () => {
        const groups = groupByDay([
            entry(3, new Date(2026, 8, 23, 15, 0)),
            entry(2, new Date(2026, 8, 23, 9, 0)),
            entry(1, new Date(2026, 8, 22, 18, 0)),
        ]);

        expect(groups.map((g) => g.key)).toEqual(['2026-09-23', '2026-09-22']);
        expect(groups[0].entries.map((e) => e.id)).toEqual([3, 2]);
        expect(groups[1].entries.map((e) => e.id)).toEqual([1]);
    });

    it('ne réordonne jamais : un jour interrompu donne deux groupes', () => {
        const groups = groupByDay([
            entry(3, new Date(2026, 8, 23, 15, 0)),
            entry(2, new Date(2026, 8, 22, 9, 0)),
            entry(1, new Date(2026, 8, 23, 8, 0)),
        ]);

        expect(groups.map((g) => g.key)).toEqual(['2026-09-23', '2026-09-22', '2026-09-23']);
    });

    it('rend une liste vide pour un flux vide', () => {
        expect(groupByDay([])).toEqual([]);
    });
});

describe('compteurs de la ligne', () => {
    it('« +N » ne compte que les champs au-delà des pastilles', () => {
        expect(extraFieldCount(entry(1, new Date(), { summary_fields: ['Arrivée', 'Statut'], summary_count: 5 }))).toBe(3);
        expect(extraFieldCount(entry(1, new Date(), { summary_fields: ['Arrivée'], summary_count: 1 }))).toBe(0);
    });

    it('« actions liées » exclut la ligne principale', () => {
        const base = entry(1, new Date());
        expect(relatedCount(base)).toBe(0);
        expect(relatedCount({ ...base, records: [...base.records, ...base.records, ...base.records] })).toBe(2);
    });

    it('un élément sans libellé est désigné par son numéro', () => {
        expect(subjectLabel(entry(1, new Date()).subject)).toBe('#10');
    });
});

describe('splitChanges', () => {
    it('replie les champs techniques à part', () => {
        const { visible, technical } = splitChanges([
            { field: 'check_in', label: 'Arrivée', old: '01/10/2026', new: '02/10/2026', technical: false },
            { field: 'updated_at', label: 'Mis à jour', old: null, new: 'x', technical: true },
        ]);

        expect(visible.map((c) => c.field)).toEqual(['check_in']);
        expect(technical.map((c) => c.field)).toEqual(['updated_at']);
    });
});

describe('sortNewestFirst', () => {
    it('trie l’historique du plus récent au plus ancien sans muter l’entrée', () => {
        const liste = [entry(1, new Date(2026, 8, 20)), entry(3, new Date(2026, 8, 23)), entry(2, new Date(2026, 8, 21))];

        expect(sortNewestFirst(liste).map((e) => e.id)).toEqual([3, 2, 1]);
        expect(liste.map((e) => e.id)).toEqual([1, 3, 2]);
    });
});

describe('fill / formatCount', () => {
    it('remplace les jetons connus et laisse les autres', () => {
        expect(fill('Actions suivies depuis le :date', { date: '01/09/2026' })).toBe('Actions suivies depuis le 01/09/2026');
        expect(fill('+:count · :inconnu', { count: 3 })).toBe('+3 · :inconnu');
    });

    it('sépare les milliers selon la langue', () => {
        // Espace insécable (fine) côté français : NFKC la ramène à l'espace simple.
        expect(formatCount(12345, 'fr').normalize('NFKC')).toBe('12 345');
        expect(formatCount(12345, 'en')).toBe('12,345');
    });
});

describe('filtres ⇄ URL', () => {
    it('omet tout ce qui est vide, et l’onglet par défaut', () => {
        expect(queryToParams(queryFromFilters(filters))).toEqual({ from: '2026-09-01', to: '2026-09-23' });
    });

    it('écrit chaque filtre posé', () => {
        const params = queryToParams({
            ...queryFromFilters(filters),
            tab: 'team',
            user: '7',
            module: 'finances',
            event: 'deleted',
            search: '  villa  ',
            system: true,
            franchise: 'hq',
        });

        expect(params).toEqual({
            tab: 'team',
            user: '7',
            module: 'finances',
            event: 'deleted',
            from: '2026-09-01',
            to: '2026-09-23',
            search: 'villa',
            system: '1',
            franchise: 'hq',
        });
    });

    it('une recherche faite d’espaces ne part pas', () => {
        expect(queryToParams({ ...queryFromFilters(filters), search: '   ' })).not.toHaveProperty('search');
    });

    it('la réinitialisation rend la période au serveur (dates omises)', () => {
        const params = queryToParams({ ...queryFromFilters(filters), ...CLEARED_FILTERS });

        expect(params).toEqual({});
    });

});

/*
 * Préréglages de période. `now` est construit en LOCAL : les fonctions
 * raisonnent en jours locaux, le résultat ne dépend pas du fuseau de la CI.
 */
describe('presetRange', () => {
    const NOW = new Date(2026, 8, 23, 14, 0); // mercredi 23/09/2026 14:00

    it('aujourd’hui et hier sont des journées uniques', () => {
        expect(presetRange('today', NOW)).toEqual({ from: '2026-09-23', to: '2026-09-23' });
        expect(presetRange('yesterday', NOW)).toEqual({ from: '2026-09-22', to: '2026-09-22' });
    });

    it('« 7 derniers jours » = aujourd’hui et les six jours d’avant (la fenêtre du serveur)', () => {
        expect(presetRange('last7', NOW)).toEqual({ from: '2026-09-17', to: '2026-09-23' });
    });

    it('« 30 derniers jours » inclut aujourd’hui', () => {
        expect(presetRange('last30', NOW)).toEqual({ from: '2026-08-25', to: '2026-09-23' });
    });

    it('« Ce mois-ci » part du 1er', () => {
        expect(presetRange('this_month', NOW)).toEqual({ from: '2026-09-01', to: '2026-09-23' });
    });

    it('franchit les bornes de mois et d’année', () => {
        const premierMars = new Date(2026, 2, 1, 9, 0);
        expect(presetRange('yesterday', premierMars)).toEqual({ from: '2026-02-28', to: '2026-02-28' });
        expect(presetRange('last7', premierMars)).toEqual({ from: '2026-02-23', to: '2026-03-01' });

        const deuxJanvier = new Date(2027, 0, 2, 9, 0);
        expect(presetRange('last7', deuxJanvier)).toEqual({ from: '2026-12-27', to: '2027-01-02' });
        expect(presetRange('last30', deuxJanvier)).toEqual({ from: '2026-12-04', to: '2027-01-02' });
    });

    it('« Ce mois-ci » le 1er ne couvre que ce jour-là', () => {
        expect(presetRange('this_month', new Date(2026, 9, 1, 0, 5))).toEqual({ from: '2026-10-01', to: '2026-10-01' });
    });

    it('un instant juste avant minuit reste sur son jour local', () => {
        expect(presetRange('today', new Date(2026, 8, 23, 23, 59, 59))).toEqual({ from: '2026-09-23', to: '2026-09-23' });
    });
});

describe('detectPreset / effectiveRange / isDefaultPeriod', () => {
    const NOW = new Date(2026, 8, 23, 14, 0);

    it('reconnaît chaque préréglage', () => {
        for (const preset of PERIOD_PRESETS) {
            const { from, to } = presetRange(preset, NOW);
            expect(detectPreset(from, to, NOW)).toBe(preset);
        }
    });

    it('rend null pour des dates précises', () => {
        expect(detectPreset('2026-09-02', '2026-09-10', NOW)).toBeNull();
    });

    it('le 1er du mois, « Ce mois-ci » se lit « Aujourd’hui » — même jours, premier nom de la liste', () => {
        const premier = new Date(2026, 9, 1, 10, 0);
        const { from, to } = presetRange('this_month', premier);
        expect(detectPreset(from, to, premier)).toBe('today');
    });

    it('le 7 du mois, « Ce mois-ci » se lit « 7 derniers jours »', () => {
        const sept = new Date(2026, 9, 7, 10, 0);
        const { from, to } = presetRange('this_month', sept);
        expect(detectPreset(from, to, sept)).toBe('last7');
    });

    it('sans dates dans l’URL, la période affichée est la fenêtre par défaut du serveur', () => {
        expect(effectiveRange({ from: null, to: null }, NOW)).toEqual({ from: '2026-09-17', to: '2026-09-23' });
        expect(isDefaultPeriod({ from: null, to: null }, NOW)).toBe(true);
    });

    it('distingue la fenêtre par défaut d’une autre période', () => {
        expect(isDefaultPeriod({ from: '2026-09-17', to: '2026-09-23' }, NOW)).toBe(true);
        expect(isDefaultPeriod({ from: '2026-09-22', to: '2026-09-22' }, NOW)).toBe(false);
    });
});

describe('formatShortDay', () => {
    const NOW = new Date(2026, 8, 23);

    it('jj/mm dans l’année en cours, jj/mm/aaaa sinon', () => {
        expect(formatShortDay('2026-09-17', NOW)).toBe('17/09');
        expect(formatShortDay('2025-12-31', NOW)).toBe('31/12/2025');
    });
});

describe('activeFilterCount — le (n) de « Tout effacer »', () => {
    const NOW = new Date(2026, 8, 23, 14, 0);
    const base = queryFromFilters({ ...filters, from: '2026-09-17', to: '2026-09-23' });

    it('vaut 0 sur la vue par défaut', () => {
        expect(activeFilterCount(base, NOW)).toBe(0);
        expect(activeFilterCount({ ...base, from: null, to: null }, NOW)).toBe(0);
    });

    it('compte chaque critère qui restreint, la période comprise quand elle a changé', () => {
        expect(
            activeFilterCount(
                { ...base, user: '7', event: 'deleted', franchise: 'hq', search: 'villa', system: true, module: 'crm', from: '2026-09-01', to: '2026-09-10' },
                NOW,
            ),
        ).toBe(7);
    });

    it('une recherche faite d’espaces ne compte pas', () => {
        expect(activeFilterCount({ ...base, search: '   ' }, NOW)).toBe(0);
    });

    it('l’onglet ne compte pas', () => {
        expect(activeFilterCount({ ...base, tab: 'team' }, NOW)).toBe(0);
    });
});

describe('dates du sélecteur de période', () => {
    it('aller-retour aaaa-mm-jj ⇄ Date locale, sans recul d’un jour', () => {
        const date = isoDayToDate('2026-09-01');

        expect(date?.getDate()).toBe(1);
        expect(dateToIsoDay(date as Date)).toBe('2026-09-01');
    });

    it('refuse une valeur illisible', () => {
        expect(isoDayToDate('01/09/2026')).toBeUndefined();
        expect(isoDayToDate(null)).toBeUndefined();
    });
});

/* ─────────────────────────────────────────────────────────────────────────
 * TL-293 phases 1 à 3 : aperçu, historique d'un élément, sensibles, export.
 * ───────────────────────────────────────────────────────────────────────── */

describe('rowDetail — ce que la ligne montre sous sa phrase', () => {
    it('préfère l’aperçu du serveur aux noms de champs', () => {
        const withPreview = entry(1, new Date(), {
            preview: { kind: 'change', label: 'Statut', old: 'Confirmée', new: 'Annulée', more: 2 },
            summary_fields: ['Statut', 'Départ'],
            summary_count: 3,
        });
        expect(rowDetail(withPreview)).toBe('preview');
    });

    it('accepte un aperçu de valeurs (création, suppression)', () => {
        expect(rowDetail(entry(1, new Date(), { preview: { kind: 'values', text: 'A15 Malaga · 27/11 → 30/11/2026' } }))).toBe('preview');
    });

    it('retombe sur les noms de champs quand le serveur n’a pas d’aperçu', () => {
        expect(rowDetail(entry(1, new Date(), { summary_fields: ['Statut'], summary_count: 1 }))).toBe('fields');
        // Plus de champs que de pastilles : le « +N » seul justifie encore la rangée.
        expect(rowDetail(entry(1, new Date(), { summary_fields: [], summary_count: 2 }))).toBe('fields');
    });

    it('ne montre rien quand il n’y a rien de lisible', () => {
        expect(rowDetail(entry(1, new Date()))).toBe('none');
    });
});

describe('événement « exported »', () => {
    it('est une action connue du filtre', () => {
        expect(isActivityEvent('exported')).toBe(true);
        expect(isActivityEvent('inconnu')).toBe(false);
    });

    it('n’a pas de bloc « élément » dans le panneau, comme les connexions', () => {
        expect(hasNoSubjectBlock('exported')).toBe(true);
        expect(hasNoSubjectBlock('login')).toBe(true);
        expect(hasNoSubjectBlock('logout')).toBe(true);
        // Un échec de connexion NOMME le compte visé : lui garde son bloc.
        expect(hasNoSubjectBlock('login_failed')).toBe(false);
        expect(hasNoSubjectBlock('updated')).toBe(false);
    });
});

describe('filtres « élément » et « sensibles »', () => {
    const withBoth: ActivityFilters = { ...filters, subject: 'Reservation:15509', sensitive: true };

    it('se lisent depuis les filtres du serveur', () => {
        const query = queryFromFilters(withBoth);
        expect(query.subject).toBe('Reservation:15509');
        expect(query.sensitive).toBe(true);
    });

    it('s’écrivent dans l’URL, et seulement quand ils sont posés', () => {
        expect(queryToParams(queryFromFilters(withBoth))).toEqual({ from: '2026-09-01', to: '2026-09-23', subject: 'Reservation:15509', sensitive: '1' });
        expect(queryToParams(queryFromFilters(filters))).not.toHaveProperty('subject');
        expect(queryToParams(queryFromFilters(filters))).not.toHaveProperty('sensitive');
    });

    it('comptent chacun pour un dans « Tout effacer »', () => {
        const NOW = new Date(2026, 8, 23, 14, 0);
        const base = queryFromFilters({ ...filters, from: '2026-09-17', to: '2026-09-23' });
        expect(activeFilterCount({ ...base, subject: 'Property:373' }, NOW)).toBe(1);
        expect(activeFilterCount({ ...base, sensitive: true }, NOW)).toBe(1);
        expect(activeFilterCount({ ...base, subject: 'Property:373', sensitive: true }, NOW)).toBe(2);
    });

    it('sont effacés par « Tout effacer »', () => {
        expect(queryToParams({ ...queryFromFilters(withBoth), ...CLEARED_FILTERS })).toEqual({});
    });

    it('sont retirés en passant à l’onglet Équipe (ses chiffres n’en dépendent pas)', () => {
        const team = { ...queryFromFilters(withBoth), tab: 'team' as const, ...FEED_ONLY_CLEARED };
        expect(team.subject).toBeNull();
        expect(team.sensitive).toBe(false);
    });
});

describe('exportParams — l’export reprend exactement le flux affiché', () => {
    it('porte les mêmes filtres que l’URL du flux', () => {
        const query = { ...queryFromFilters(filters), user: '7', module: 'finances' as const, sensitive: true, search: 'villa' };
        expect(exportParams(query)).toEqual(queryToParams(query));
    });

    it('n’emporte jamais l’onglet', () => {
        expect(exportParams({ ...queryFromFilters(filters), tab: 'team' })).not.toHaveProperty('tab');
    });
});
