import { describe, expect, it } from 'vitest'
import {
    buildAccountingPayload,
    buildAddressPayload,
    buildContactPayload,
    buildInformationPayload,
    buildMainPayload,
    buildRibPayload,
    mapAccountingFormDraft,
    mapInformationDraft,
    mapMainDraft,
    resolveTabEditability,
    type AccountingFormDraft,
    type InformationFormDraft,
    type MainFormDraft,
} from '../clientEdit'
import type { ClientDetail } from '../clientConsultation'
import type { AddressFormDraft, ContactFormDraft, RibFormDraft } from '~/modules/commercial/types/clientForm'

// ── Fabriques de brouillons (valeurs distinctes pour reperer les fuites) ─────

function mainDraft(overrides: Partial<MainFormDraft> = {}): MainFormDraft {
    return {
        companyName: 'ACME',
        categoryIris: ['/api/categories/1'],
        relationType: null,
        distributorIri: null,
        brokerIri: null,
        triageService: false,
        ...overrides,
    }
}

function informationDraft(overrides: Partial<InformationFormDraft> = {}): InformationFormDraft {
    return {
        description: 'desc',
        competitors: 'concurrents',
        foundedAt: '2010-05-01',
        foundedAtRaw: '',
        employeesCount: '42',
        revenueAmount: '1000000',
        profitAmount: '50000',
        directorName: 'PDG',
        ...overrides,
    }
}

function accountingDraft(overrides: Partial<AccountingFormDraft> = {}): AccountingFormDraft {
    return {
        siren: '123456789',
        accountNumber: 'C-001',
        nTva: 'FR123',
        tvaModeIri: '/api/tva_modes/1',
        paymentDelayIri: '/api/payment_delays/1',
        paymentTypeIri: '/api/payment_types/1',
        bankIri: '/api/banks/1',
        ...overrides,
    }
}

// Champs de chaque groupe de serialisation (miroir back ClientProcessor).
// Le contact inline (nom/prenom/telephones/email) ne fait plus partie du groupe
// main : les coordonnees vivent desormais sur la sous-ressource ClientContact.
const MAIN_KEYS = [
    // relationType : champ transitoire envoye au back pour la validation croisee
    // « relation choisie => FK obligatoire » (RG-1.03 bis, ERP-119).
    'companyName', 'categories', 'relationType', 'distributor', 'broker', 'triageService',
]
const INFORMATION_KEYS = [
    'description', 'competitors', 'foundedAt', 'employeesCount',
    'revenueAmount', 'profitAmount', 'directorName',
]
const ACCOUNTING_KEYS = ['siren', 'accountNumber', 'tvaMode', 'nTva', 'paymentDelay', 'paymentType', 'bank']

describe('buildMainPayload — scoping strict groupe client:write:main', () => {
    it('n\'expose QUE les champs du groupe main (aucune fuite information/accounting)', () => {
        expect(Object.keys(buildMainPayload(mainDraft())).sort()).toEqual([...MAIN_KEYS].sort())
    })

    it('relation distributeur : renseigne distributor, force broker a null (RG-1.03)', () => {
        const payload = buildMainPayload(mainDraft({
            relationType: 'distributeur',
            distributorIri: '/api/clients/9',
            brokerIri: '/api/clients/7',
        }))
        expect(payload.distributor).toBe('/api/clients/9')
        expect(payload.broker).toBeNull()
    })

    it('relation courtier : renseigne broker, force distributor a null (RG-1.03)', () => {
        const payload = buildMainPayload(mainDraft({
            relationType: 'courtier',
            distributorIri: '/api/clients/9',
            brokerIri: '/api/clients/7',
        }))
        expect(payload.broker).toBe('/api/clients/7')
        expect(payload.distributor).toBeNull()
    })

    it('sans relation : distributor et broker a null', () => {
        const payload = buildMainPayload(mainDraft({ relationType: null }))
        expect(payload.distributor).toBeNull()
        expect(payload.broker).toBeNull()
    })

    it('transmet relationType au back pour la validation croisee (RG-1.03 bis)', () => {
        expect(buildMainPayload(mainDraft({ relationType: 'distributeur' })).relationType).toBe('distributeur')
        expect(buildMainPayload(mainDraft({ relationType: 'courtier' })).relationType).toBe('courtier')
        expect(buildMainPayload(mainDraft({ relationType: null })).relationType).toBeNull()
    })

    // ERP-119 : companyName est requis ET adosse a une colonne NON-nullable. Si le
    // champ est vide, on OMET la cle (au lieu d'envoyer null) pour que le back
    // renvoie une 422 NotBlank (propertyPath companyName) et non un 400 de type.
    it('omet companyName quand il est vide (null) -> 422 NotBlank cote back', () => {
        expect('companyName' in buildMainPayload(mainDraft({ companyName: null }))).toBe(false)
    })

    it('omet companyName quand il est une chaine vide', () => {
        expect('companyName' in buildMainPayload(mainDraft({ companyName: '' }))).toBe(false)
    })

    it('conserve companyName quand il est renseigne', () => {
        expect(buildMainPayload(mainDraft({ companyName: 'ACME' })).companyName).toBe('ACME')
    })
})

