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

const mockApiGet = vi.hoisted(() => vi.fn())
vi.stubGlobal('useApi', () => ({ get: mockApiGet }))

/**
 * Tests du composable `usePaginatedList`.
 *
 * Couvre les invariants critiques :
 *  - parse Hydra (member / totalItems)
 *  - navigation page (goToPage / next / prev / bornes)
 *  - changement items/page → retour page 1
 *  - mutation filtres / tri → retour page 1
 *  - cas limite : page courante hors borne apres filtre → derniere page valide
 *  - liste vide / page unique
 *  - reset → defaults
 *  - swallow d'erreur reseau (la promesse `fetch` ne reject jamais)
 *  - header `Accept: application/ld+json` toujours envoye (besoin du
 *    paginator Hydra cote API Platform 4).
 */
describe('usePaginatedList', () => {
    beforeEach(() => {
        mockApiGet.mockReset()
    })

    function mockResponse(member: unknown[], totalItems: number): void {
        mockApiGet.mockResolvedValueOnce({ member, totalItems })
    }

    it('fetch initial : page=1, itemsPerPage par defaut, parse Hydra', async () => {
        mockResponse([{ id: 1 }, { id: 2 }], 42)
        const list = usePaginatedList<{ id: number }>({ url: '/sites' })

        await list.fetch()

        expect(mockApiGet).toHaveBeenCalledTimes(1)
        const [url, query, opts] = mockApiGet.mock.calls[0]
        expect(url).toBe('/sites')
        expect(query).toMatchObject({ page: 1, itemsPerPage: 10 })
        expect(opts).toMatchObject({
            toast: false,
            headers: { Accept: 'application/ld+json' },
        })
        expect(list.items.value).toEqual([{ id: 1 }, { id: 2 }])
        expect(list.totalItems.value).toBe(42)
        expect(list.totalPages.value).toBe(5)
        expect(list.isEmpty.value).toBe(false)
        expect(list.isSinglePage.value).toBe(false)
    })

    it('itemsPerPage personnalise est respecte au premier fetch', async () => {
        mockResponse([], 0)
        const list = usePaginatedList({ url: '/users', defaultItemsPerPage: 25 })
        await list.fetch()
        expect(mockApiGet.mock.calls[0][1]).toMatchObject({ itemsPerPage: 25 })
        expect(list.itemsPerPage.value).toBe(25)
    })

    it('goToPage(N) declenche un nouvel appel avec page=N', async () => {
        mockResponse([{ id: 1 }], 30) // page 1
        const list = usePaginatedList<{ id: number }>({ url: '/users' })
        await list.fetch()

        mockResponse([{ id: 2 }], 30) // page 2
        await list.goToPage(2)

        expect(mockApiGet).toHaveBeenCalledTimes(2)
        expect(mockApiGet.mock.calls[1][1]).toMatchObject({ page: 2, itemsPerPage: 10 })
        expect(list.currentPage.value).toBe(2)
    })

    it('goToPage hors borne sup. est clampe a totalPages', async () => {
        mockResponse([], 30) // totalPages = 3
        const list = usePaginatedList({ url: '/roles' })
        await list.fetch()

        mockResponse([], 30)
        await list.goToPage(999)

        expect(list.currentPage.value).toBe(3)
    })

    it('goToPage hors borne inf. est clampe a 1 (no-op si deja en 1)', async () => {
        mockResponse([], 30)
        const list = usePaginatedList({ url: '/roles' })
        await list.fetch()

        mockApiGet.mockClear()
        await list.goToPage(-5)

        // Deja en page 1 -> aucun nouvel appel.
        expect(mockApiGet).toHaveBeenCalledTimes(0)
        expect(list.currentPage.value).toBe(1)
    })

    it('nextPage / prevPage avancent et reculent dans les bornes', async () => {
        mockResponse([], 30) // page 1, totalPages 3
        const list = usePaginatedList({ url: '/roles' })
        await list.fetch()

        mockResponse([], 30)
        await list.nextPage()
        expect(list.currentPage.value).toBe(2)

        mockResponse([], 30)
        await list.nextPage()
        expect(list.currentPage.value).toBe(3)

        // En derniere page -> no-op
        mockApiGet.mockClear()
        await list.nextPage()
        expect(mockApiGet).toHaveBeenCalledTimes(0)
        expect(list.currentPage.value).toBe(3)

        mockResponse([], 30)
        await list.prevPage()
        expect(list.currentPage.value).toBe(2)
    })

    it('setItemsPerPage revient en page 1 et refetch', async () => {
        mockResponse([], 100)
        const list = usePaginatedList({ url: '/users' })
        await list.fetch()
        // place-toi page 3
        mockResponse([], 100)
        await list.goToPage(3)
        expect(list.currentPage.value).toBe(3)

        mockResponse([], 100)
        await list.setItemsPerPage(25)

        expect(list.currentPage.value).toBe(1)
        expect(list.itemsPerPage.value).toBe(25)
        expect(mockApiGet.mock.calls.at(-1)?.[1]).toMatchObject({ page: 1, itemsPerPage: 25 })
    })

    it('setItemsPerPage no-op si meme valeur', async () => {
        mockResponse([], 10)
        const list = usePaginatedList({ url: '/users' })
        await list.fetch()

        mockApiGet.mockClear()
        await list.setItemsPerPage(10)
        expect(mockApiGet).toHaveBeenCalledTimes(0)
    })

    it('setFilters fusionne et retombe en page 1', async () => {
        mockResponse([], 100)
        const list = usePaginatedList<unknown, { name?: string; active?: boolean }>({
            url: '/users',
            defaultFilters: { active: true },
        })
        await list.fetch()
        mockResponse([], 100)
        await list.goToPage(2)

        mockResponse([], 100)
        await list.setFilters({ name: 'alice' })

        expect(list.currentPage.value).toBe(1)
        expect(list.filters.value).toEqual({ active: true, name: 'alice' })
        expect(mockApiGet.mock.calls.at(-1)?.[1]).toMatchObject({
            page: 1,
            active: true,
            name: 'alice',
        })
    })

    it('setFilters({ key: undefined }) supprime la cle', async () => {
        mockResponse([], 100)
        const list = usePaginatedList<unknown, { name?: string }>({
            url: '/users',
            defaultFilters: { name: 'alice' },
        })
        await list.fetch()

        mockResponse([], 100)
        await list.setFilters({ name: undefined })

        expect(list.filters.value).toEqual({})
        // Le query envoye ne contient plus `name` (compactQuery elimine
        // aussi les valeurs vides).
        const q = mockApiGet.mock.calls.at(-1)?.[1] as Record<string, unknown>
        expect(q.name).toBeUndefined()
    })

    it('setFilters({ replace: true }) remplace integralement', async () => {
        mockResponse([], 100)
        const list = usePaginatedList<unknown, { a?: string; b?: string }>({
            url: '/users',
            defaultFilters: { a: 'x' },
        })
        await list.fetch()

        mockResponse([], 100)
        await list.setFilters({ b: 'y' }, { replace: true })

        expect(list.filters.value).toEqual({ b: 'y' })
    })

    it('setSort envoie order[field]=direction et reset page', async () => {
        mockResponse([], 100)
        const list = usePaginatedList({ url: '/users' })
        await list.fetch()
        mockResponse([], 100)
        await list.goToPage(2)

        mockResponse([], 100)
        await list.setSort({ field: 'username', direction: 'desc' })

        expect(list.currentPage.value).toBe(1)
        const q = mockApiGet.mock.calls.at(-1)?.[1] as Record<string, unknown>
        expect(q['order[username]']).toBe('desc')
    })

    it('setSort(null) retire le tri', async () => {
        mockResponse([], 100)
        const list = usePaginatedList({
            url: '/users',
            defaultSort: { field: 'name', direction: 'asc' },
        })
        await list.fetch()
        // Le tri initial est applique
        let q = mockApiGet.mock.calls[0][1] as Record<string, unknown>
        expect(q['order[name]']).toBe('asc')

        mockResponse([], 100)
        await list.setSort(null)
        q = mockApiGet.mock.calls.at(-1)?.[1] as Record<string, unknown>
        expect(q['order[name]']).toBeUndefined()
    })

    it('setFilters retombe en page 1 (cas standard, pas le hors-borne)', async () => {
        // setFilters remet toujours page=1 avant de refetcher : ce n'est
        // donc PAS le chemin de retry hors-borne (couvert par le test
        // suivant via un refetch a page constante). On verifie juste le
        // reset de page ici.
        mockResponse([], 50) // 5 pages
        const list = usePaginatedList({ url: '/users' })
        await list.fetch()
        mockResponse([], 50)
        await list.goToPage(5)
        expect(list.currentPage.value).toBe(5)

        mockResponse([{ id: 1 }, { id: 2 }], 12)
        await list.setFilters({ active: true } as never)

        expect(list.currentPage.value).toBe(1)
        expect(mockApiGet.mock.calls.at(-1)?.[1]).toMatchObject({ page: 1 })
    })

    it('declenche le retry sur derniere page si currentPage > totalPages apres fetch', async () => {
        // Scenario : on a fait un fetch (5 pages, page=1). Sans toucher aux
        // filtres mais entre deux fetchs la donnee a change cote serveur,
        // la page courante peut devenir hors borne. On force le scenario
        // en montant manuellement currentPage via goToPage borne, puis en
        // simulant une reponse plus petite.
        mockResponse([], 50) // 5 pages
        const list = usePaginatedList({ url: '/users' })
        await list.fetch()

        mockResponse([], 50)
        await list.goToPage(5)
        expect(list.currentPage.value).toBe(5)

        // Maintenant simule : refetch -> totalItems chute a 12 (2 pages),
        // le composable doit refetcher sur page=2.
        mockApiGet.mockReset()
        mockApiGet
            .mockResolvedValueOnce({ member: [], totalItems: 12 }) // page=5 vide
            .mockResolvedValueOnce({ member: [{ id: 11 }, { id: 12 }], totalItems: 12 }) // page=2

        await list.fetch()

        expect(mockApiGet).toHaveBeenCalledTimes(2)
        expect(mockApiGet.mock.calls[1][1]).toMatchObject({ page: 2 })
        expect(list.currentPage.value).toBe(2)
        expect(list.items.value).toEqual([{ id: 11 }, { id: 12 }])
    })

    it('liste vide : isEmpty true, isSinglePage true', async () => {
        mockResponse([], 0)
        const list = usePaginatedList({ url: '/users' })
        await list.fetch()

        expect(list.totalItems.value).toBe(0)
        expect(list.isEmpty.value).toBe(true)
        expect(list.isSinglePage.value).toBe(true)
        expect(list.totalPages.value).toBe(1)
    })

    it('isEmpty est faux avant le premier fetch (etat indetermine)', () => {
        const list = usePaginatedList({ url: '/users' })
        expect(list.isEmpty.value).toBe(false)
    })

    it('reset revient aux defaults', async () => {
        mockResponse([], 100)
        const list = usePaginatedList<unknown, { a?: string }>({
            url: '/users',
            defaultItemsPerPage: 10,
            defaultFilters: { a: 'x' },
            defaultSort: { field: 'name', direction: 'asc' },
        })
        await list.fetch()

        mockResponse([], 100)
        await list.setItemsPerPage(50)
        mockResponse([], 100)
        await list.setFilters({ a: 'y' })
        mockResponse([], 100)
        await list.setSort({ field: 'id', direction: 'desc' })
        mockResponse([], 100)
        await list.goToPage(2)
        expect(list.currentPage.value).toBe(2)

        mockResponse([], 100)
        await list.reset()

        expect(list.itemsPerPage.value).toBe(10)
        expect(list.filters.value).toEqual({ a: 'x' })
        expect(list.sort.value).toEqual({ field: 'name', direction: 'asc' })
        expect(list.currentPage.value).toBe(1)
    })

    it('swallow l\'erreur reseau : items vides, loading false, fetch ne reject pas', async () => {
        const boom = new Error('boom')
        mockApiGet.mockRejectedValueOnce(boom)
        const list = usePaginatedList({ url: '/users' })

        await expect(list.fetch()).resolves.toBeUndefined()
        expect(list.items.value).toEqual([])
        expect(list.totalItems.value).toBe(0)
        expect(list.loading.value).toBe(false)
        // L'erreur est consideree comme un fetch consume -> isEmpty=true.
        expect(list.isEmpty.value).toBe(true)
        // ... mais `error` est expose pour distinguer « vide » d'« echec ».
        expect(list.error.value).toBe(boom)
    })

    it('error est remis a null des qu\'un fetch ulterieur reussit', async () => {
        mockApiGet.mockRejectedValueOnce(new Error('boom'))
        const list = usePaginatedList({ url: '/users' })
        await list.fetch()
        expect(list.error.value).toBeInstanceOf(Error)

        mockResponse([{ id: 1 }], 1)
        await list.fetch()
        expect(list.error.value).toBeNull()
        expect(list.items.value).toEqual([{ id: 1 }])
    })

    it('ignore une reponse periemee : la derniere requete *demandee* gagne', async () => {
        // Deux fetch concurrents : le 1er resout APRES le 2eme. Sans garde
        // de sequence, la reponse arrivee en dernier (token 1) ecraserait
        // les donnees plus fraiches du token 2. Avec la garde, token 2 fait
        // foi quel que soit l'ordre d'arrivee reseau.
        const list = usePaginatedList<{ id: number }>({ url: '/users' })

        let resolveSlow!: (v: unknown) => void
        const slow = new Promise((r) => { resolveSlow = r })
        // 1er appel : reponse lente (en vol).
        mockApiGet.mockReturnValueOnce(slow)
        // 2eme appel : reponse immediate avec des donnees plus fraiches.
        mockApiGet.mockResolvedValueOnce({ member: [{ id: 2 }], totalItems: 30 })

        const p1 = list.fetch()   // token 1, en vol
        const p2 = list.fetch()   // token 2, resout tout de suite
        await p2
        expect(list.items.value).toEqual([{ id: 2 }])

        // La reponse lente du token 1 arrive enfin : elle doit etre ignoree.
        resolveSlow({ member: [{ id: 1 }], totalItems: 30 })
        await p1
        expect(list.items.value).toEqual([{ id: 2 }])
        // Le spinner reste eteint (la requete recente l'avait deja coupe).
        expect(list.loading.value).toBe(false)
    })

    it('extraQuery est injecte a chaque fetch (ex : includeDeleted)', async () => {
        mockResponse([], 0)
        const list = usePaginatedList({
            url: '/categories',
            extraQuery: { includeDeleted: 'true' },
        })
        await list.fetch()

        expect(mockApiGet.mock.calls[0][1]).toMatchObject({ includeDeleted: 'true' })
    })

    it('valeurs nulles/vides des filtres ne sont pas envoyees', async () => {
        mockResponse([], 0)
        const list = usePaginatedList<unknown, { name?: string; q?: string }>({
            url: '/users',
            defaultFilters: { name: '', q: undefined } as never,
        })
        await list.fetch()

        const q = mockApiGet.mock.calls[0][1] as Record<string, unknown>
        expect(q.name).toBeUndefined()
        expect(q.q).toBeUndefined()
    })

    it('refresh() est un alias de fetch()', async () => {
        mockResponse([{ id: 1 }], 1)
        const list = usePaginatedList<{ id: number }>({ url: '/users' })

        await list.refresh()
        expect(list.items.value).toEqual([{ id: 1 }])
    })
})
