import { describe, expect, it } from 'vitest';
import {
    buildRepushPayload,
    canSubmit,
    defaultMode,
    FEE_MARKUP_PART_CODES,
    feeMarkupAnchor,
    feeMarkupIsDirty,
    groupModes,
    parseFeeMarkup,
    partsForMode,
    repushChannelsOf,
    type RepushFormState,
    type RepushLabelled,
    type RepushMode,
    type RepushOptions,
} from './repush-utils';

const part = (code: string, known = true): RepushLabelled => ({ code, label: code, known });

const mode = (name: string, isPartial: boolean, parts: RepushLabelled[] = [], known = true): RepushMode => ({
    name,
    label: name,
    known,
    is_partial: isPartial,
    parts,
});

/** Airbnb : un mode complet, un mode partiel. */
const airbnbModes: RepushMode[] = [
    mode('FullRepush', false),
    mode('PartialRepush', true, [part('Description'), part('Images'), part('FeesAndTaxes'), part('Amenities')]),
];

/** Booking.com : trois modes complets, trois partiels. */
const bookingModes: RepushMode[] = [
    mode('WholeHotelUpdate', false),
    mode('OnlyHotelUpdate', false),
    mode('OnlyRoomUpdate', false),
    mode('WholeHotelPartialUpdate', true, [part('Images'), part('FeesAndTaxes')]),
    mode('OnlyHotelPartialUpdate', true, [part('HotelImages'), part('HotelFeesAndTaxes')]),
    mode('OnlyRoomPartialUpdate', true, [part('RoomImages'), part('RoomFeesAndTaxes')]),
];

const options = (overrides: Partial<RepushOptions> = {}): RepushOptions => ({
    channel_id: '426592',
    channel_name: 'Airbnb',
    modes: airbnbModes,
    fee_markup: 10,
    fee_markup_is_default: false,
    default_fee_markup: 50,
    fee_markup_available: true,
    ...overrides,
});

const form = (overrides: Partial<RepushFormState> = {}): RepushFormState => ({
    channelId: '426592',
    modeName: 'FullRepush',
    checked: [],
    feeMarkupRaw: '',
    ...overrides,
});

describe('groupModes', () => {
    it('partitions on is_partial', () => {
        const { full, partial } = groupModes(airbnbModes);
        expect(full.map((m) => m.name)).toEqual(['FullRepush']);
        expect(partial.map((m) => m.name)).toEqual(['PartialRepush']);
    });

    it('splits a multi-unit channel into three complete and three partial modes', () => {
        const { full, partial } = groupModes(bookingModes);
        expect(full.map((m) => m.name)).toEqual(['WholeHotelUpdate', 'OnlyHotelUpdate', 'OnlyRoomUpdate']);
        expect(partial.map((m) => m.name)).toEqual(['WholeHotelPartialUpdate', 'OnlyHotelPartialUpdate', 'OnlyRoomPartialUpdate']);
    });

    it('groups an unknown mode carrying zones with the partial ones', () => {
        // Les modes RU ne forment pas un enum figé. Un mode que le serveur n'a pas
        // su traduire doit quand même faire apparaître ses cases, sinon l'écran
        // enverrait un mode partiel sans aucune zone — que RU accepterait avec un
        // statut 0 sans rien republier.
        const surprise = mode('FutureRepush', true, [part('Description', false)], false);
        const { full, partial } = groupModes([mode('FullRepush', false), surprise]);

        expect(full.map((m) => m.name)).toEqual(['FullRepush']);
        expect(partial).toEqual([surprise]);
        expect(partsForMode([surprise], 'FutureRepush').map((p) => p.code)).toEqual(['Description']);
    });

    it('yields empty groups for an empty list', () => {
        expect(groupModes([])).toEqual({ full: [], partial: [] });
    });
});

describe('defaultMode', () => {
    it('prefers the first complete mode', () => {
        // Booking.com order: a partial mode can come first in RU's response.
        const booking = [mode('OnlyHotelPartialUpdate', true, [part('Images')]), mode('WholeHotelUpdate', false)];
        expect(defaultMode(booking)).toBe('WholeHotelUpdate');
    });

    it('falls back to the first mode when none is complete', () => {
        expect(defaultMode([mode('PartialRepush', true, [part('Images')])])).toBe('PartialRepush');
    });

    it('returns null without modes', () => {
        expect(defaultMode([])).toBeNull();
    });
});