describe('buildInformationPayload — scoping strict groupe client:write:information', () => {
    it('n\'expose QUE les champs du groupe information (aucune fuite main/accounting)', () => {
        expect(Object.keys(buildInformationPayload(informationDraft())).sort()).toEqual([...INFORMATION_KEYS].sort())
    })

    it('convertit employeesCount en nombre et vide -> null', () => {
        expect(buildInformationPayload(informationDraft({ employeesCount: '42' })).employeesCount).toBe(42)
        expect(buildInformationPayload(informationDraft({ employeesCount: null })).employeesCount).toBeNull()
        expect(buildInformationPayload(informationDraft({ employeesCount: '' })).employeesCount).toBeNull()
    })

    it('chaines vides normalisees en null', () => {
        const payload = buildInformationPayload(informationDraft({ description: '', directorName: '' }))
        expect(payload.description).toBeNull()
        expect(payload.directorName).toBeNull()
    })

    it('envoie la saisie invalide (foundedAtRaw) en priorite -> le back tranchera (422)', () => {
        // Saisie malformee : on transmet le texte brut tel quel pour declencher la
        // 422 back sur foundedAt (validation autoritaire du format, MUI-44).
        expect(buildInformationPayload(informationDraft({ foundedAt: null, foundedAtRaw: '32/13/2026' })).foundedAt)
            .toBe('32/13/2026')
        // Saisie valide : foundedAtRaw vide -> on envoie la date ISO.
        expect(buildInformationPayload(informationDraft({ foundedAt: '2010-05-01', foundedAtRaw: '' })).foundedAt)
            .toBe('2010-05-01')
    })
})

describe('buildAccountingPayload — scoping strict groupe client:write:accounting', () => {
    it('n\'expose QUE les champs du groupe accounting (aucune fuite main/information)', () => {
        expect(Object.keys(buildAccountingPayload(accountingDraft(), true)).sort()).toEqual([...ACCOUNTING_KEYS].sort())
    })

    it('banque conservee si requise (Virement), forcee a null sinon (RG-1.12)', () => {
        expect(buildAccountingPayload(accountingDraft(), true).bank).toBe('/api/banks/1')
        expect(buildAccountingPayload(accountingDraft(), false).bank).toBeNull()
    })
})

