import { beforeEach, describe, expect, it, vi } from 'vitest'

/**
 * Tests du workflow « Ajouter un transporteur » (M4 Transport, ERP-165).
 *
 * `useCarrierForm` porte le formulaire principal (Nom + Certification + Affréter)
 * et l'orchestration des onglets de création. On vérifie ici le CONTRAT propre à
 * la création :
 *  - pré-check front : nom requis → POST bloqué, erreur inline, aucun appel réseau ;
 *  - POST /carriers (groupe carrier:write:main) : payload + Accept ld+json +
 *    toast:false ; au succès, verrouillage + bascule sur l'onglet Qualimat +
 *    réaffichage du nom normalisé ;
 *  - 409 doublon (RG-4.12) → erreur inline dédiée sur `name` ;
 *  - 422 → mapping inline par champ (propertyPath) ;
 *  - onglets : 4 onglets (Qualimat/Adresses/Contacts/Prix), completeTab
 *    déverrouille/avance et signale le dernier onglet ;
 *  - patchCarrier : PATCH partiel, no-op avant création.
 */

const mockPost = vi.hoisted(() => vi.fn())
const mockPatch = vi.hoisted(() => vi.fn())
const mockDelete = vi.hoisted(() => vi.fn())

vi.stubGlobal('useApi', () => ({
    get: vi.fn(),
    post: mockPost,
    put: vi.fn(),
    patch: mockPatch,
    delete: mockDelete,
}))
vi.stubGlobal('useI18n', () => ({ t: (key: string) => key }))
vi.stubGlobal('useToast', () => ({
    success: vi.fn(),
    error: vi.fn(),
    warning: vi.fn(),
    info: vi.fn(),
}))

const { useCarrierForm, CARRIER_TAB_KEYS } = await import('../useCarrierForm')
const { buildCarrierContactPayload, isCarrierContactBlank, isCarrierContactNamed } = await import('../../utils/forms/carrierContact')
const { buildCarrierPricePayload, isCarrierPriceValid } = await import('../../utils/forms/carrierPrice')
const { emptyCarrierContact, emptyCarrierPrice } = await import('../../types/carrierForm')