describe('partsForMode', () => {
    it('lists the parts of a partial mode', () => {
        expect(partsForMode(airbnbModes, 'PartialRepush').map((p) => p.code)).toEqual(['Description', 'Images', 'FeesAndTaxes', 'Amenities']);
    });

    it('returns nothing for a complete mode', () => {
        expect(partsForMode(airbnbModes, 'FullRepush')).toEqual([]);
    });

    it('returns nothing for an unknown mode', () => {
        expect(partsForMode(airbnbModes, 'Nope')).toEqual([]);
    });
});

describe('feeMarkupAnchor', () => {
    it('finds the first fee-bearing part', () => {
        expect(feeMarkupAnchor([part('Images'), part('FeesAndTaxes'), part('Fees')], FEE_MARKUP_PART_CODES)).toBe('FeesAndTaxes');
    });

    it('recognises the other spellings a channel may use', () => {
        expect(feeMarkupAnchor([part('Images'), part('Fees')], FEE_MARKUP_PART_CODES)).toBe('Fees');
        expect(feeMarkupAnchor(partsForMode(bookingModes, 'OnlyHotelPartialUpdate'), FEE_MARKUP_PART_CODES)).toBe('HotelFeesAndTaxes');
        expect(feeMarkupAnchor(partsForMode(bookingModes, 'OnlyRoomPartialUpdate'), FEE_MARKUP_PART_CODES)).toBe('RoomFeesAndTaxes');
    });

    it('returns null when the channel exposes no fee zone', () => {
        expect(feeMarkupAnchor([part('Images'), part('Description')], FEE_MARKUP_PART_CODES)).toBeNull();
    });
});

describe('parseFeeMarkup', () => {
    it('treats an empty field as "do not touch", never as 0 %', () => {
        expect(parseFeeMarkup('')).toEqual({ value: null, error: null });
        expect(parseFeeMarkup('   ')).toEqual({ value: null, error: null });
    });

    it('accepts the French decimal comma', () => {
        expect(parseFeeMarkup('10,5')).toEqual({ value: 10.5, error: null });
    });

    it('accepts a dot and surrounding spaces', () => {
        expect(parseFeeMarkup(' 10.5 ')).toEqual({ value: 10.5, error: null });
    });

    it('accepts both bounds', () => {
        expect(parseFeeMarkup('0')).toEqual({ value: 0, error: null });
        expect(parseFeeMarkup('100')).toEqual({ value: 100, error: null });
    });

    it('rejects values outside [0, 100]', () => {
        expect(parseFeeMarkup('101').error).toBe('range');
        expect(parseFeeMarkup('-5').error).toBe('range');
    });

    it('rejects non-numeric input', () => {
        expect(parseFeeMarkup('dix').error).toBe('format');
        expect(parseFeeMarkup('10%').error).toBe('format');
        expect(parseFeeMarkup('1,2,3').error).toBe('format');
    });
});

describe('feeMarkupIsDirty', () => {
    it('is false for an untouched field', () => {
        expect(feeMarkupIsDirty('', 10, false)).toBe(false);
    });

    it('is false when the typed value equals the pinned one', () => {
        expect(feeMarkupIsDirty('10', 10, false)).toBe(false);
        expect(feeMarkupIsDirty('10,0', 10, false)).toBe(false);
    });

    it('is true when the typed value differs', () => {
        expect(feeMarkupIsDirty('12', 10, false)).toBe(true);
    });

    it('is true whenever the property inherits the channel default', () => {
        // Typing the inherited value is an explicit choice to pin it — and pinning
        // is irreversible without the "back to default" link.
        expect(feeMarkupIsDirty('50', 0, true)).toBe(true);
    });

    it('is true when the current value could not be read', () => {
        expect(feeMarkupIsDirty('12', null, null)).toBe(true);
    });
});

