import type { LogementShowCancellation, LogementShowStay } from '@/types/logement-show';
import { describe, expect, it } from 'vitest';
import {
    cancellationAxisStart,
    cancellationSegmentLabel,
    cancellationSegments,
    checkInWindow,
    dayMonth,
    depositLabel,
    distributeWidths,
    fill,
    floorLabel,
    formatMoney,
    formatPercent,
    intlLocale,
    isDepositAmountIgnored,
    monthName,
    nightsLabel,
    stayMeta,
    weekdayDayMonth,
    type Translate,
} from './format';

/** `t` de test : rend le repli français, comme une traduction absente. */
const t: Translate = (_namespace, key, fallback) => fallback ?? key;

/** Intl insère des espaces insécables (fines ou non) : NFKC les ramène à l'espace simple. */
const plain = (value: string) => value.normalize('NFKC');

const stay: LogementShowStay = {
    id: 1,
    guest_name: 'Paul',
    check_in: '08/10/2026',
    check_out: '11/10/2026',
    check_in_iso: '2026-10-08',
    check_out_iso: '2026-10-11',
    nights: 3,
    source: 'Airbnb',
    arrival_time: '16:00',
    is_owner: false,
    url: null,
    instructions_url: null,
    instructions_valid_until: null,
    is_paid: true,
};

describe('locale et gabarits', () => {
    it('ne connaît que fr et en, et retombe sur le français', () => {
        expect(intlLocale('en')).toBe('en-GB');
        expect(intlLocale('fr')).toBe('fr-FR');
        expect(intlLocale(undefined)).toBe('fr-FR');
    });

    it('remplit les paramètres sans relire une valeur injectée', () => {
        expect(fill('Arrivée de :name', { name: 'Paul' })).toBe('Arrivée de Paul');
        expect(fill(':from → :to', { from: '16:00', to: '02:00' })).toBe('16:00 → 02:00');
        expect(fill('dès :time', {})).toBe('dès :time');
    });
});

describe('montants', () => {
    it('deux décimales et le code devise, selon la langue', () => {
        expect(plain(formatMoney(150, 'USD', 'fr'))).toBe('150,00 USD');
        expect(plain(formatMoney(1500.5, 'EUR', 'fr'))).toBe('1 500,50 EUR');
        expect(formatMoney(1500.5, 'EUR', 'en')).toBe('1,500.50 EUR');
    });

    it('sans devise connue, le montant seul', () => {
        expect(plain(formatMoney(50, null, 'fr'))).toBe('50,00');
    });

    it('les taux gardent une décimale au plus', () => {
        expect(plain(formatPercent(84.3, 'fr'))).toBe('84,3 %');
        expect(formatPercent(84, 'en')).toBe('84%');
        expect(plain(formatPercent(0, 'fr'))).toBe('0 %');
    });
});

describe('nuits et étage', () => {
    it('accorde « nuit » selon la langue', () => {
        expect(nightsLabel(1, 'fr', t)).toBe('1 nuit');
        expect(nightsLabel(3, 'fr', t)).toBe('3 nuits');
        expect(nightsLabel(4.5, 'fr', t)).toBe('4,5 nuits');
        // En français, 1,5 reste au singulier.
        expect(nightsLabel(1.5, 'fr', t)).toBe('1,5 nuit');
    });

    it('le rez-de-chaussée est nommé, l’étage inconnu dit « Non défini »', () => {
        expect(floorLabel(0, 'fr', t)).toBe('Rez-de-chaussée');
        expect(floorLabel(3, 'fr', t)).toBe('3');
        expect(floorLabel(-1, 'fr', t)).toBe('-1');
        expect(floorLabel(null, 'fr', t)).toBe('Non défini');
    });
});

describe('dépôt de garantie', () => {
    it('chaque type a son libellé', () => {
        expect(depositLabel({ type: 'none', amount: null }, 'USD', 'fr', t)).toBe('Aucun dépôt');
        expect(depositLabel({ type: 'percent_without_cleaning', amount: 30 }, 'USD', 'fr', t)).toBe('30 % du total (hors ménage)');
        expect(depositLabel({ type: 'percent_total', amount: 30 }, 'USD', 'fr', t)).toBe('30 % du total');
        expect(plain(depositLabel({ type: 'per_night', amount: 150 }, 'USD', 'fr', t))).toBe('150,00 USD par nuit');
        expect(plain(depositLabel({ type: 'per_stay', amount: 150 }, 'USD', 'fr', t))).toBe('150,00 USD par séjour');
        expect(depositLabel({ type: 'unknown', amount: 150 }, 'USD', 'fr', t)).toBe('Non défini');
    });

    it('un type à montant sans montant est « Non défini », pas « 0 »', () => {
        expect(depositLabel({ type: 'per_stay', amount: null }, 'USD', 'fr', t)).toBe('Non défini');
    });

    it('signale un montant saisi que le type « aucun dépôt » n’applique pas', () => {
        expect(isDepositAmountIgnored({ type: 'none', amount: 150 })).toBe(true);
        expect(isDepositAmountIgnored({ type: 'none', amount: 0 })).toBe(false);
        expect(isDepositAmountIgnored({ type: 'none', amount: null })).toBe(false);
        expect(isDepositAmountIgnored({ type: 'per_stay', amount: 150 })).toBe(false);
    });
});

describe('arrivée', () => {
    it('plage complète, borne seule, ou non définie', () => {
        const base = { check_out_until: '11:00', preparation_hours: 4 };
        expect(checkInWindow({ ...base, check_in_from: '16:00', check_in_to: '02:00' }, t)).toBe('16:00 → 02:00');
        expect(checkInWindow({ ...base, check_in_from: '16:00', check_in_to: null }, t)).toBe('dès 16:00');
        expect(checkInWindow({ ...base, check_in_from: null, check_in_to: '22:00' }, t)).toBe("jusqu'à 22:00");
        expect(checkInWindow({ ...base, check_in_from: null, check_in_to: null }, t)).toBe('Non défini');
    });
});