describe('useCarrierForm', () => {
    beforeEach(() => {
        mockPost.mockReset()
        mockPatch.mockReset()
    })

    it('front : nom vide → erreur inline sur name, pas de POST', async () => {
        const form = useCarrierForm()

        const created = await form.submitMain()

        expect(created).toBe(false)
        expect(mockPost).not.toHaveBeenCalled()
        expect(form.mainErrors.errors.name).toBe('transport.carriers.form.errors.nameRequired')
        expect(form.mainLocked.value).toBe(false)
    })

    it('front : nom en espaces uniquement → erreur inline, pas de POST', async () => {
        const form = useCarrierForm()
        form.main.name = '   '

        const created = await form.submitMain()

        expect(created).toBe(false)
        expect(mockPost).not.toHaveBeenCalled()
        expect(form.mainErrors.errors.name).toBe('transport.carriers.form.errors.nameRequired')
    })

    it('front : certification vide (hors LIOT) → erreur inline sur certificationType, pas de POST', async () => {
        const form = useCarrierForm()
        form.main.name = 'Acme'
        // certificationType laissé null → bloqué côté front (RG-4.01).

        const created = await form.submitMain()

        expect(created).toBe(false)
        expect(mockPost).not.toHaveBeenCalled()
        expect(form.mainErrors.errors.certificationType).toBe('transport.carriers.form.errors.certificationRequired')
    })

    it('front : cas LIOT → certification non requise (aucune erreur de certification)', async () => {
        mockPost.mockResolvedValueOnce({ id: 5, name: 'LIOT', certificationType: null })
        const form = useCarrierForm()
        form.main.name = 'LIOT'
        form.main.liotPlates = 'AA-123-BB'

        const created = await form.submitMain()

        expect(created).toBe(true)
        expect(form.mainErrors.errors.certificationType).toBeUndefined()
    })

    it('front RG-4.02 : certification AUTRE sans décharge → erreur inline, pas de POST', async () => {
        const form = useCarrierForm()
        form.main.name = 'Acme'
        form.main.certificationType = 'AUTRE'
        // dischargeDocumentIri null (upload non fourni).

        const created = await form.submitMain()

        expect(created).toBe(false)
        expect(mockPost).not.toHaveBeenCalled()
        expect(form.mainErrors.errors.dischargeDocument).toBe('transport.carriers.form.errors.dischargeRequired')
    })

    it('front RG-4.03 : affrété sans indexation / contenant / volume → 3 erreurs inline, pas de POST', async () => {
        const form = useCarrierForm()
        form.main.name = 'Acme'
        form.main.certificationType = 'GMP_PLUS'
        form.main.isChartered = true

        const created = await form.submitMain()

        expect(created).toBe(false)
        expect(mockPost).not.toHaveBeenCalled()
        expect(form.mainErrors.errors.indexationRate).toBe('transport.carriers.form.errors.indexationRequired')
        expect(form.mainErrors.errors.containerType).toBe('transport.carriers.form.errors.containerTypeRequired')
        expect(form.mainErrors.errors.volumeM3).toBe('transport.carriers.form.errors.volumeRequired')
    })

    it('front RG-4.03 : affrété avec tous les champs remplis → POST envoyé', async () => {
        mockPost.mockResolvedValueOnce({ id: 8, name: 'ACME', certificationType: 'GMP_PLUS' })
        const form = useCarrierForm()
        form.main.name = 'Acme'
        form.main.certificationType = 'GMP_PLUS'
        form.main.isChartered = true
        form.main.indexationRate = '5'
        form.main.containerType = 'BENNE'
        form.main.volumeM3 = '30'

        const created = await form.submitMain()

        expect(created).toBe(true)
        expect(mockPost).toHaveBeenCalledTimes(1)
        expect(mockPost.mock.calls[0]?.[1]).toMatchObject({
            isChartered: true,
            indexationRate: '5',
            containerType: 'BENNE',
            volumeM3: '30',
        })
    })

    it('POST /carriers avec Accept ld+json, verrouille et bascule sur Qualimat', async () => {
        mockPost.mockResolvedValueOnce({ id: 42, name: 'TRANSPORTS ACME', certificationType: 'GMP_PLUS' })
        const form = useCarrierForm()
        form.main.name = 'Transports Acme'
        form.main.certificationType = 'GMP_PLUS'

        const created = await form.submitMain()

        expect(created).toBe(true)
        expect(mockPost).toHaveBeenCalledTimes(1)
        const [url, body, opts] = mockPost.mock.calls[0] ?? []
        expect(url).toBe('/carriers')
        expect(body).toEqual({
            name: 'Transports Acme',
            certificationType: 'GMP_PLUS',
            isChartered: false,
        })
        expect(opts).toMatchObject({ toast: false, headers: { Accept: 'application/ld+json' } })

        expect(form.carrierId.value).toBe(42)
        // RG-4.13 : réaffiche le nom normalisé (UPPERCASE) renvoyé par le serveur.
        expect(form.main.name).toBe('TRANSPORTS ACME')
        expect(form.mainLocked.value).toBe(true)
        // L'onglet Qualimat était déjà accessible (saisie assistée) ; le POST
        // déverrouille Adresses (index 1) et bascule dessus.
        expect(form.activeTab.value).toBe('addresses')
        expect(form.unlockedIndex.value).toBe(1)
    })

    it('buildMainPayload : omet certificationType vide, garde isChartered', () => {
        const form = useCarrierForm()
        form.main.name = 'X'

        const body = form.buildMainPayload()
        expect(body).toEqual({ name: 'X', isChartered: false })
        expect('certificationType' in body).toBe(false)
    })

    it('409 doublon (RG-4.12) : erreur inline dédiée sur name, pas de verrouillage', async () => {
        mockPost.mockRejectedValueOnce({ response: { status: 409 } })
        const form = useCarrierForm()
        form.main.name = 'Doublon'
        form.main.certificationType = 'GMP_PLUS'

        const created = await form.submitMain()

        expect(created).toBe(false)
        expect(form.mainErrors.errors.name).toBe('transport.carriers.form.duplicateName')
        expect(form.mainLocked.value).toBe(false)
    })

    it('422 : mappe les violations serveur inline par champ', async () => {
        // Contrainte re-validée uniquement back (ex. longueur du nom) : le pré-check
        // front passe (nom rempli, certif choisie, non affrété), la 422 mappe inline
        // sur le champ via son propertyPath.
        mockPost.mockRejectedValueOnce({
            response: {
                status: 422,
                _data: { violations: [{ propertyPath: 'name', message: 'Le nom du transporteur ne peut dépasser 255 caractères.' }] },
            },
        })
        const form = useCarrierForm()
        form.main.name = 'Acme'
        form.main.certificationType = 'GMP_PLUS'

        const created = await form.submitMain()

        expect(created).toBe(false)
        expect(form.mainErrors.errors.name).toBe('Le nom du transporteur ne peut dépasser 255 caractères.')
        expect(form.mainLocked.value).toBe(false)
    })

    it('onglets : 4 clés Qualimat/Adresses/Contacts/Prix', () => {
        expect(CARRIER_TAB_KEYS).toEqual(['qualimat', 'addresses', 'contacts', 'prices'])
        const form = useCarrierForm()
        expect(form.tabKeys.value).toEqual(['qualimat', 'addresses', 'contacts', 'prices'])
        // L'onglet Qualimat (index 0) est accessible dès le départ (saisie assistée) ;
        // Adresses / Contacts / Prix restent verrouillés jusqu'au POST principal.
        expect(form.unlockedIndex.value).toBe(0)
    })

    it('completeTab : déverrouille/avance, et signale le dernier onglet du flux', () => {
        const form = useCarrierForm()

        // Qualimat → Adresses (pas le dernier).
        expect(form.completeTab('qualimat')).toBe(false)
        expect(form.isValidated('qualimat')).toBe(true)
        expect(form.activeTab.value).toBe('addresses')
        expect(form.unlockedIndex.value).toBe(1)

        expect(form.completeTab('addresses')).toBe(false)
        expect(form.activeTab.value).toBe('contacts')

        expect(form.completeTab('contacts')).toBe(false)
        expect(form.activeTab.value).toBe('prices')

        // Prix = dernier onglet → true (création terminée).
        expect(form.completeTab('prices')).toBe(true)
        expect(form.isValidated('prices')).toBe(true)
    })

    it('editMode : completeTab ne verrouille pas et ne bascule pas d\'onglet', () => {
        const form = useCarrierForm()
        form.editMode.value = true
        form.activeTab.value = 'qualimat'

        expect(form.completeTab('qualimat')).toBe(false)
        expect(form.isValidated('qualimat')).toBe(false)
        expect(form.activeTab.value).toBe('qualimat')
    })

    it('patchCarrier : PATCH /carriers/{id} en mode strict, no-op avant création', async () => {
        const form = useCarrierForm()

        await form.patchCarrier({ liotPlates: 'AA-123-BB' })
        expect(mockPatch).not.toHaveBeenCalled()

        mockPost.mockResolvedValueOnce({ id: 9, name: 'ACME', certificationType: 'OVOCOM' })
        form.main.name = 'Acme'
        form.main.certificationType = 'OVOCOM'
        await form.submitMain()

        await form.patchCarrier({ liotPlates: 'AA-123-BB' })
        expect(mockPatch).toHaveBeenCalledWith('/carriers/9', { liotPlates: 'AA-123-BB' }, { toast: false })
    })
})