describe('canSubmit', () => {
    it('refuses without loaded options', () => {
        expect(canSubmit(form(), null)).toBe(false);
    });

    it('refuses without a channel', () => {
        expect(canSubmit(form({ channelId: '' }), options())).toBe(false);
    });

    it('refuses without a mode', () => {
        expect(canSubmit(form({ modeName: '' }), options())).toBe(false);
    });

    it('refuses an unknown mode', () => {
        expect(canSubmit(form({ modeName: 'Invented' }), options())).toBe(false);
    });

    it('accepts a complete mode with no box checked', () => {
        expect(canSubmit(form(), options())).toBe(true);
    });

    it('refuses a partial mode with nothing checked', () => {
        expect(canSubmit(form({ modeName: 'PartialRepush' }), options())).toBe(false);
    });

    it('refuses a partial mode whose checked codes belong to another mode', () => {
        expect(canSubmit(form({ modeName: 'PartialRepush', checked: ['RoomImages'] }), options())).toBe(false);
    });

    it('accepts a partial mode with one box checked', () => {
        expect(canSubmit(form({ modeName: 'PartialRepush', checked: ['Images'] }), options())).toBe(true);
    });

    it('refuses an invalid fee markup', () => {
        expect(canSubmit(form({ feeMarkupRaw: '120' }), options())).toBe(false);
        expect(canSubmit(form({ feeMarkupRaw: 'abc' }), options())).toBe(false);
    });
});

describe('buildRepushPayload', () => {
    it('returns null while the form is incomplete', () => {
        expect(buildRepushPayload(form({ modeName: 'PartialRepush' }), options())).toBeNull();
        expect(buildRepushPayload(form(), null)).toBeNull();
    });

    it('sends an empty parts list for a complete mode, even with stale checkboxes', () => {
        // RU ignores RepushParts on a complete mode: sending them would suggest a
        // selection that has no effect.
        expect(buildRepushPayload(form({ checked: ['Images', 'Description'] }), options())).toEqual({
            channel_id: '426592',
            mode: 'FullRepush',
            parts: [],
        });
    });

    it('sends the checked parts in the mode order, dropping foreign codes', () => {
        const payload = buildRepushPayload(form({ modeName: 'PartialRepush', checked: ['Amenities', 'RoomImages', 'Description'] }), options());
        expect(payload).toEqual({ channel_id: '426592', mode: 'PartialRepush', parts: ['Description', 'Amenities'] });
    });

    it('omits fee_markup when the field was not touched', () => {
        const payload = buildRepushPayload(form(), options());
        expect(payload).not.toHaveProperty('fee_markup');
        expect(payload).not.toHaveProperty('fee_markup_seen');
    });

    it('omits fee_markup when the typed value matches the pinned one', () => {
        expect(buildRepushPayload(form({ feeMarkupRaw: '10' }), options())).not.toHaveProperty('fee_markup');
    });

    it('sends fee_markup with both witnesses when the value changed', () => {
        expect(buildRepushPayload(form({ feeMarkupRaw: '12,5' }), options())).toEqual({
            channel_id: '426592',
            mode: 'FullRepush',
            parts: [],
            fee_markup: 12.5,
            fee_markup_seen: 10,
            fee_markup_seen_is_default: false,
        });
    });

    it('sends the witnesses even when the property inherited the channel value', () => {
        const inherited = options({ fee_markup: null, fee_markup_is_default: true });
        expect(buildRepushPayload(form({ feeMarkupRaw: '30' }), inherited)).toMatchObject({
            fee_markup: 30,
            fee_markup_seen: null,
            fee_markup_seen_is_default: true,
        });
    });

    it('sends the reset flag alone — RU forbids pairing it with a value', () => {
        const payload = buildRepushPayload(form({ feeMarkupRaw: '', resetFeeMarkup: true }), options());
        expect(payload).toEqual({ channel_id: '426592', mode: 'FullRepush', parts: [], reset_fee_markup_to_default: true });
    });
});

describe('repushChannelsOf', () => {
    const direct = [
        { id: 'thelandlord', name: 'The Landlord', role: 'own' },
        { id: '426592', name: 'Airbnb', role: 'primary' },
        { id: '421327', name: 'Booking.com', role: 'primary' },
        { id: '421402', name: 'Expedia', role: 'optional' },
    ];

    it('keeps only connected channels, in the display order', () => {
        const listings = [{ channel_id: '421402' }, { channel_id: '426592' }];
        expect(repushChannelsOf(listings, direct)).toEqual([
            { id: '426592', name: 'Airbnb' },
            { id: '421402', name: 'Expedia' },
        ]);
    });

    it('never offers The Landlord — it is not a Rentals United channel', () => {
        expect(repushChannelsOf([{ channel_id: 'thelandlord' }], direct)).toEqual([]);
    });

    it('ignores a listing on a channel we do not know', () => {
        expect(repushChannelsOf([{ channel_id: '999999' }], direct)).toEqual([]);
    });

    it('returns nothing when the property is connected nowhere', () => {
        expect(repushChannelsOf([], direct)).toEqual([]);
    });
});
