import { describe, it, expect, vi, beforeEach } from 'vitest'
import type { Category, CategoryType } from '~/modules/catalog/types/category'
import { useFormErrors } from '~/shared/composables/useFormErrors'
import { useCategoryForm } from '../useCategoryForm'

// Stubs des auto-imports Nuxt consommes par le composable.
const mockGet = vi.hoisted(() => vi.fn())
const mockPost = vi.hoisted(() => vi.fn())
const mockPatch = vi.hoisted(() => vi.fn())
const mockDelete = vi.hoisted(() => vi.fn())
const mockToastSuccess = vi.hoisted(() => vi.fn())
const mockToastError = vi.hoisted(() => vi.fn())

vi.stubGlobal('useApi', () => ({
    get: mockGet,
    post: mockPost,
    put: vi.fn(),
    patch: mockPatch,
    delete: mockDelete,
}))
vi.stubGlobal('useToast', () => ({
    success: mockToastSuccess,
    error: mockToastError,
}))
// useFormErrors est un auto-import Nuxt : on expose l'implementation reelle
// (elle consomme useToast, deja stubbe ci-dessus) pour tester l'integration.
vi.stubGlobal('useFormErrors', useFormErrors)
// useI18n.t : on renvoie la cle telle quelle (pratique pour asserter dessus).
// Quand le composable passe des params (ex: doublon), on les serialise pour
// pouvoir verifier que l'interpolation a bien recu le bon nom.
vi.stubGlobal('useI18n', () => ({
    t: (key: string, params?: Record<string, unknown>) =>
        params ? `${key}::${JSON.stringify(params)}` : key,
}))

const TYPE_VENTE: CategoryType = { id: 1, code: 'VENTE', label: 'Vente' }
const TYPE_ACHAT: CategoryType = { id: 2, code: 'ACHAT', label: 'Achat' }

const CAT: Category = {
    id: 42,
    name: 'Vis',
    categoryTypes: [TYPE_VENTE],
    deletedAt: null,
    createdAt: '2026-01-01T10:00:00+00:00',
    updatedAt: '2026-01-01T10:00:00+00:00',
    createdBy: null,
    updatedBy: null,
}