describe('useCarrierForm — champs conditionnels (ERP-166)', () => {
    beforeEach(() => {
        mockPost.mockReset()
        mockPatch.mockReset()
    })

    it('cas LIOT (insensible à la casse) : masque la certification, payload réduit', () => {
        const form = useCarrierForm()
        form.main.name = 'liot'

        expect(form.isLiot.value).toBe(true)
        expect(form.showCertification.value).toBe(false)

        form.main.liotPlates = 'AA-123-BB ; CC-456-DD'
        expect(form.buildMainPayload()).toEqual({
            name: 'liot',
            isChartered: false,
            liotPlates: 'AA-123-BB ; CC-456-DD',
        })
    })

    it('LIOT masque les champs conditionnels (affrètement / décharge)', () => {
        const form = useCarrierForm()
        form.main.name = 'LIOT'
        form.main.isChartered = true
        form.main.certificationType = 'AUTRE'

        expect(form.showCharteredFields.value).toBe(false)
        expect(form.showDischarge.value).toBe(false)
    })

    it('RG-4.03 affrété : indexation / contenant / volume visibles et dans le payload', () => {
        const form = useCarrierForm()
        form.main.name = 'Acme'
        form.main.certificationType = 'GMP_PLUS'
        form.main.isChartered = true

        expect(form.showCharteredFields.value).toBe(true)

        form.main.indexationRate = '5'
        form.main.containerType = 'BENNE'
        form.main.volumeM3 = '30'

        expect(form.buildMainPayload()).toEqual({
            name: 'Acme',
            certificationType: 'GMP_PLUS',
            isChartered: true,
            indexationRate: '5',
            containerType: 'BENNE',
            volumeM3: '30',
        })
    })

    it('RG-4.03 affrété mais champs vides : omis du payload (422 NotBlank back)', () => {
        const form = useCarrierForm()
        form.main.name = 'Acme'
        form.main.certificationType = 'GMP_PLUS'
        form.main.isChartered = true

        expect(form.buildMainPayload()).toEqual({
            name: 'Acme',
            certificationType: 'GMP_PLUS',
            isChartered: true,
        })
    })

    it('RG-4.02 AUTRE : décharge visible + dischargeDocument dans le payload si IRI résolu', () => {
        const form = useCarrierForm()
        form.main.name = 'Acme'
        form.main.certificationType = 'AUTRE'

        expect(form.showDischarge.value).toBe(true)

        form.main.dischargeDocumentIri = '/api/uploaded_documents/7'
        expect(form.buildMainPayload()).toMatchObject({ dischargeDocument: '/api/uploaded_documents/7' })
    })
})