describe('buildContactPayload / buildAddressPayload / buildRibPayload', () => {
    it('contact : telephone secondaire ignore si non revele', () => {
        const contact: ContactFormDraft = {
            id: 5, iri: '/api/client_contacts/5', firstName: 'A', lastName: 'B',
            jobTitle: null, phonePrimary: '0549112233', phoneSecondary: '0600000000',
            email: null, hasSecondaryPhone: false,
        }
        expect(buildContactPayload(contact).phoneSecondary).toBeNull()
    })

    it('adresse : email facturation conserve uniquement si requis (RG-1.11)', () => {
        const address: AddressFormDraft = {
            id: 3, isProspect: false, isDelivery: false, isBilling: true, isBroker: false, isDistributor: false, country: 'France',
            postalCode: '86100', city: 'Châtellerault', street: '1 rue X', streetComplement: null,
            categoryIris: ['/api/categories/2'], siteIris: ['/api/sites/1'], contactIris: [],
            billingEmail: 'facturation@acme.fr', billingEmailSecondary: 'compta@acme.fr', hasSecondaryBillingEmail: true,
        }
        expect(buildAddressPayload(address, true).billingEmail).toBe('facturation@acme.fr')
        expect(buildAddressPayload(address, false).billingEmail).toBeNull()
        // 2e email : transmis si facturation + revele, sinon null (ERP-119).
        expect(buildAddressPayload(address, true).billingEmailSecondary).toBe('compta@acme.fr')
        expect(buildAddressPayload(address, false).billingEmailSecondary).toBeNull()
        expect(buildAddressPayload({ ...address, hasSecondaryBillingEmail: false }, true).billingEmailSecondary).toBeNull()
    })

    it('rib : label / bic / iban transmis tels quels', () => {
        const rib: RibFormDraft = { id: 1, label: 'Compte principal', bic: 'BNPAFRPP', iban: 'FR76...' }
        expect(buildRibPayload(rib)).toEqual({ label: 'Compte principal', bic: 'BNPAFRPP', iban: 'FR76...' })
    })

    // ERP-119 : un RIB partiel (IBAN seul) doit omettre label/bic vides pour
    // declencher la 422 NotBlank par champ, pas un 400 de type a la deserialisation.
    it('rib partiel : omet label / bic vides, conserve iban', () => {
        const rib: RibFormDraft = { id: null, label: null, bic: null, iban: 'FR7612345' }
        const payload = buildRibPayload(rib)
        expect('label' in payload).toBe(false)
        expect('bic' in payload).toBe(false)
        expect(payload.iban).toBe('FR7612345')
    })

    // ERP-119 : une adresse partielle omet postalCode/city/street vides (NotBlank).
    it('adresse partielle : omet postalCode / city / street vides', () => {
        const address: AddressFormDraft = {
            id: null, isProspect: false, isDelivery: true, isBilling: false, isBroker: false, isDistributor: false, country: 'France',
            postalCode: null, city: '', street: null, streetComplement: null,
            categoryIris: ['/api/categories/2'], siteIris: ['/api/sites/1'], contactIris: [],
            billingEmail: null, billingEmailSecondary: null, hasSecondaryBillingEmail: false,
        }
        const payload = buildAddressPayload(address, false)
        expect('postalCode' in payload).toBe(false)
        expect('city' in payload).toBe(false)
        expect('street' in payload).toBe(false)
        // Les champs non requis / booleens restent presents.
        expect(payload.isDelivery).toBe(true)
        expect(payload.sites).toEqual(['/api/sites/1'])
    })
})

// Bug edition : en PATCH (merge), une cle de champ requis OMISE laisse la valeur
// serveur inchangee -> faux 200 quand l'utilisateur vide le champ. En `forUpdate`,
// on envoie `''` (chaine valide, pas de 400 de type) -> NotBlank 422 inline.
describe('forUpdate (EDITION/PATCH) : champ requis vide -> `\'\'` au lieu d\'etre omis', () => {
    it('buildMainPayload : companyName vide envoye en `\'\'`', () => {
        const payload = buildMainPayload(mainDraft({ companyName: '' }), { forUpdate: true })
        expect('companyName' in payload).toBe(true)
        expect(payload.companyName).toBe('')
    })

    it('buildAddressPayload : postalCode / city / street vides envoyes en `\'\'`', () => {
        const address: AddressFormDraft = {
            id: 7, isProspect: false, isDelivery: true, isBilling: false, isBroker: false, isDistributor: false, country: 'France',
            postalCode: '', city: null, street: '1 rue X', streetComplement: null,
            categoryIris: ['/api/categories/2'], siteIris: ['/api/sites/1'], contactIris: [],
            billingEmail: null, billingEmailSecondary: null, hasSecondaryBillingEmail: false,
        }
        const payload = buildAddressPayload(address, false, { forUpdate: true })
        expect(payload.postalCode).toBe('')
        expect(payload.city).toBe('')
        // Un champ requis renseigne reste tel quel.
        expect(payload.street).toBe('1 rue X')
    })

    it('buildRibPayload : label / bic vides envoyes en `\'\'`, iban conserve', () => {
        const payload = buildRibPayload({ id: 4, label: '', bic: null, iban: 'FR7612345' }, { forUpdate: true })
        expect(payload.label).toBe('')
        expect(payload.bic).toBe('')
        expect(payload.iban).toBe('FR7612345')
    })
})