describe('useCategoryForm', () => {
    beforeEach(() => {
        mockGet.mockReset()
        mockPost.mockReset()
        mockPatch.mockReset()
        mockDelete.mockReset()
        mockToastSuccess.mockReset()
        mockToastError.mockReset()
    })

    describe('loadFrom', () => {
        it('pre-remplit le formulaire depuis une categorie existante (multi-types)', () => {
            const form = useCategoryForm()

            form.loadFrom({ ...CAT, categoryTypes: [TYPE_VENTE, TYPE_ACHAT] })

            expect(form.name.value).toBe('Vis')
            expect(form.categoryTypeIds.value).toEqual([1, 2])
            expect(form.errors).toEqual({})
        })

        it('vide le formulaire en mode creation (null)', () => {
            const form = useCategoryForm()
            form.name.value = 'old'
            form.categoryTypeIds.value = [99]

            form.loadFrom(null)

            expect(form.name.value).toBe('')
            expect(form.categoryTypeIds.value).toEqual([])
        })

        it('reinitialise le snapshot initial → isDirty=false juste apres', () => {
            const form = useCategoryForm()

            form.loadFrom(CAT)

            expect(form.isDirty.value).toBe(false)
        })
    })

    describe('isDirty', () => {
        it('passe a true des qu une valeur diverge du snapshot initial', () => {
            const form = useCategoryForm()
            form.loadFrom(CAT)
            expect(form.isDirty.value).toBe(false)

            form.name.value = 'Vis modifie'

            expect(form.isDirty.value).toBe(true)
        })

        it('passe a true quand on ajoute un type (selection multi)', () => {
            const form = useCategoryForm()
            form.loadFrom(CAT)
            expect(form.isDirty.value).toBe(false)

            form.categoryTypeIds.value = [1, 2]

            expect(form.isDirty.value).toBe(true)
        })

        it('reste false si la selection est identique dans un autre ordre', () => {
            const form = useCategoryForm()
            form.loadFrom({ ...CAT, categoryTypes: [TYPE_VENTE, TYPE_ACHAT] })

            form.categoryTypeIds.value = [2, 1]

            expect(form.isDirty.value).toBe(false)
        })
    })

    describe('validate', () => {
        it('signale une erreur si name est vide (RG-1.02)', () => {
            const form = useCategoryForm()
            form.name.value = ''
            form.categoryTypeIds.value = [1]

            const ok = form.validate()

            expect(ok).toBe(false)
            expect(form.errors.name).toBe('admin.categories.validation.nameRequired')
        })

        it('signale erreur si name est whitespace-only (trim → vide)', () => {
            const form = useCategoryForm()
            form.name.value = '   '
            form.categoryTypeIds.value = [1]

            const ok = form.validate()

            expect(ok).toBe(false)
            expect(form.errors.name).toBe('admin.categories.validation.nameRequired')
        })

        it('signale erreur si name fait 1 caractere (< 2, RG-1.04)', () => {
            const form = useCategoryForm()
            form.name.value = 'A'
            form.categoryTypeIds.value = [1]

            const ok = form.validate()

            expect(ok).toBe(false)
            expect(form.errors.name).toBe('admin.categories.validation.nameLength')
        })

        it('signale erreur si name fait 121 caracteres (> 120, RG-1.04)', () => {
            const form = useCategoryForm()
            form.name.value = 'A'.repeat(121)
            form.categoryTypeIds.value = [1]

            const ok = form.validate()

            expect(ok).toBe(false)
            expect(form.errors.name).toBe('admin.categories.validation.nameLength')
        })

        it('signale erreur si aucun type selectionne (RG-1.05)', () => {
            const form = useCategoryForm()
            form.name.value = 'Vis'
            form.categoryTypeIds.value = []

            const ok = form.validate()

            expect(ok).toBe(false)
            expect(form.errors.categoryTypes).toBe('admin.categories.validation.typesRequired')
        })

        it('passe quand name et au moins un type sont valides', () => {
            const form = useCategoryForm()
            form.name.value = 'Vis'
            form.categoryTypeIds.value = [1, 2]

            const ok = form.validate()

            expect(ok).toBe(true)
            expect(form.errors).toEqual({})
        })

        it('reinitialise les erreurs avant chaque validation', () => {
            const form = useCategoryForm()
            // Erreur prealable : une validation en echec peuple errors.name.
            form.name.value = ''
            form.categoryTypeIds.value = [1]
            form.validate()
            expect(form.errors.name).toBeTruthy()

            // Seconde validation avec des valeurs valides : errors repart vide.
            form.name.value = 'Vis'
            form.validate()

            expect(form.errors).toEqual({})
        })
    })

    describe('submitCreate', () => {
        it('appelle POST /categories avec body { name trimme, categoryTypes en IRI[] }', async () => {
            mockPost.mockResolvedValueOnce(CAT)
            const form = useCategoryForm()
            form.name.value = '  Vis  '
            form.categoryTypeIds.value = [1, 2]

            const result = await form.submitCreate()

            expect(mockPost).toHaveBeenCalledWith(
                '/categories',
                { name: 'Vis', categoryTypes: ['/api/category_types/1', '/api/category_types/2'] },
                { toast: false },
            )
            expect(result).toEqual(CAT)
        })

        it('ne declenche aucun appel API si la validation client echoue', async () => {
            const form = useCategoryForm()
            form.name.value = ''
            form.categoryTypeIds.value = [1]

            const result = await form.submitCreate()

            expect(mockPost).not.toHaveBeenCalled()
            expect(result).toBeNull()
        })

        it('declenche un toast de succes en cas de creation reussie', async () => {
            mockPost.mockResolvedValueOnce(CAT)
            const form = useCategoryForm()
            form.name.value = 'Vis'
            form.categoryTypeIds.value = [1]

            await form.submitCreate()

            expect(mockToastSuccess).toHaveBeenCalledWith({
                title: 'success.title',
                message: 'admin.categories.toast.created',
            })
        })

        it('mappe un 409 (RG-1.07) sur errors.name + toast erreur avec le nom', async () => {
            mockPost.mockRejectedValueOnce({
                response: { status: 409, _data: {} },
            })
            const form = useCategoryForm()
            form.name.value = 'Vis'
            form.categoryTypeIds.value = [1]

            const result = await form.submitCreate()

            expect(result).toBeNull()
            // La cle est interpolee avec le nom soumis : on retrouve "Vis" dans
            // les params i18n (stub serialise les params).
            expect(form.errors.name).toContain('admin.categories.toast.duplicate')
            expect(form.errors.name).toContain('"name":"Vis"')
            expect(mockToastError).toHaveBeenCalledTimes(1)
            const toastArg = mockToastError.mock.calls[0]?.[0] as { message: string }
            expect(toastArg.message).toContain('Vis')
        })

        it('mappe un 422 violations sur les champs concernes (errors.name)', async () => {
            mockPost.mockRejectedValueOnce({
                response: {
                    status: 422,
                    _data: {
                        violations: [
                            { propertyPath: 'name', message: 'name should not be blank.' },
                        ],
                    },
                },
            })
            const form = useCategoryForm()
            form.name.value = 'Vis'
            form.categoryTypeIds.value = [1]

            const result = await form.submitCreate()

            expect(result).toBeNull()
            expect(form.errors.name).toBe('name should not be blank.')
            // Pas de toast quand on a mappe les violations : l erreur est
            // affichee inline sous le champ concerne.
            expect(mockToastError).not.toHaveBeenCalled()
        })

        it('mappe une violation sur categoryTypes (hydra:violations alternative)', async () => {
            mockPost.mockRejectedValueOnce({
                response: {
                    status: 422,
                    _data: {
                        'hydra:violations': [
                            { propertyPath: 'categoryTypes', message: 'Sélectionnez au moins un type de catégorie.' },
                        ],
                    },
                },
            })
            const form = useCategoryForm()
            form.name.value = 'Vis'
            form.categoryTypeIds.value = [1]

            await form.submitCreate()

            expect(form.errors.categoryTypes).toBe('Sélectionnez au moins un type de catégorie.')
        })

        it('fallback en toast generique si le status n est ni 409 ni 422', async () => {
            mockPost.mockRejectedValueOnce({
                response: { status: 500, _data: { 'hydra:description': 'Boom server' } },
            })
            const form = useCategoryForm()
            form.name.value = 'Vis'
            form.categoryTypeIds.value = [1]

            await form.submitCreate()

            // Pas d'erreur inline par champ : l'erreur transverse part en toast.
            expect(form.errors).toEqual({})
            expect(mockToastError).toHaveBeenCalledWith({
                title: 'errors.title',
                message: 'Boom server',
            })
        })

        it('passe submitting a true pendant la requete et a false apres', async () => {
            let resolveRequest: (v: Category) => void = () => {}
            mockPost.mockImplementationOnce(
                () => new Promise((resolve) => { resolveRequest = resolve }),
            )
            const form = useCategoryForm()
            form.name.value = 'Vis'
            form.categoryTypeIds.value = [1]

            const pending = form.submitCreate()
            expect(form.submitting.value).toBe(true)

            resolveRequest(CAT)
            await pending

            expect(form.submitting.value).toBe(false)
        })
    })

    describe('submitUpdate', () => {
        it('appelle PATCH /categories/{id} avec le payload complet (name + categoryTypes)', async () => {
            mockPatch.mockResolvedValueOnce({ ...CAT, name: 'Vis V2' })
            const form = useCategoryForm()
            form.loadFrom(CAT)
            form.name.value = 'Vis V2' // types inchanges

            await form.submitUpdate(42)

            // Payload complet : meme si seul le name change, on renvoie aussi
            // les categoryTypes (PATCH full payload, cf. drawers simples).
            expect(mockPatch).toHaveBeenCalledWith(
                '/categories/42',
                { name: 'Vis V2', categoryTypes: ['/api/category_types/1'] },
                { toast: false },
            )
        })

        it('envoie categoryTypes en IRI[] quand on ajoute un type', async () => {
            mockPatch.mockResolvedValueOnce({ ...CAT, categoryTypes: [TYPE_VENTE, TYPE_ACHAT] })
            const form = useCategoryForm()
            form.loadFrom(CAT)
            form.categoryTypeIds.value = [1, 2]

            await form.submitUpdate(42)

            expect(mockPatch).toHaveBeenCalledWith(
                '/categories/42',
                { name: CAT.name, categoryTypes: ['/api/category_types/1', '/api/category_types/2'] },
                { toast: false },
            )
        })

        it('envoie un PATCH complet meme sans modification (save a tout moment)', async () => {
            mockPatch.mockResolvedValueOnce(CAT)
            const form = useCategoryForm()
            form.loadFrom(CAT)
            // Aucune modification : le PATCH part quand meme avec le payload complet.

            const result = await form.submitUpdate(42)

            expect(mockPatch).toHaveBeenCalledWith(
                '/categories/42',
                { name: CAT.name, categoryTypes: ['/api/category_types/1'] },
                { toast: false },
            )
            expect(result).toEqual(CAT)
            expect(form.submitting.value).toBe(false)
        })

        it('declenche un toast de succes au PATCH reussi', async () => {
            mockPatch.mockResolvedValueOnce({ ...CAT, name: 'Vis V2' })
            const form = useCategoryForm()
            form.loadFrom(CAT)
            form.name.value = 'Vis V2'

            await form.submitUpdate(42)

            expect(mockToastSuccess).toHaveBeenCalledWith({
                title: 'success.title',
                message: 'admin.categories.toast.updated',
            })
        })

        it('mappe le 409 sur errors.name en mode update aussi', async () => {
            mockPatch.mockRejectedValueOnce({
                response: { status: 409, _data: {} },
            })
            const form = useCategoryForm()
            form.loadFrom(CAT)
            form.name.value = 'Doublon'

            const result = await form.submitUpdate(42)

            expect(result).toBeNull()
            expect(form.errors.name).toContain('admin.categories.toast.duplicate')
            expect(form.errors.name).toContain('"name":"Doublon"')
        })
    })

    describe('submitDelete', () => {
        it('appelle DELETE /categories/{id} et declenche un toast succes', async () => {
            mockDelete.mockResolvedValueOnce(undefined)
            const form = useCategoryForm()

            const ok = await form.submitDelete(42)

            expect(mockDelete).toHaveBeenCalledWith('/categories/42', {}, { toast: false })
            expect(ok).toBe(true)
            expect(mockToastSuccess).toHaveBeenCalledWith({
                title: 'success.title',
                message: 'admin.categories.toast.deleted',
            })
        })

        it('retourne false et toast erreur en cas d echec', async () => {
            mockDelete.mockRejectedValueOnce({
                response: { status: 500, _data: { detail: 'down' } },
            })
            const form = useCategoryForm()

            const ok = await form.submitDelete(42)

            expect(ok).toBe(false)
            expect(mockToastError).toHaveBeenCalled()
        })
    })

    describe('reset', () => {
        it('vide le formulaire et les erreurs', () => {
            const form = useCategoryForm()
            form.loadFrom(CAT)
            form.name.value = ''
            form.validate() // peuple errors.name
            form.submitting.value = true

            form.reset()

            expect(form.name.value).toBe('')
            expect(form.categoryTypeIds.value).toEqual([])
            expect(form.errors).toEqual({})
            expect(form.submitting.value).toBe(false)
        })
    })

    describe('isolation', () => {
        it('deux instances useCategoryForm() ont des states independants', () => {
            const a = useCategoryForm()
            const b = useCategoryForm()

            a.name.value = 'A'
            b.name.value = 'B'

            expect(a.name.value).toBe('A')
            expect(b.name.value).toBe('B')
            // Les refs sont distinctes (pas singleton — chaque drawer son state).
            expect(a.name).not.toBe(b.name)
        })
    })
})