describe('useCarrierForm — copie QUALIMAT (ERP-166)', () => {
    const QUALIMAT_ROW = {
        '@id': '/api/qualimat_carriers/42',
        id: '42',
        name: 'TRANSPORTS QUALIMAT',
        siret: '12345678900012',
        address: '1 rue du Port',
        postalCode: '86000',
        city: 'Poitiers',
        validityDate: '2027-01-15',
        status: 'VALIDE',
    }

    beforeEach(() => {
        mockPost.mockReset()
        mockPatch.mockReset()
    })

    it('copie name + certificationType=QUALIMAT (readonly) + IRI + adresse, sans PATCH avant création', async () => {
        const form = useCarrierForm()

        const ok = await form.applyQualimatSelection(QUALIMAT_ROW)

        expect(ok).toBe(true)
        expect(mockPatch).not.toHaveBeenCalled()
        expect(form.main.name).toBe('TRANSPORTS QUALIMAT')
        expect(form.main.certificationType).toBe('QUALIMAT')
        expect(form.main.qualimatCarrierIri).toBe('/api/qualimat_carriers/42')
        expect(form.isQualimat.value).toBe(true)
        expect(form.certificationReadonly.value).toBe(true)
        expect(form.qualimatAddress.value).toEqual({
            country: 'France',
            postalCode: '86000',
            city: 'Poitiers',
            street: '1 rue du Port',
        })
    })

    it('après création : PATCH /carriers/{id} avec qualimatCarrier + name + certification', async () => {
        mockPost.mockResolvedValueOnce({ id: 9, name: 'X', certificationType: 'GMP_PLUS' })
        mockPatch.mockResolvedValueOnce({})
        const form = useCarrierForm()
        form.main.name = 'X'
        form.main.certificationType = 'GMP_PLUS'
        await form.submitMain()

        const ok = await form.applyQualimatSelection(QUALIMAT_ROW)

        expect(ok).toBe(true)
        expect(mockPatch).toHaveBeenCalledWith(
            '/carriers/9',
            {
                qualimatCarrier: '/api/qualimat_carriers/42',
                name: 'TRANSPORTS QUALIMAT',
                certificationType: 'QUALIMAT',
            },
            { toast: false },
        )
    })

    it('après création : PATCH en échec → pas de copie locale (rollback) et retour false', async () => {
        mockPost.mockResolvedValueOnce({ id: 9, name: 'X', certificationType: 'GMP_PLUS' })
        mockPatch.mockRejectedValueOnce({ response: { status: 500, _data: {} } })
        const form = useCarrierForm()
        form.main.name = 'X'
        form.main.certificationType = 'GMP_PLUS'
        await form.submitMain()

        const ok = await form.applyQualimatSelection(QUALIMAT_ROW)

        // Échec serveur : l'UI ne doit pas refléter une intégration QUALIMAT non persistée.
        expect(ok).toBe(false)
        expect(form.main.name).toBe('X')
        expect(form.main.certificationType).toBe('GMP_PLUS')
        expect(form.main.qualimatCarrierIri).toBeNull()
    })

    it('buildMainPayload inclut qualimatCarrier + certificationType QUALIMAT après intégration', async () => {
        const form = useCarrierForm()
        form.main.name = 'Acme'
        await form.applyQualimatSelection(QUALIMAT_ROW)

        expect(form.buildMainPayload()).toMatchObject({
            qualimatCarrier: '/api/qualimat_carriers/42',
            certificationType: 'QUALIMAT',
        })
    })

    it('applyQualimatSelection pré-remplit le 1er bloc d\'adresse (RG-4.05)', async () => {
        const form = useCarrierForm()
        form.main.name = 'Acme'

        await form.applyQualimatSelection(QUALIMAT_ROW)

        expect(form.addresses.value).toHaveLength(1)
        expect(form.addresses.value[0]).toEqual({
            id: null,
            country: 'France',
            postalCode: '86000',
            city: 'Poitiers',
            street: '1 rue du Port',
            streetComplement: null,
        })
    })
})

describe('useCarrierForm — onglet Adresses (ERP-167)', () => {
    beforeEach(() => {
        mockPost.mockReset()
        mockPatch.mockReset()
        mockDelete.mockReset()
    })

    /** Transporteur créé, onglet Adresses accessible. */
    function createdForm() {
        const form = useCarrierForm()
        form.carrierId.value = 7
        return form
    }

    /** Remplit un bloc adresse complet (CP + ville + rue). */
    function fillAddress(form: ReturnType<typeof useCarrierForm>, index = 0): void {
        const a = form.addresses.value[index]
        if (a) {
            a.postalCode = '86100'
            a.city = 'Châtellerault'
            a.street = '1 rue du Test'
        }
    }

    it('canAddAddress : désactivé tant que la dernière adresse est incomplète', () => {
        const form = createdForm()
        expect(form.canAddAddress.value).toBe(false)

        form.addAddress()
        expect(form.addresses.value).toHaveLength(1) // no-op tant qu'incomplète

        fillAddress(form)
        expect(form.canAddAddress.value).toBe(true)
        form.addAddress()
        expect(form.addresses.value).toHaveLength(2)
    })

    it('submitAddresses : POST des nouvelles adresses, capture l\'id, finalise l\'onglet', async () => {
        mockPost.mockResolvedValueOnce({ id: 88 })
        const form = createdForm()
        fillAddress(form)

        const ok = await form.submitAddresses(vi.fn())

        expect(ok).toBe(true)
        const [url, body, opts] = mockPost.mock.calls[0] ?? []
        expect(url).toBe('/carriers/7/addresses')
        expect(body).toEqual({
            country: 'France',
            postalCode: '86100',
            city: 'Châtellerault',
            street: '1 rue du Test',
            streetComplement: null,
        })
        expect(opts).toMatchObject({ toast: false, headers: { Accept: 'application/ld+json' } })
        expect(form.addresses.value[0]?.id).toBe(88)
        expect(form.isValidated('addresses')).toBe(true)
    })

    it('submitAddresses : PATCH des adresses existantes sur /carrier_addresses/{id}', async () => {
        mockPatch.mockResolvedValueOnce({})
        const form = createdForm()
        fillAddress(form)
        const first = form.addresses.value[0]
        if (first) first.id = 88

        await form.submitAddresses(vi.fn())

        expect(mockPost).not.toHaveBeenCalled()
        expect(mockPatch).toHaveBeenCalledWith('/carrier_addresses/88', expect.objectContaining({ city: 'Châtellerault' }), { toast: false })
    })

    it('submitAddresses : mappe les 422 PAR LIGNE et ne finalise pas l\'onglet (RG-4.05)', async () => {
        mockPost.mockRejectedValueOnce({
            response: {
                status: 422,
                _data: { violations: [{ propertyPath: 'city', message: 'La ville est obligatoire pour un transporteur affrété.' }] },
            },
        })
        const form = createdForm()
        fillAddress(form)

        const ok = await form.submitAddresses(vi.fn())

        expect(ok).toBe(false)
        expect(form.addressErrors.value[0]?.city).toBe('La ville est obligatoire pour un transporteur affrété.')
        expect(form.isValidated('addresses')).toBe(false)
    })

    it('removeAddress : DELETE /carrier_addresses/{id} puis retrait du bloc', async () => {
        mockDelete.mockResolvedValueOnce({})
        const form = createdForm()
        fillAddress(form)
        const first = form.addresses.value[0]
        if (first) first.id = 88
        form.addAddress()
        fillAddress(form, 1)

        await form.removeAddress(0)

        expect(mockDelete).toHaveBeenCalledWith('/carrier_addresses/88', {}, { toast: false })
        expect(form.addresses.value).toHaveLength(1)
    })
})