describe('dates', () => {
    it('lit les champs ISO sans décalage de fuseau', () => {
        expect(dayMonth('2026-10-08')).toBe('08/10');
        expect(weekdayDayMonth('2026-10-08', 'fr')).toBe('jeudi 08/10');
        expect(weekdayDayMonth('2026-10-08', 'en')).toBe('Thursday 08/10');
        expect(monthName('2026-10-08', 'fr')).toBe('octobre');
        expect(monthName('2026-10-08', 'en')).toBe('October');
    });

    it('rend la valeur telle quelle si elle n’est pas une date ISO', () => {
        expect(dayMonth('n/a')).toBe('n/a');
        expect(weekdayDayMonth('n/a', 'fr')).toBe('n/a');
    });

    it('détaille un séjour : dates, nuits, source, heure d’arrivée sur demande', () => {
        expect(stayMeta(stay, 'fr', t, { arrivalTime: true })).toBe('08/10/2026 → 11/10/2026 · 3 nuits · Airbnb · dès 16:00');
        expect(stayMeta(stay, 'fr', t)).toBe('08/10/2026 → 11/10/2026 · 3 nuits · Airbnb');
        expect(stayMeta({ ...stay, arrival_time: null }, 'fr', t, { dates: false, arrivalTime: true })).toBe('3 nuits · Airbnb');
    });
});

describe("barre d'annulation", () => {
    const policy: LogementShowCancellation = {
        // Du plus proche de l'arrivée au plus lointain, comme le serveur les envoie.
        tiers: [
            { from_days: 0, to_days: 7, percent: 100 },
            { from_days: 8, to_days: 30, percent: 50 },
        ],
        free_from_days: 31,
    };

    it('aucun palier = aucune barre', () => {
        expect(cancellationSegments({ tiers: [], free_from_days: 31 })).toEqual([]);
    });

    it('va du plus lointain à l’arrivée : gratuit, puis chaque palier', () => {
        const segments = cancellationSegments(policy);
        expect(segments.map((segment) => segment.tone)).toEqual(['free', 'partial', 'full']);
        expect(segments.map((segment) => [segment.fromDays, segment.toDays])).toEqual([
            [31, null],
            [8, 30],
            [0, 7],
        ]);
    });

    it('les largeurs font 100 % et suivent la durée de chaque palier', () => {
        const segments = cancellationSegments(policy);
        const total = segments.reduce((sum, segment) => sum + segment.width, 0);
        expect(total).toBeCloseTo(100, 1);
        // 23 jours à 50 % pèsent plus que 8 jours à 100 %.
        expect(segments[1].width).toBeGreaterThan(segments[2].width);
        for (const segment of segments) {
            expect(segment.width).toBeGreaterThanOrEqual(12);
        }
    });

    it('sans annulation gratuite, pas de segment vert', () => {
        const segments = cancellationSegments({ ...policy, free_from_days: null });
        expect(segments.map((segment) => segment.tone)).toEqual(['partial', 'full']);
    });

    it('un palier à 0 % se lit comme gratuit', () => {
        const segments = cancellationSegments({ tiers: [{ from_days: 0, to_days: 3, percent: 0 }], free_from_days: null });
        expect(segments[0].tone).toBe('free');
        expect(cancellationSegmentLabel(segments[0], 'fr', t).title).toBe('Gratuite');
    });

    it('nomme chaque segment et sa plage en jours', () => {
        const [free, partial, full] = cancellationSegments(policy);
        expect(cancellationSegmentLabel(free, 'fr', t)).toEqual({ title: 'Gratuite', range: "31 j et plus avant l'arrivée" });
        expect(cancellationSegmentLabel(partial, 'fr', t)).toEqual({ title: '50 % facturés', range: "de 8 à 30 j avant l'arrivée" });
        expect(cancellationSegmentLabel(full, 'fr', t)).toEqual({ title: '100 % facturés', range: "de 0 à 7 j avant l'arrivée" });
    });

    it('un palier d’un seul jour', () => {
        const [arrival] = cancellationSegments({ tiers: [{ from_days: 0, to_days: 0, percent: 100 }], free_from_days: null });
        expect(cancellationSegmentLabel(arrival, 'fr', t).range).toBe("le jour de l'arrivée");
        const [single] = cancellationSegments({ tiers: [{ from_days: 2, to_days: 2, percent: 100 }], free_from_days: null });
        expect(cancellationSegmentLabel(single, 'fr', t).range).toBe("2 j avant l'arrivée");
    });

    it('légende le bord gauche de la barre', () => {
        expect(cancellationAxisStart(cancellationSegments(policy), t)).toBe('31 j et plus');
        expect(cancellationAxisStart(cancellationSegments({ ...policy, free_from_days: null }), t)).toBe('30 j avant');
        expect(cancellationAxisStart([], t)).toBe('');
    });

    it('répartit les largeurs avec un minimum, ou à parts égales au-delà', () => {
        expect(distributeWidths([])).toEqual([]);
        const widths = distributeWidths([1, 99]);
        expect(widths[0]).toBeGreaterThanOrEqual(12);
        expect(widths[0] + widths[1]).toBeCloseTo(100, 5);
        expect(distributeWidths([1, 1, 1, 1, 1, 1, 1, 1, 1], 12)).toEqual(Array(9).fill(100 / 9));
    });
});