describe('mapMainDraft — pre-remplissage bloc principal', () => {
    it('resout la relation et extrait les IRI (sans contact inline)', () => {
        const client = {
            '@id': '/api/clients/1', id: 1,
            companyName: 'ACME', triageService: true,
            categories: [{ '@id': '/api/categories/1', code: 'SECTEUR' }],
            distributor: { '@id': '/api/clients/9', companyName: 'DISTRIB' },
        } as ClientDetail

        const draft = mapMainDraft(client)
        expect(draft.companyName).toBe('ACME')
        expect(draft.categoryIris).toEqual(['/api/categories/1'])
        expect(draft.relationType).toBe('distributeur')
        expect(draft.distributorIri).toBe('/api/clients/9')
        expect(draft.brokerIri).toBeNull()
        expect(draft.triageService).toBe(true)
    })

    it('gere les cles omises (skip_null_values) sans planter', () => {
        const draft = mapMainDraft({ '@id': '/api/clients/2', id: 2 } as ClientDetail)
        expect(draft.companyName).toBeNull()
        expect(draft.categoryIris).toEqual([])
        expect(draft.relationType).toBeNull()
        expect(draft.triageService).toBe(false)
    })
})

describe('mapInformationDraft — pre-remplissage onglet Information', () => {
    it('tronque foundedAt en YYYY-MM-DD et stringifie employeesCount', () => {
        const draft = mapInformationDraft({
            '@id': '/api/clients/1', id: 1,
            foundedAt: '2010-05-01T00:00:00+00:00', employeesCount: 42, revenueAmount: '1000000',
        } as ClientDetail)
        expect(draft.foundedAt).toBe('2010-05-01')
        expect(draft.employeesCount).toBe('42')
        expect(draft.revenueAmount).toBe('1000000')
    })

    it('cles omises -> null', () => {
        const draft = mapInformationDraft({ '@id': '/api/clients/1', id: 1 } as ClientDetail)
        expect(draft.foundedAt).toBeNull()
        expect(draft.employeesCount).toBeNull()
        expect(draft.description).toBeNull()
    })
})

describe('mapAccountingFormDraft — pre-remplissage onglet Comptabilite', () => {
    it('extrait les scalaires et les IRI des referentiels embarques', () => {
        const draft = mapAccountingFormDraft({
            '@id': '/api/clients/1', id: 1,
            siren: '123456789', accountNumber: 'C-001', nTva: 'FR123',
            tvaMode: { '@id': '/api/tva_modes/2', label: 'Normal' },
            paymentType: '/api/payment_types/3',
        } as ClientDetail)
        expect(draft.siren).toBe('123456789')
        expect(draft.tvaModeIri).toBe('/api/tva_modes/2')
        expect(draft.paymentTypeIri).toBe('/api/payment_types/3')
        expect(draft.bankIri).toBeNull()
    })
})

describe('resolveTabEditability — gating par role (matrice § 2.7)', () => {
    it('Admin : tout editable', () => {
        expect(resolveTabEditability({ canManage: true, canAccountingView: true, canAccountingManage: true }))
            .toEqual({ businessEditable: true, accountingVisible: true, accountingEditable: true })
    })

    it('Bureau / Commerciale (manage seul) : metier editable, Comptabilite masquee', () => {
        expect(resolveTabEditability({ canManage: true, canAccountingView: false, canAccountingManage: false }))
            .toEqual({ businessEditable: true, accountingVisible: false, accountingEditable: false })
    })

    it('Compta (accounting seul) : metier readonly, Comptabilite editable', () => {
        expect(resolveTabEditability({ canManage: false, canAccountingView: true, canAccountingManage: true }))
            .toEqual({ businessEditable: false, accountingVisible: true, accountingEditable: true })
    })

    it('Sans permission d\'edition : rien d\'editable', () => {
        expect(resolveTabEditability({ canManage: false, canAccountingView: false, canAccountingManage: false }))
            .toEqual({ businessEditable: false, accountingVisible: false, accountingEditable: false })
    })
})