describe('carrierContact (util) — validité alignée M1/M2/M3 + max 2 téléphones', () => {
    it('isCarrierContactBlank : vrai si vide, faux dès un champ comptant rempli (phoneSecondary exclu)', () => {
        expect(isCarrierContactBlank(emptyCarrierContact())).toBe(true)
        expect(isCarrierContactBlank({ ...emptyCarrierContact(), jobTitle: 'Acheteur' })).toBe(false)
        expect(isCarrierContactBlank({ ...emptyCarrierContact(), phonePrimary: '0102030405' })).toBe(false)
        // phoneSecondary seul ne compte pas (aligné M1/M2/M3).
        expect(isCarrierContactBlank({ ...emptyCarrierContact(), phoneSecondary: '0605040302', hasSecondaryPhone: true })).toBe(true)
    })

    it('isCarrierContactNamed : nommé seulement avec un prénom OU un nom', () => {
        expect(isCarrierContactNamed(emptyCarrierContact())).toBe(false)
        expect(isCarrierContactNamed({ ...emptyCarrierContact(), firstName: 'Jean' })).toBe(true)
        expect(isCarrierContactNamed({ ...emptyCarrierContact(), lastName: 'Doe' })).toBe(true)
        // Fonction / téléphone / email seuls ne « nomment » pas (≠ RG-4.08 large).
        expect(isCarrierContactNamed({ ...emptyCarrierContact(), jobTitle: 'Acheteur' })).toBe(false)
        expect(isCarrierContactNamed({ ...emptyCarrierContact(), email: 'a@b.fr' })).toBe(false)
    })

    it('buildCarrierContactPayload : phones = 1 numéro sans secondaire', () => {
        const body = buildCarrierContactPayload({ ...emptyCarrierContact(), phonePrimary: '0102030405' })
        expect(body.phones).toEqual(['0102030405'])
    })

    it('buildCarrierContactPayload : phones = 2 numéros si secondaire révélé', () => {
        const body = buildCarrierContactPayload({
            ...emptyCarrierContact(),
            phonePrimary: '0102030405',
            phoneSecondary: '0605040302',
            hasSecondaryPhone: true,
        })
        expect(body.phones).toEqual(['0102030405', '0605040302'])
    })

    it('buildCarrierContactPayload : 2e numéro ignoré tant que non révélé', () => {
        const body = buildCarrierContactPayload({
            ...emptyCarrierContact(),
            phonePrimary: '0102030405',
            phoneSecondary: '0605040302',
            hasSecondaryPhone: false,
        })
        expect(body.phones).toEqual(['0102030405'])
    })
})

describe('useCarrierForm — onglet Contacts (ERP-168)', () => {
    beforeEach(() => {
        mockPost.mockReset()
        mockPatch.mockReset()
        mockDelete.mockReset()
    })

    /** Transporteur créé, onglet Contacts accessible. */
    function createdForm() {
        const form = useCarrierForm()
        form.carrierId.value = 7
        return form
    }

    it('« + Nouveau contact » désactivé tant que le bloc n\'est pas nommé (prénom OU nom, aligné M1/M2/M3)', () => {
        const form = createdForm()
        expect(form.canAddContact.value).toBe(false)

        // addContact est un no-op tant que le bloc n'est pas nommé.
        form.addContact()
        expect(form.contacts.value).toHaveLength(1)

        // Fonction seule ne suffit PAS à ajouter un nouveau bloc (≠ RG-4.08 large).
        const first = form.contacts.value[0]
        if (first) first.jobTitle = 'Acheteur'
        expect(form.canAddContact.value).toBe(false)
        form.addContact()
        expect(form.contacts.value).toHaveLength(1)

        // Un nom (ou prénom) débloque l'ajout.
        if (first) first.lastName = 'Doe'
        expect(form.canAddContact.value).toBe(true)
        form.addContact()
        expect(form.contacts.value).toHaveLength(2)
    })

    it('submitContacts : POST des nouveaux contacts (phones tableau), capture id, finalise', async () => {
        mockPost.mockResolvedValueOnce({ id: 55 })
        const form = createdForm()
        const c = form.contacts.value[0]
        if (c) { c.firstName = 'Jean'; c.phonePrimary = '0102030405' }

        const ok = await form.submitContacts(vi.fn())

        expect(ok).toBe(true)
        const [url, body, opts] = mockPost.mock.calls[0] ?? []
        expect(url).toBe('/carriers/7/contacts')
        expect(body).toMatchObject({ firstName: 'Jean', phones: ['0102030405'] })
        expect(opts).toMatchObject({ toast: false, headers: { Accept: 'application/ld+json' } })
        expect(form.contacts.value[0]?.id).toBe(55)
        expect(form.isValidated('contacts')).toBe(true)
    })

    it('submitContacts : PATCH des contacts existants sur /carrier_contacts/{id}', async () => {
        mockPatch.mockResolvedValueOnce({})
        const form = createdForm()
        const c = form.contacts.value[0]
        if (c) { c.id = 55; c.lastName = 'Doe' }

        await form.submitContacts(vi.fn())

        expect(mockPost).not.toHaveBeenCalled()
        expect(mockPatch).toHaveBeenCalledWith('/carrier_contacts/55', expect.objectContaining({ lastName: 'Doe' }), { toast: false })
    })

    it('RG-4.08 : onglet vide → soumet l\'amorce pour déclencher la 422 inline', async () => {
        mockPost.mockRejectedValueOnce({
            response: {
                status: 422,
                _data: { violations: [{ propertyPath: 'firstName', message: 'Au moins un champ du contact est obligatoire.' }] },
            },
        })
        const form = createdForm()

        const ok = await form.submitContacts(vi.fn())

        expect(ok).toBe(false)
        expect(mockPost).toHaveBeenCalledTimes(1)
        expect(form.contactErrors.value[0]?.firstName).toBe('Au moins un champ du contact est obligatoire.')
        expect(form.isValidated('contacts')).toBe(false)
    })

    it('removeContact : DELETE /carrier_contacts/{id} puis retrait du bloc', async () => {
        mockDelete.mockResolvedValueOnce({})
        const form = createdForm()
        const c = form.contacts.value[0]
        if (c) { c.id = 90; c.lastName = 'Doe' }
        form.addContact()
        const c2 = form.contacts.value[1]
        if (c2) c2.firstName = 'Jean'

        await form.removeContact(0)

        expect(mockDelete).toHaveBeenCalledWith('/carrier_contacts/90', {}, { toast: false })
        expect(form.contacts.value).toHaveLength(1)
    })
})

describe('carrierPrice (util) — bascule CLIENT/FOURNISSEUR + champs requis par branche', () => {
    const CLIENT = '/api/clients/3'
    const CLIENT_ADDR = '/api/client_addresses/8'
    const SUPPLIER = '/api/suppliers/5'
    const SUPPLIER_ADDR = '/api/supplier_addresses/9'
    const SITE = '/api/sites/1'

    it('buildCarrierPricePayload CLIENT : branche client envoyée, branche fournisseur à null', () => {
        const body = buildCarrierPricePayload({
            ...emptyCarrierPrice(),
            direction: 'CLIENT',
            clientIri: CLIENT,
            clientDeliveryAddressIri: CLIENT_ADDR,
            departureSiteIri: SITE,
            containerType: 'BENNE',
            pricingUnit: 'FORFAIT',
            price: '120.00',
            priceState: 'EN_COURS',
        })
        expect(body).toMatchObject({
            direction: 'CLIENT',
            client: CLIENT,
            clientDeliveryAddress: CLIENT_ADDR,
            departureSite: SITE,
            supplier: null,
            supplierSupplyAddress: null,
            deliverySite: null,
            containerType: 'BENNE',
            pricingUnit: 'FORFAIT',
            price: '120.00',
            priceState: 'EN_COURS',
        })
    })

    it('buildCarrierPricePayload FOURNISSEUR : branche fournisseur envoyée, branche client à null', () => {
        const body = buildCarrierPricePayload({
            ...emptyCarrierPrice(),
            direction: 'FOURNISSEUR',
            supplierIri: SUPPLIER,
            supplierSupplyAddressIri: SUPPLIER_ADDR,
            deliverySiteIri: SITE,
            containerType: 'FOND_MOUVANT',
            pricingUnit: 'TONNE',
            price: '45',
            priceState: 'VALIDE',
        })
        expect(body).toMatchObject({
            direction: 'FOURNISSEUR',
            supplier: SUPPLIER,
            supplierSupplyAddress: SUPPLIER_ADDR,
            deliverySite: SITE,
            client: null,
            clientDeliveryAddress: null,
            departureSite: null,
        })
    })

    it('isCarrierPriceValid : faux si branche incomplète, vrai si branche complète + communs', () => {
        const base = { ...emptyCarrierPrice(), containerType: 'BENNE', pricingUnit: 'FORFAIT', price: '10', priceState: 'EN_COURS' }
        // Direction non choisie → invalide.
        expect(isCarrierPriceValid({ ...base, direction: null })).toBe(false)
        // Sens CLIENT par défaut mais branche incomplète → invalide.
        expect(isCarrierPriceValid(base)).toBe(false)
        // CLIENT sans adresse/site → invalide.
        expect(isCarrierPriceValid({ ...base, direction: 'CLIENT', clientIri: CLIENT })).toBe(false)
        // CLIENT complet → valide.
        expect(isCarrierPriceValid({ ...base, direction: 'CLIENT', clientIri: CLIENT, clientDeliveryAddressIri: CLIENT_ADDR, departureSiteIri: SITE })).toBe(true)
        // FOURNISSEUR complet → valide.
        expect(isCarrierPriceValid({ ...base, direction: 'FOURNISSEUR', supplierIri: SUPPLIER, supplierSupplyAddressIri: SUPPLIER_ADDR, deliverySiteIri: SITE })).toBe(true)
        // Prix manquant → invalide même branche complète.
        expect(isCarrierPriceValid({ ...emptyCarrierPrice(), direction: 'CLIENT', clientIri: CLIENT, clientDeliveryAddressIri: CLIENT_ADDR, departureSiteIri: SITE, containerType: 'BENNE', pricingUnit: 'FORFAIT', priceState: 'EN_COURS' })).toBe(false)
    })
})

describe('useCarrierForm — onglet Prix (ERP-169)', () => {
    beforeEach(() => {
        mockPost.mockReset()
        mockPatch.mockReset()
        mockDelete.mockReset()
    })

    function createdForm() {
        const form = useCarrierForm()
        form.carrierId.value = 7
        return form
    }

    it('démarre avec un bloc CLIENT par défaut ; « + Nouveau prix » bloqué tant qu\'il est incomplet', () => {
        const form = createdForm()
        // Un bloc présent d'office, sens CLIENT pré-sélectionné.
        expect(form.prices.value).toHaveLength(1)
        expect(form.prices.value[0]?.direction).toBe('CLIENT')
        // Bloc incomplet → on ne peut pas en ajouter un autre.
        expect(form.canAddPrice.value).toBe(false)
        form.addPrice()
        expect(form.prices.value).toHaveLength(1)

        // Une fois le bloc complété, l'ajout est autorisé.
        const p = form.prices.value[0]
        if (p) {
            p.clientIri = '/api/clients/3'
            p.clientDeliveryAddressIri = '/api/client_addresses/8'
            p.departureSiteIri = '/api/sites/1'
            p.price = '120'
            p.priceState = 'EN_COURS'
        }
        expect(form.canAddPrice.value).toBe(true)
        form.addPrice()
        expect(form.prices.value).toHaveLength(2)
    })

    it('submitPrices : POST des nouveaux prix (branche CLIENT), capture l\'id, finalise', async () => {
        mockPost.mockResolvedValueOnce({ id: 50 })
        const form = createdForm()
        form.addPrice()
        const p = form.prices.value[0]
        if (p) {
            p.direction = 'CLIENT'
            p.clientIri = '/api/clients/3'
            p.clientDeliveryAddressIri = '/api/client_addresses/8'
            p.departureSiteIri = '/api/sites/1'
            p.containerType = 'BENNE'
            p.pricingUnit = 'FORFAIT'
            p.price = '120'
            p.priceState = 'EN_COURS'
        }

        const ok = await form.submitPrices(vi.fn())

        expect(ok).toBe(true)
        const [url, body, opts] = mockPost.mock.calls[0] ?? []
        expect(url).toBe('/carriers/7/prices')
        expect(body).toMatchObject({ direction: 'CLIENT', client: '/api/clients/3', supplier: null })
        expect(opts).toMatchObject({ toast: false, headers: { Accept: 'application/ld+json' } })
        expect(form.prices.value[0]?.id).toBe(50)
        expect(form.isValidated('prices')).toBe(true)
    })

    it('submitPrices : PATCH des prix existants sur /carrier_prices/{id}', async () => {
        mockPatch.mockResolvedValueOnce({})
        const form = createdForm()
        const p = form.prices.value[0]
        if (p) {
            p.id = 50
            p.direction = 'FOURNISSEUR'
            p.supplierIri = '/api/suppliers/5'
            p.supplierSupplyAddressIri = '/api/supplier_addresses/9'
            p.deliverySiteIri = '/api/sites/1'
            p.price = '10'
            p.priceState = 'VALIDE'
        }

        await form.submitPrices(vi.fn())

        expect(mockPost).not.toHaveBeenCalled()
        expect(mockPatch).toHaveBeenCalledWith('/carrier_prices/50', expect.objectContaining({ direction: 'FOURNISSEUR' }), { toast: false })
    })

    it('front : bloc prix incomplet → erreurs inline sous chaque champ requis, pas d\'appel back', async () => {
        const form = createdForm()
        // Bloc CLIENT par défaut, rien d'autre rempli.
        const ok = await form.submitPrices(vi.fn())

        expect(ok).toBe(false)
        expect(mockPost).not.toHaveBeenCalled()
        const errs = form.priceErrors.value[0]
        expect(errs?.client).toBeTruthy()
        expect(errs?.clientDeliveryAddress).toBeTruthy()
        expect(errs?.departureSite).toBeTruthy()
        expect(errs?.price).toBeTruthy()
        expect(errs?.priceState).toBeTruthy()
    })

    it('submitPrices : mappe les 422 back par ligne (appartenance adresse) et ne finalise pas', async () => {
        mockPost.mockRejectedValueOnce({
            response: { status: 422, _data: { violations: [{ propertyPath: 'clientDeliveryAddress', message: 'L\'adresse de livraison doit appartenir au client selectionne.' }] } },
        })
        const form = createdForm()
        // Tous les champs requis remplis (le pré-check front passe) ; le back 422 sur
        // une RG qu'il est seul à connaître (appartenance de l'adresse au client).
        const p = form.prices.value[0]
        if (p) {
            p.direction = 'CLIENT'
            p.clientIri = '/api/clients/3'
            p.clientDeliveryAddressIri = '/api/client_addresses/8'
            p.departureSiteIri = '/api/sites/1'
            p.price = '10'
            p.priceState = 'EN_COURS'
        }

        const ok = await form.submitPrices(vi.fn())

        expect(ok).toBe(false)
        expect(form.priceErrors.value[0]?.clientDeliveryAddress).toBe('L\'adresse de livraison doit appartenir au client selectionne.')
        expect(form.isValidated('prices')).toBe(false)
    })

    it('removePrice : DELETE /carrier_prices/{id} puis retrait du bloc', async () => {
        mockDelete.mockResolvedValueOnce({})
        const form = createdForm()
        form.addPrice()
        const p = form.prices.value[0]
        if (p) { p.id = 77; p.direction = 'CLIENT'; p.clientIri = '/api/clients/3'; p.clientDeliveryAddressIri = '/api/client_addresses/8'; p.departureSiteIri = '/api/sites/1'; p.containerType = 'BENNE'; p.pricingUnit = 'FORFAIT'; p.price = '10'; p.priceState = 'EN_COURS' }
        form.addPrice()

        await form.removePrice(0)

        expect(mockDelete).toHaveBeenCalledWith('/carrier_prices/77', {}, { toast: false })
        expect(form.prices.value).toHaveLength(1)
    })
})

describe('useCarrierForm — édition (ERP-170)', () => {
    beforeEach(() => {
        mockPost.mockReset()
        mockPatch.mockReset()
    })

    it('prefillFrom : peuple carrierId + principal + sous-collections, passe en editMode', () => {
        const form = useCarrierForm()
        form.prefillFrom({
            '@id': '/api/carriers/7',
            id: 7,
            name: 'TRANSPORTS ACME',
            certificationType: 'GMP_PLUS',
            addresses: [{ '@id': '/api/carrier_addresses/3', id: 3, city: 'Poitiers' }],
            contacts: [{ '@id': '/api/carrier_contacts/9', id: 9, lastName: 'Doe', phonePrimary: '0102030405' }],
            prices: [{ '@id': '/api/carrier_prices/5', id: 5, direction: 'CLIENT', client: { '@id': '/api/clients/3' }, containerType: 'BENNE', pricingUnit: 'FORFAIT', price: '120', priceState: 'EN_COURS' }],
        })

        expect(form.carrierId.value).toBe(7)
        expect(form.editMode.value).toBe(true)
        expect(form.main.name).toBe('TRANSPORTS ACME')
        expect(form.main.certificationType).toBe('GMP_PLUS')
        expect(form.addresses.value).toHaveLength(1)
        expect(form.addresses.value[0]?.id).toBe(3)
        expect(form.contacts.value[0]?.id).toBe(9)
        expect(form.prices.value[0]?.clientIri).toBe('/api/clients/3')
    })

    it('updateMain : PATCH /carriers/{id} (pas de POST), réaffiche le nom normalisé', async () => {
        mockPatch.mockResolvedValueOnce({ id: 7, name: 'TRANSPORTS ACME', certificationType: 'GMP_PLUS' })
        const form = useCarrierForm()
        form.prefillFrom({ '@id': '/api/carriers/7', id: 7, name: 'Transports Acme', certificationType: 'GMP_PLUS' })

        const ok = await form.updateMain()

        expect(ok).toBe(true)
        expect(mockPost).not.toHaveBeenCalled()
        expect(mockPatch).toHaveBeenCalledWith(
            '/carriers/7',
            expect.objectContaining({ name: 'Transports Acme', certificationType: 'GMP_PLUS' }),
            { toast: false },
        )
        expect(form.main.name).toBe('TRANSPORTS ACME')
    })
})
