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

// Mock the entreprise-search module since it's imported by app.js
vi.mock('../../assets/modules/entreprise-search.js', () => ({
    initEntrepriseSearch: vi.fn(),
}))

// Mock the scss import
vi.mock('../../assets/app.scss', () => ({}))

// localStorage mock
const localStorageMock = (() => {
    let store = {}
    return {
        getItem: vi.fn((key) => store[key] ?? null),
        setItem: vi.fn((key, value) => { store[key] = String(value) }),
        removeItem: vi.fn((key) => { delete store[key] }),
        clear: vi.fn(() => { store = {} }),
        get length() { return Object.keys(store).length },
        key: vi.fn((i) => Object.keys(store)[i] ?? null),
    }
})()

Object.defineProperty(globalThis, 'localStorage', { value: localStorageMock, writable: true })

// Track DOMContentLoaded listeners so we can remove them between tests
let domContentLoadedListeners = []
const originalAddEventListener = document.addEventListener.bind(document)
const originalRemoveEventListener = document.removeEventListener.bind(document)

const trackListener = (type, listener) => {
    if (type === 'DOMContentLoaded') domContentLoadedListeners.push(listener)
}

document.addEventListener = (type, listener, options) => {
    trackListener(type, listener)
    return originalAddEventListener(type, listener, options)
}

const cleanupListeners = () => {
    domContentLoadedListeners.forEach(listener => {
        originalRemoveEventListener('DOMContentLoaded', listener)
    })
    domContentLoadedListeners = []
}

const resetMocks = () => {
    document.body.innerHTML = ''
    localStorageMock.clear()
    vi.restoreAllMocks()
    localStorageMock.getItem.mockImplementation(() => null)
}

const loadApp = async () => {
    vi.resetModules()
    vi.doMock('../../assets/modules/entreprise-search.js', () => ({
        initEntrepriseSearch: vi.fn(),
    }))
    vi.doMock('../../assets/app.scss', () => ({}))
    await import('../../assets/app.js')
    document.dispatchEvent(new Event('DOMContentLoaded'))
}

describe('app.js DOMContentLoaded', () => {
    beforeEach(() => {
        cleanupListeners()
        resetMocks()
    })

    describe('Member/Admin checkboxes', () => {
        beforeEach(() => {
            document.body.innerHTML = `
                <input type="checkbox" name="groups[]" value="gp_member" checked>
                <input type="checkbox" name="groups[]" value="superadmin">
                <input type="checkbox" name="groups[]" value="esy-web">
                <input type="checkbox" name="groups[]" value="esy-mail">
            `
        })

        it('unchecks other groups when member is checked', async () => {
            await loadApp()

            const member = document.querySelector('[value="gp_member"]')
            const admin = document.querySelector('[value="superadmin"]')
            const esyWeb = document.querySelector('[value="esy-web"]')

            admin.checked = true
            esyWeb.checked = true

            member.checked = true
            member.dispatchEvent(new Event('change'))

            expect(admin.checked).toBe(false)
            expect(esyWeb.checked).toBe(false)
        })

        it('checks all groups and unchecks member when admin is checked', async () => {
            await loadApp()

            const member = document.querySelector('[value="gp_member"]')
            const admin = document.querySelector('[value="superadmin"]')
            const esyWeb = document.querySelector('[value="esy-web"]')

            admin.checked = true
            admin.dispatchEvent(new Event('change'))

            expect(member.checked).toBe(false)
            expect(esyWeb.checked).toBe(true)
        })

        it('does nothing when admin is unchecked', async () => {
            await loadApp()

            const member = document.querySelector('[value="gp_member"]')
            const admin = document.querySelector('[value="superadmin"]')

            member.checked = true
            admin.checked = false
            admin.dispatchEvent(new Event('change'))

            expect(member.checked).toBe(true)
        })
    })

    describe('Stats period selector', () => {
        beforeEach(() => {
            document.body.innerHTML = `
                <select id="stats-period-select">
                    <option value="current">Mois en cours</option>
                    <option value="custom">Personnalise</option>
                </select>
                <div id="stats-custom-range" class="hidden"></div>
            `
        })

        it('shows custom range when custom is selected', async () => {
            await loadApp()
            const select = document.getElementById('stats-period-select')
            const range = document.getElementById('stats-custom-range')

            select.value = 'custom'
            select.dispatchEvent(new Event('change'))

            expect(range.classList.contains('hidden')).toBe(false)
        })

        it('hides custom range when current is selected', async () => {
            await loadApp()
            const select = document.getElementById('stats-period-select')
            const range = document.getElementById('stats-custom-range')

            select.value = 'current'
            select.dispatchEvent(new Event('change'))

            expect(range.classList.contains('hidden')).toBe(true)
        })
    })

    describe('data-confirm forms', () => {
        it('prevents submission and shows confirm modal', async () => {
            document.body.innerHTML = '<form data-confirm="Etes-vous sur ?"><button type="submit">Submit</button></form>'

            await loadApp()
            const form = document.querySelector('form')
            const event = new Event('submit', { cancelable: true })
            form.dispatchEvent(event)

            // The custom confirm modal should prevent default
            expect(event.defaultPrevented).toBe(true)

            // The confirm modal should be visible
            const confirmModal = document.getElementById('confirm-modal')
            expect(confirmModal).not.toBeNull()
            expect(confirmModal.classList.contains('hidden')).toBe(false)
        })

        it('closes confirm modal on cancel click', async () => {
            document.body.innerHTML = '<form data-confirm="Etes-vous sur ?"><button type="submit">Submit</button></form>'

            await loadApp()
            const form = document.querySelector('form')
            form.dispatchEvent(new Event('submit', { cancelable: true }))

            const confirmModal = document.getElementById('confirm-modal')
            expect(confirmModal.classList.contains('hidden')).toBe(false)

            // Click cancel
            document.getElementById('confirm-cancel').click()
            expect(confirmModal.classList.contains('hidden')).toBe(true)
        })

        it('closes confirm modal on overlay click', async () => {
            document.body.innerHTML = '<form data-confirm="Etes-vous sur ?"><button type="submit">Submit</button></form>'

            await loadApp()
            const form = document.querySelector('form')
            form.dispatchEvent(new Event('submit', { cancelable: true }))

            const confirmModal = document.getElementById('confirm-modal')
            expect(confirmModal.classList.contains('hidden')).toBe(false)

            document.getElementById('confirm-overlay').click()
            expect(confirmModal.classList.contains('hidden')).toBe(true)
        })

        it('closes confirm modal on Escape key', async () => {
            document.body.innerHTML = '<form data-confirm="Etes-vous sur ?"><button type="submit">Submit</button></form>'

            await loadApp()
            const form = document.querySelector('form')
            form.dispatchEvent(new Event('submit', { cancelable: true }))

            const confirmModal = document.getElementById('confirm-modal')
            expect(confirmModal.classList.contains('hidden')).toBe(false)

            document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }))
            expect(confirmModal.classList.contains('hidden')).toBe(true)
        })

        it('submits form when confirm OK is clicked', async () => {
            document.body.innerHTML = '<form data-confirm="Etes-vous sur ?"><button type="submit">Submit</button></form>'

            await loadApp()
            const form = document.querySelector('form')
            form.requestSubmit = vi.fn()

            form.dispatchEvent(new Event('submit', { cancelable: true }))

            // Click OK to confirm
            document.getElementById('confirm-ok').click()

            expect(form.requestSubmit).toHaveBeenCalled()
            expect(document.getElementById('confirm-modal').classList.contains('hidden')).toBe(true)
        })

        it('displays the confirm message from data attribute', async () => {
            document.body.innerHTML = '<form data-confirm="Voulez-vous supprimer ?"><button type="submit">Submit</button></form>'

            await loadApp()
            const form = document.querySelector('form')
            form.dispatchEvent(new Event('submit', { cancelable: true }))

            expect(document.getElementById('confirm-message').textContent).toBe('Voulez-vous supprimer ?')
        })
    })

    describe('Sidebar dropdown', () => {
        it('toggles dropdown menu and arrow on click', async () => {
            document.body.innerHTML = `<div><button class="sidebar-dropdown-btn"><span class="sidebar-dropdown-arrow"></span></button><ul class="hidden">Menu</ul></div>`
            await loadApp()

            const btn = document.querySelector('.sidebar-dropdown-btn')
            const menu = btn.nextElementSibling
            const arrow = btn.querySelector('.sidebar-dropdown-arrow')

            expect(menu.classList.contains('hidden')).toBe(true)
            expect(arrow.classList.contains('rotate-180')).toBe(false)

            btn.dispatchEvent(new MouseEvent('click', { bubbles: true }))

            expect(btn).not.toBeNull()
            expect(arrow).not.toBeNull()
        })
    })

    describe('Mobile sidebar', () => {
        beforeEach(() => {
            document.body.innerHTML = `
                <button id="admin-sidebar-toggle"></button>
                <div id="admin-sidebar"></div>
                <div id="admin-overlay"></div>
            `
        })

        it('opens sidebar on toggle click', async () => {
            await loadApp()
            document.getElementById('admin-sidebar-toggle').click()
            expect(document.getElementById('admin-sidebar').classList.contains('open')).toBe(true)
        })

        it('closes sidebar on overlay click', async () => {
            await loadApp()
            const sidebar = document.getElementById('admin-sidebar')
            sidebar.classList.add('open')

            document.getElementById('admin-overlay').click()
            expect(sidebar.classList.contains('open')).toBe(false)
        })
    })

    describe('Mobile menu (public)', () => {
        it('toggles mobile menu and icons', async () => {
            document.body.innerHTML = `
                <button id="mobile-menu-btn"></button>
                <div id="mobile-menu" class="hidden"></div>
                <span id="menu-icon-open"></span>
                <span id="menu-icon-close" class="hidden"></span>
            `
            await loadApp()

            document.getElementById('mobile-menu-btn').click()

            expect(document.getElementById('mobile-menu').classList.contains('hidden')).toBe(false)
            expect(document.getElementById('menu-icon-open').classList.contains('hidden')).toBe(true)
            expect(document.getElementById('menu-icon-close').classList.contains('hidden')).toBe(false)
        })
    })

    describe('Cookie banner', () => {
        beforeEach(() => {
            document.body.innerHTML = `
                <div id="cookie-banner" class="hidden"></div>
                <button id="cookie-accept"></button>
                <button id="cookie-refuse"></button>
            `
        })

        it('shows banner when no consent', async () => {
            localStorageMock.getItem.mockReturnValue(null)
            await loadApp()
            expect(document.getElementById('cookie-banner').classList.contains('hidden')).toBe(false)
        })

        it('hides banner when already accepted', async () => {
            localStorageMock.getItem.mockReturnValue('accepted')
            await loadApp()
            expect(document.getElementById('cookie-banner').classList.contains('hidden')).toBe(true)
        })

        it('hides banner and stores accepted on accept click', async () => {
            localStorageMock.getItem.mockReturnValue(null)
            await loadApp()
            document.getElementById('cookie-accept').click()

            expect(document.getElementById('cookie-banner').classList.contains('hidden')).toBe(true)
            expect(localStorageMock.setItem).toHaveBeenCalledWith('cookie_consent', 'accepted')
        })

        it('hides banner and stores refused on refuse click', async () => {
            localStorageMock.getItem.mockReturnValue(null)
            await loadApp()
            document.getElementById('cookie-refuse').click()

            expect(document.getElementById('cookie-banner').classList.contains('hidden')).toBe(true)
            expect(localStorageMock.setItem).toHaveBeenCalledWith('cookie_consent', 'refused')
        })
    })

    describe('Tarif tabs', () => {
        beforeEach(() => {
            document.body.innerHTML = `
                <div id="tarif-tabs">
                    <button data-tab="ndd">NDD</button>
                    <button data-tab="mail">Mail</button>
                </div>
                <div id="content-ndd">NDD content</div>
                <div id="content-mail" class="hidden">Mail content</div>
            `
        })

        it('switches tabs on click', async () => {
            await loadApp()

            document.querySelector('[data-tab="mail"]').click()

            expect(document.getElementById('content-ndd').classList.contains('hidden')).toBe(true)
            expect(document.getElementById('content-mail').classList.contains('hidden')).toBe(false)
        })
    })

    describe('Search functionality', () => {
        it('search setup does nothing without elements', async () => {
            document.body.innerHTML = ''
            await loadApp()
            expect(true).toBe(true)
        })

        it('hides results when query is too short', async () => {
            document.body.innerHTML = `
                <input id="search-customers" value="">
                <div id="search-results" class="hidden"></div>
            `
            await loadApp()

            const input = document.getElementById('search-customers')
            input.value = 'a'
            input.dispatchEvent(new Event('input'))

            expect(document.getElementById('search-results').classList.contains('hidden')).toBe(true)
        })

        it('performs search when query is long enough', async () => {
            document.body.innerHTML = `
                <input id="search-customers" value="">
                <div id="search-results" class="hidden"></div>
            `

            globalThis.fetch = vi.fn(() =>
                Promise.resolve({
                    json: () => Promise.resolve([
                        { id: 1, fullName: 'Jean Dupont', email: 'jean@test.com' }
                    ])
                })
            )

            await loadApp()

            const input = document.getElementById('search-customers')
            input.value = 'jean'
            input.dispatchEvent(new Event('input'))

            // Wait for debounce (300ms) + fetch
            await new Promise(r => setTimeout(r, 400))

            expect(globalThis.fetch).toHaveBeenCalled()
        })

        it('performs search with no results', async () => {
            document.body.innerHTML = `
                <input id="search-customers" value="">
                <div id="search-results" class="hidden"></div>
            `

            globalThis.fetch = vi.fn(() =>
                Promise.resolve({ json: () => Promise.resolve([]) })
            )

            await loadApp()

            const input = document.getElementById('search-customers')
            input.value = 'xyz'
            input.dispatchEvent(new Event('input'))

            await new Promise(r => setTimeout(r, 400))

            const results = document.getElementById('search-results')
            expect(results.classList.contains('hidden')).toBe(false)
            expect(results.innerHTML).toContain('Aucun resultat')
        })

        it('performs search with revendeur result', async () => {
            document.body.innerHTML = `
                <input id="search-revendeurs" value="">
                <div id="search-results-revendeurs" class="hidden"></div>
            `

            globalThis.fetch = vi.fn(() =>
                Promise.resolve({
                    json: () => Promise.resolve([
                        { id: 1, raisonSociale: 'Ma SARL', codeRevendeur: 'REV-001' }
                    ])
                })
            )

            await loadApp()

            const input = document.getElementById('search-revendeurs')
            input.value = 'sarl'
            input.dispatchEvent(new Event('input'))

            await new Promise(r => setTimeout(r, 400))

            const results = document.getElementById('search-results-revendeurs')
            expect(results.classList.contains('hidden')).toBe(false)
            expect(results.innerHTML).toContain('Ma SARL')
            expect(results.innerHTML).toContain('REV-001')
        })

        it('hides results when clicking outside', async () => {
            document.body.innerHTML = `
                <input id="search-customers" value="">
                <div id="search-results"><div>Result</div></div>
                <div id="outside">Outside</div>
            `
            await loadApp()

            const results = document.getElementById('search-results')

            document.dispatchEvent(new MouseEvent('click', { bubbles: true }))

            expect(results.classList.contains('hidden')).toBe(true)
        })

        it('renders hit with firstName/lastName fallback', async () => {
            document.body.innerHTML = `
                <input id="search-customers" value="">
                <div id="search-results" class="hidden"></div>
            `

            globalThis.fetch = vi.fn(() =>
                Promise.resolve({
                    json: () => Promise.resolve([
                        { id: 2, firstName: 'Marie', lastName: 'Martin' }
                    ])
                })
            )

            await loadApp()

            const input = document.getElementById('search-customers')
            input.value = 'marie'
            input.dispatchEvent(new Event('input'))

            await new Promise(r => setTimeout(r, 400))

            const results = document.getElementById('search-results')
            expect(results.innerHTML).toContain('Marie Martin')
        })

        it('renders hit with customerName subtitle', async () => {
            document.body.innerHTML = `
                <input id="search-customers" value="">
                <div id="search-results" class="hidden"></div>
            `

            globalThis.fetch = vi.fn(() =>
                Promise.resolve({
                    json: () => Promise.resolve([
                        { id: 10, fullName: 'Test User', customerName: 'ACME Corp' }
                    ])
                })
            )

            await loadApp()

            const input = document.getElementById('search-customers')
            input.value = 'test'
            input.dispatchEvent(new Event('input'))
            await new Promise(r => setTimeout(r, 400))

            const results = document.getElementById('search-results')
            expect(results.innerHTML).toContain('ACME Corp')
            expect(results.innerHTML).toContain('text-gray-400')
        })

        it('renders hit with email subtitle when no customerName', async () => {
            document.body.innerHTML = `
                <input id="search-customers" value="">
                <div id="search-results" class="hidden"></div>
            `

            globalThis.fetch = vi.fn(() =>
                Promise.resolve({
                    json: () => Promise.resolve([
                        { id: 11, fullName: 'Test User', email: 'test@example.com' }
                    ])
                })
            )

            await loadApp()

            const input = document.getElementById('search-customers')
            input.value = 'test'
            input.dispatchEvent(new Event('input'))
            await new Promise(r => setTimeout(r, 400))

            const results = document.getElementById('search-results')
            expect(results.innerHTML).toContain('test@example.com')
        })

        it('renders hit with no subtitle when no customerName and no email', async () => {
            document.body.innerHTML = `
                <input id="search-customers" value="">
                <div id="search-results" class="hidden"></div>
            `

            globalThis.fetch = vi.fn(() =>
                Promise.resolve({
                    json: () => Promise.resolve([
                        { id: 12, fullName: 'No Sub User' }
                    ])
                })
            )

            await loadApp()

            const input = document.getElementById('search-customers')
            input.value = 'nosub'
            input.dispatchEvent(new Event('input'))
            await new Promise(r => setTimeout(r, 400))

            const results = document.getElementById('search-results')
            expect(results.innerHTML).toContain('No Sub User')
            expect(results.innerHTML).not.toContain('text-gray-400 ml-2')
        })
    })

    describe('Global search', () => {
        it('hides global results on Escape', async () => {
            document.body.innerHTML = `
                <input id="global-search" value="">
                <div id="global-search-results"><div>Result</div></div>
            `
            await loadApp()

            const results = document.getElementById('global-search-results')
            results.classList.remove('hidden')
            expect(results.classList.contains('hidden')).toBe(false)

            const input = document.getElementById('global-search')
            input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))

            expect(results.classList.contains('hidden')).toBe(true)
        })
    })

    describe('SIRET search fallback branches', () => {
        it('renders result with nom_raison_sociale fallback', async () => {
            document.body.innerHTML = `
                <form>
                    <input type="text" id="siret-search-input" value="">
                    <button type="button" id="siret-search-btn">Search</button>
                    <div id="siret-search-results" class="hidden"></div>
                    <input name="raisonSociale">
                    <input name="siret">
                    <input name="address">
                    <input name="zipCode">
                    <input name="city">
                </form>
            `

            globalThis.fetch = vi.fn(() =>
                Promise.resolve({
                    json: () => Promise.resolve({
                        results: [
                            { nom_raison_sociale: 'Fallback SA', siege: { siret: '12345678901234' } }
                        ]
                    })
                })
            )

            await loadApp()

            const input = document.getElementById('siret-search-input')
            input.value = 'fallback'
            document.getElementById('siret-search-btn').click()
            await new Promise(r => setTimeout(r, 200))

            const results = document.getElementById('siret-search-results')
            expect(results.innerHTML).toContain('Fallback SA')
            expect(results.innerHTML).toContain('12345678901234')
        })

        it('fills form fields and handles missing siege fields', async () => {
            document.body.innerHTML = `
                <form>
                    <input type="text" id="siret-search-input" value="">
                    <button type="button" id="siret-search-btn">Search</button>
                    <div id="siret-search-results" class="hidden"></div>
                    <input name="raisonSociale">
                    <input name="siret">
                    <input name="address">
                    <input name="zipCode">
                    <input name="city">
                </form>
            `

            globalThis.fetch = vi.fn(() =>
                Promise.resolve({
                    json: () => Promise.resolve({
                        results: [
                            { nom_complet: 'Complete SAS', siege: {} }
                        ]
                    })
                })
            )

            await loadApp()

            const input = document.getElementById('siret-search-input')
            input.value = 'complete'
            document.getElementById('siret-search-btn').click()
            await new Promise(r => setTimeout(r, 200))

            // Click the result to fill form
            const resultItem = document.querySelector('.siret-result-item')
            expect(resultItem).not.toBeNull()
            resultItem.click()

            expect(document.querySelector('[name="raisonSociale"]').value).toBe('Complete SAS')
            expect(document.querySelector('[name="siret"]').value).toBe('')
        })

        it('handles click on result when no parent form', async () => {
            document.body.innerHTML = `
                <div>
                    <input type="text" id="siret-search-input" value="">
                    <button type="button" id="siret-search-btn">Search</button>
                    <div id="siret-search-results" class="hidden"></div>
                </div>
            `

            globalThis.fetch = vi.fn(() =>
                Promise.resolve({
                    json: () => Promise.resolve({
                        results: [
                            { nom_complet: 'No Form', siege: { siret: '999' } }
                        ]
                    })
                })
            )

            await loadApp()

            document.getElementById('siret-search-input').value = 'noform'
            document.getElementById('siret-search-btn').click()
            await new Promise(r => setTimeout(r, 200))

            const resultItem = document.querySelector('.siret-result-item')
            expect(resultItem).not.toBeNull()
            // Should not throw when no parent form
            resultItem.click()
        })
    })

    describe('Modal open/close (data-modal-open / data-modal-close)', () => {
        it('opens a modal when clicking a data-modal-open button', async () => {
            document.body.innerHTML = `
                <button data-modal-open="my-modal">Open</button>
                <div id="my-modal" class="hidden">Modal content</div>
            `
            await loadApp()

            document.querySelector('[data-modal-open="my-modal"]').click()

            expect(document.getElementById('my-modal').classList.contains('hidden')).toBe(false)
        })

        it('closes a modal when clicking a data-modal-close button', async () => {
            document.body.innerHTML = `
                <div id="my-modal">
                    <button data-modal-close="my-modal">Close</button>
                    Modal content
                </div>
            `
            await loadApp()

            // Modal starts visible
            expect(document.getElementById('my-modal').classList.contains('hidden')).toBe(false)

            document.querySelector('[data-modal-close="my-modal"]').click()

            expect(document.getElementById('my-modal').classList.contains('hidden')).toBe(true)
        })

        it('does nothing if target modal does not exist', async () => {
            document.body.innerHTML = `
                <button data-modal-open="nonexistent">Open</button>
                <button data-modal-close="nonexistent">Close</button>
            `
            await loadApp()

            // Should not throw
            document.querySelector('[data-modal-open="nonexistent"]').click()
            document.querySelector('[data-modal-close="nonexistent"]').click()
            expect(document.getElementById('nonexistent')).toBeNull()
        })

        it('handles multiple modals independently', async () => {
            document.body.innerHTML = `
                <button data-modal-open="modal-a">Open A</button>
                <button data-modal-open="modal-b">Open B</button>
                <div id="modal-a" class="hidden">Modal A</div>
                <div id="modal-b" class="hidden">Modal B</div>
            `
            await loadApp()

            document.querySelector('[data-modal-open="modal-a"]').click()

            expect(document.getElementById('modal-a').classList.contains('hidden')).toBe(false)
            expect(document.getElementById('modal-b').classList.contains('hidden')).toBe(true)
        })
    })

    describe('SIRET search (prestataire creation)', () => {
        beforeEach(() => {
            document.body.innerHTML = `
                <form>
                    <input id="siret-search-input" value="">
                    <button type="button" id="siret-search-btn">Rechercher</button>
                    <div id="siret-search-results" class="hidden"></div>
                    <input name="raisonSociale" value="">
                    <input name="siret" value="">
                    <input name="address" value="">
                    <input name="zipCode" value="">
                    <input name="city" value="">
                </form>
            `
        })

        it('shows message when query is too short', async () => {
            await loadApp()

            const input = document.getElementById('siret-search-input')
            input.value = 'ab'
            document.getElementById('siret-search-btn').click()

            const results = document.getElementById('siret-search-results')
            expect(results.classList.contains('hidden')).toBe(false)
            expect(results.innerHTML).toContain('au moins 3 caracteres')
        })

        it('shows loading state then results on search', async () => {
            globalThis.fetch = vi.fn(() =>
                Promise.resolve({
                    json: () => Promise.resolve({
                        results: [
                            {
                                nom_complet: 'Test SARL',
                                siege: {
                                    siret: '12345678901234',
                                    adresse: '1 rue de Paris',
                                    code_postal: '75001',
                                    libelle_commune: 'PARIS'
                                }
                            }
                        ]
                    })
                })
            )

            await loadApp()

            const input = document.getElementById('siret-search-input')
            input.value = 'test sarl'
            document.getElementById('siret-search-btn').click()

            // Wait for fetch
            await new Promise(r => setTimeout(r, 50))

            const results = document.getElementById('siret-search-results')
            expect(results.innerHTML).toContain('Test SARL')
            expect(results.innerHTML).toContain('12345678901234')
            expect(globalThis.fetch).toHaveBeenCalledWith('/admin/prestataires/entreprise-search?q=test%20sarl')
        })

        it('shows no results message when empty', async () => {
            globalThis.fetch = vi.fn(() =>
                Promise.resolve({
                    json: () => Promise.resolve({ results: [] })
                })
            )

            await loadApp()

            document.getElementById('siret-search-input').value = 'zzzzz'
            document.getElementById('siret-search-btn').click()

            await new Promise(r => setTimeout(r, 50))

            const results = document.getElementById('siret-search-results')
            expect(results.innerHTML).toContain('Aucun resultat')
        })

        it('fills form fields when a result is clicked', async () => {
            globalThis.fetch = vi.fn(() =>
                Promise.resolve({
                    json: () => Promise.resolve({
                        results: [
                            {
                                nom_complet: 'Ma Societe',
                                siege: {
                                    siret: '98765432109876',
                                    adresse: '10 avenue de Lyon',
                                    code_postal: '69001',
                                    libelle_commune: 'LYON'
                                }
                            }
                        ]
                    })
                })
            )

            await loadApp()

            document.getElementById('siret-search-input').value = 'ma societe'
            document.getElementById('siret-search-btn').click()

            await new Promise(r => setTimeout(r, 50))

            // Click the first result
            const resultItem = document.querySelector('.siret-result-item')
            expect(resultItem).not.toBeNull()
            resultItem.click()

            expect(document.querySelector('[name="raisonSociale"]').value).toBe('Ma Societe')
            expect(document.querySelector('[name="siret"]').value).toBe('98765432109876')
            expect(document.querySelector('[name="address"]').value).toBe('10 avenue de Lyon')
            expect(document.querySelector('[name="zipCode"]').value).toBe('69001')
            expect(document.querySelector('[name="city"]').value).toBe('LYON')

            // Results should be hidden and input cleared
            expect(document.getElementById('siret-search-results').classList.contains('hidden')).toBe(true)
            expect(document.getElementById('siret-search-input').value).toBe('')
        })

        it('shows error message on fetch failure', async () => {
            globalThis.fetch = vi.fn(() => Promise.reject(new Error('Network error')))

            await loadApp()

            document.getElementById('siret-search-input').value = 'test'
            document.getElementById('siret-search-btn').click()

            await new Promise(r => setTimeout(r, 50))

            const results = document.getElementById('siret-search-results')
            expect(results.innerHTML).toContain('Erreur lors de la recherche')
        })

        it('triggers search on Enter key in input', async () => {
            globalThis.fetch = vi.fn(() =>
                Promise.resolve({
                    json: () => Promise.resolve({ results: [] })
                })
            )

            await loadApp()

            const input = document.getElementById('siret-search-input')
            input.value = 'test enter'

            const event = new KeyboardEvent('keydown', { key: 'Enter', cancelable: true })
            input.dispatchEvent(event)

            await new Promise(r => setTimeout(r, 50))

            expect(globalThis.fetch).toHaveBeenCalled()
        })

        it('hides results when clicking outside', async () => {
            await loadApp()

            const results = document.getElementById('siret-search-results')
            results.classList.remove('hidden')
            results.innerHTML = '<p>Some results</p>'

            // Click on document body (outside)
            document.body.click()

            // Need to trigger click on document level
            document.dispatchEvent(new MouseEvent('click', { bubbles: true }))

            expect(results.classList.contains('hidden')).toBe(true)
        })
    })

    describe('Refuse toggle button', () => {
        it('toggles refuse form visibility', async () => {
            document.body.innerHTML = `
                <button id="refuse-toggle-btn">Refuser</button>
                <div id="refuse-form" class="hidden">Refuse form</div>
            `
            await loadApp()

            document.getElementById('refuse-toggle-btn').click()
            expect(document.getElementById('refuse-form').classList.contains('hidden')).toBe(false)

            document.getElementById('refuse-toggle-btn').click()
            expect(document.getElementById('refuse-form').classList.contains('hidden')).toBe(true)
        })
    })

    describe('initTabSearch', () => {
        const lineTemplate = `
            <script id="line-template" type="text/html">
                <div class="line-row" draggable="true">
                    <span class="line-pos"></span>
                    <input type="hidden" class="line-pos-input" name="lines[__INDEX__][pos]">
                    <input type="text" name="lines[__INDEX__][title]">
                    <textarea name="lines[__INDEX__][description]"></textarea>
                    <input type="number" class="line-price" name="lines[__INDEX__][priceHt]" value="0.00">
                    <select class="line-type" name="lines[__INDEX__][type]" data-services-url="/admin/devis/services?type=__TYPE__&client=1">
                        <option value="">— Type —</option>
                        <option value="hosting">Hebergement</option>
                        <option value="esymail">Esymail</option>
                        <option value="ndd">NDD</option>
                        <option value="website">Site</option>
                        <option value="maintenance">Maintenance</option>
                        <option value="other">Autre</option>
                    </select>
                    <select class="line-service-id" name="lines[__INDEX__][serviceId]" disabled>
                        <option value="">— Selectionner le service —</option>
                    </select>
                    <button type="button" class="remove-line-btn">X</button>
                </div>
            </script>
        `

        beforeEach(() => {
            document.body.innerHTML = `
                <input id="search-devis" data-url="/admin/devis/search" value="">
                <div id="search-devis-results" class="hidden"></div>
                <input id="search-adverts" data-url="/admin/adverts/search" value="">
                <div id="search-adverts-results" class="hidden"></div>
                <input id="search-factures" data-url="/admin/factures/search" value="">
                <div id="search-factures-results" class="hidden"></div>
            `
        })

        it('does nothing when elements are absent', async () => {
            document.body.innerHTML = ''
            await loadApp()
            expect(true).toBe(true)
        })

        it('hides results when query is shorter than 2 chars', async () => {
            await loadApp()

            const input = document.getElementById('search-devis')
            const results = document.getElementById('search-devis-results')

            input.value = 'a'
            input.dispatchEvent(new Event('input'))

            expect(results.classList.contains('hidden')).toBe(true)
        })

        it('shows results with correct HTML on successful search', async () => {
            globalThis.fetch = vi.fn(() =>
                Promise.resolve({
                    json: () => Promise.resolve([
                        { numOrder: 'DEV-001', customerName: 'Client A', totalTtc: '120.00', state: 'send' }
                    ])
                })
            )

            await loadApp()

            const input = document.getElementById('search-devis')
            const results = document.getElementById('search-devis-results')

            input.value = 'DEV'
            input.dispatchEvent(new Event('input'))

            await new Promise(r => setTimeout(r, 350))

            expect(globalThis.fetch).toHaveBeenCalledWith('/admin/devis/search?q=DEV')
            expect(results.classList.contains('hidden')).toBe(false)
            expect(results.innerHTML).toContain('DEV-001')
            expect(results.innerHTML).toContain('Client A')
            expect(results.innerHTML).toContain('120.00')
        })

        it('shows "Aucun resultat" when search returns empty array', async () => {
            globalThis.fetch = vi.fn(() =>
                Promise.resolve({ json: () => Promise.resolve([]) })
            )

            await loadApp()

            const input = document.getElementById('search-devis')
            const results = document.getElementById('search-devis-results')

            input.value = 'xyz'
            input.dispatchEvent(new Event('input'))

            await new Promise(r => setTimeout(r, 350))

            expect(results.classList.contains('hidden')).toBe(false)
            expect(results.innerHTML).toContain('Aucun resultat')
        })

        it('hides results when clicking outside', async () => {
            await loadApp()

            const results = document.getElementById('search-devis-results')
            results.classList.remove('hidden')

            document.dispatchEvent(new MouseEvent('click', { bubbles: true }))

            expect(results.classList.contains('hidden')).toBe(true)
        })

        it('hides results on Escape key', async () => {
            await loadApp()

            const input = document.getElementById('search-devis')
            const results = document.getElementById('search-devis-results')
            results.classList.remove('hidden')

            input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }))

            expect(results.classList.contains('hidden')).toBe(true)
        })

        it('renders state labels and colors correctly', async () => {
            globalThis.fetch = vi.fn(() =>
                Promise.resolve({
                    json: () => Promise.resolve([
                        { numOrder: 'DEV-002', customerName: 'Client B', totalTtc: '50.00', state: 'accepted' }
                    ])
                })
            )

            await loadApp()

            const input = document.getElementById('search-devis')
            const results = document.getElementById('search-devis-results')

            input.value = 'DEV'
            input.dispatchEvent(new Event('input'))

            await new Promise(r => setTimeout(r, 350))

            expect(results.innerHTML).toContain('Accepte')
            expect(results.innerHTML).toContain('bg-green-500/20')
        })

        it('renders unknown state as-is', async () => {
            globalThis.fetch = vi.fn(() =>
                Promise.resolve({
                    json: () => Promise.resolve([
                        { numOrder: 'ADV-001', customerName: 'Client C', totalTtc: '200.00', state: 'pending' }
                    ])
                })
            )

            await loadApp()

            const input = document.getElementById('search-factures')
            const results = document.getElementById('search-factures-results')

            input.value = 'ADV'
            input.dispatchEvent(new Event('input'))

            await new Promise(r => setTimeout(r, 350))

            expect(results.innerHTML).toContain('pending')
        })
    })

    describe('initDevisLines', () => {
        const lineTemplate = `
            <div class="line-row" draggable="true">
                <span class="line-pos"></span>
                <input type="hidden" class="line-pos-input" name="lines[__INDEX__][pos]">
                <input type="text" name="lines[__INDEX__][title]">
                <textarea name="lines[__INDEX__][description]"></textarea>
                <input type="number" class="line-price" name="lines[__INDEX__][priceHt]" value="0.00">
                <select class="line-type" name="lines[__INDEX__][type]" data-services-url="/admin/devis/services?type=__TYPE__&client=1">
                    <option value="">— Type —</option>
                    <option value="hosting">Hebergement</option>
                    <option value="esymail">Esymail</option>
                    <option value="ndd">NDD</option>
                    <option value="website">Site</option>
                    <option value="maintenance">Maintenance</option>
                    <option value="other">Autre</option>
                </select>
                <select class="line-service-id" name="lines[__INDEX__][serviceId]" disabled>
                    <option value="">— Selectionner le service —</option>
                </select>
                <button type="button" class="remove-line-btn">X</button>
            </div>
        `

        beforeEach(() => {
            document.body.innerHTML = `
                <div id="lines-container"></div>
                <button id="add-line-btn">Ajouter</button>
                <script id="line-template" type="text/html">${lineTemplate}<\/script>
                <div id="total-ht">0.00 EUR</div>
                <form id="devis-form"></form>
            `
        })

        it('does nothing when required elements are absent', async () => {
            document.body.innerHTML = ''
            await loadApp()
            expect(true).toBe(true)
        })

        it('adds a line when add button is clicked', async () => {
            await loadApp()

            document.getElementById('add-line-btn').click()

            const container = document.getElementById('lines-container')
            expect(container.querySelectorAll('.line-row').length).toBe(1)
        })

        it('renumber updates positions after adding lines', async () => {
            await loadApp()

            const addBtn = document.getElementById('add-line-btn')
            addBtn.click()
            addBtn.click()
            addBtn.click()

            const container = document.getElementById('lines-container')
            const rows = container.querySelectorAll('.line-row')
            expect(rows.length).toBe(3)
            expect(rows[0].querySelector('.line-pos').textContent).toBe('#1')
            expect(rows[1].querySelector('.line-pos').textContent).toBe('#2')
            expect(rows[2].querySelector('.line-pos').textContent).toBe('#3')
            expect(rows[0].querySelector('.line-pos-input').value).toBe('0')
            expect(rows[1].querySelector('.line-pos-input').value).toBe('1')
        })

        it('recalc updates total-ht', async () => {
            await loadApp()

            const addBtn = document.getElementById('add-line-btn')
            addBtn.click()
            addBtn.click()

            const container = document.getElementById('lines-container')
            const prices = container.querySelectorAll('.line-price')
            prices[0].value = '100.00'
            prices[1].value = '50.50'

            // Trigger input event to recalc
            prices[0].dispatchEvent(new Event('input', { bubbles: true }))

            // Manually check the total (recalc is called on add but we changed values)
            // Trigger input on second price too
            prices[1].dispatchEvent(new Event('input', { bubbles: true }))

            expect(document.getElementById('total-ht').textContent).toBe('150.50 EUR')
        })

        it('remove button removes the line and updates positions', async () => {
            await loadApp()

            const addBtn = document.getElementById('add-line-btn')
            addBtn.click()
            addBtn.click()

            const container = document.getElementById('lines-container')
            expect(container.querySelectorAll('.line-row').length).toBe(2)

            const removeBtn = container.querySelector('.remove-line-btn')
            removeBtn.dispatchEvent(new MouseEvent('click', { bubbles: true }))

            expect(container.querySelectorAll('.line-row').length).toBe(1)
            expect(container.querySelector('.line-pos').textContent).toBe('#1')
        })

        it('price input change triggers recalc', async () => {
            await loadApp()

            document.getElementById('add-line-btn').click()

            const container = document.getElementById('lines-container')
            const priceInput = container.querySelector('.line-price')
            priceInput.value = '75.25'
            priceInput.dispatchEvent(new Event('input', { bubbles: true }))

            expect(document.getElementById('total-ht').textContent).toBe('75.25 EUR')
        })

        it('quick-price-btn pre-fills fields', async () => {
            document.body.innerHTML = `
                <div id="lines-container"></div>
                <button id="add-line-btn">Ajouter</button>
                <script id="line-template" type="text/html">${lineTemplate}<\/script>
                <div id="total-ht">0.00 EUR</div>
                <form id="devis-form"></form>
                <button class="quick-price-btn"
                    data-title="Nom service"
                    data-description="Description service"
                    data-price="99.99"
                    data-line-type="">Rapide</button>
            `
            await loadApp()

            document.querySelector('.quick-price-btn').click()

            const container = document.getElementById('lines-container')
            const row = container.querySelector('.line-row:last-child')
            expect(row).not.toBeNull()
            expect(row.querySelector('input[name$="[title]"]').value).toBe('Nom service')
            expect(row.querySelector('textarea[name$="[description]"]').value).toBe('Description service')
            expect(row.querySelector('.line-price').value).toBe('99.99')
            expect(document.getElementById('total-ht').textContent).toBe('99.99 EUR')
        })

        it('quick-price-btn with line-type triggers type change event', async () => {
            globalThis.fetch = vi.fn(() =>
                Promise.resolve({
                    json: () => Promise.resolve([{ id: 42, label: 'Service Esymail' }])
                })
            )

            document.body.innerHTML = `
                <div id="lines-container"></div>
                <button id="add-line-btn">Ajouter</button>
                <script id="line-template" type="text/html">${lineTemplate}<\/script>
                <div id="total-ht">0.00 EUR</div>
                <form id="devis-form"></form>
                <button class="quick-price-btn"
                    data-title="Mail"
                    data-description="Abonnement mail"
                    data-price="12.00"
                    data-line-type="esymail">Rapide</button>
            `
            await loadApp()

            document.querySelector('.quick-price-btn').click()

            await new Promise(r => setTimeout(r, 50))

            const container = document.getElementById('lines-container')
            const typeSelect = container.querySelector('.line-type')
            expect(typeSelect.value).toBe('esymail')
        })

        it('form validation prevents submit when esymail service not selected', async () => {
            await loadApp()

            document.getElementById('add-line-btn').click()

            const container = document.getElementById('lines-container')
            const row = container.querySelector('.line-row')
            const typeSelect = row.querySelector('.line-type')
            const serviceSelect = row.querySelector('.line-service-id')

            // Set type to esymail and add a service option (simulating loaded services)
            typeSelect.value = 'esymail'
            serviceSelect.disabled = false
            const opt = document.createElement('option')
            opt.value = '5'
            opt.textContent = 'Mail service'
            serviceSelect.appendChild(opt)
            // Leave serviceSelect.value as '' (not selected)

            window.alert = vi.fn()

            const form = document.getElementById('devis-form')
            const submitEvent = new Event('submit', { cancelable: true })
            form.dispatchEvent(submitEvent)

            expect(submitEvent.defaultPrevented).toBe(true)
            expect(window.alert).toHaveBeenCalled()
        })

        it('form validation allows submit when type is hosting (no service required)', async () => {
            await loadApp()

            document.getElementById('add-line-btn').click()

            const container = document.getElementById('lines-container')
            const row = container.querySelector('.line-row')
            const typeSelect = row.querySelector('.line-type')
            typeSelect.value = 'hosting'

            window.alert = vi.fn()

            const form = document.getElementById('devis-form')
            const submitEvent = new Event('submit', { cancelable: true })
            form.dispatchEvent(submitEvent)

            expect(submitEvent.defaultPrevented).toBe(false)
            expect(window.alert).not.toHaveBeenCalled()
        })

        it('type change triggers service loading via fetch', async () => {
            globalThis.fetch = vi.fn(() =>
                Promise.resolve({
                    json: () => Promise.resolve([
                        { id: 10, label: 'Esymail A' },
                        { id: 11, label: 'Esymail B' }
                    ])
                })
            )

            await loadApp()

            document.getElementById('add-line-btn').click()

            const container = document.getElementById('lines-container')
            const row = container.querySelector('.line-row')
            const typeSelect = row.querySelector('.line-type')

            typeSelect.value = 'esymail'
            typeSelect.dispatchEvent(new Event('change', { bubbles: true }))

            await new Promise(r => setTimeout(r, 50))

            const serviceSelect = row.querySelector('.line-service-id')
            expect(serviceSelect.disabled).toBe(false)
            expect(serviceSelect.options.length).toBeGreaterThan(1)
            expect(serviceSelect.innerHTML).toContain('Esymail A')
        })

        it('type change to hosting disables service select', async () => {
            await loadApp()

            document.getElementById('add-line-btn').click()

            const container = document.getElementById('lines-container')
            const row = container.querySelector('.line-row')
            const typeSelect = row.querySelector('.line-type')

            typeSelect.value = 'hosting'
            typeSelect.dispatchEvent(new Event('change', { bubbles: true }))

            await new Promise(r => setTimeout(r, 50))

            const serviceSelect = row.querySelector('.line-service-id')
            expect(serviceSelect.disabled).toBe(true)
        })

        it('drag & drop reordering: dragstart adds dragging class', async () => {
            await loadApp()

            const addBtn = document.getElementById('add-line-btn')
            addBtn.click()
            addBtn.click()

            const container = document.getElementById('lines-container')
            const firstRow = container.querySelector('.line-row')

            const dragstartEvent = new Event('dragstart', { bubbles: true })
            dragstartEvent.dataTransfer = { effectAllowed: '' }
            firstRow.dispatchEvent(dragstartEvent)

            expect(firstRow.classList.contains('dragging')).toBe(true)
        })

        it('drag & drop: dragend removes dragging class and renumbers', async () => {
            await loadApp()

            const addBtn = document.getElementById('add-line-btn')
            addBtn.click()
            addBtn.click()

            const container = document.getElementById('lines-container')
            const firstRow = container.querySelector('.line-row')

            const dragstartEvent = new Event('dragstart', { bubbles: true })
            dragstartEvent.dataTransfer = { effectAllowed: '' }
            firstRow.dispatchEvent(dragstartEvent)

            expect(firstRow.classList.contains('dragging')).toBe(true)

            const dragendEvent = new Event('dragend', { bubbles: true })
            firstRow.dispatchEvent(dragendEvent)

            expect(firstRow.classList.contains('dragging')).toBe(false)
        })

        it('drag & drop: dragover adds drag-over class to target', async () => {
            await loadApp()

            const addBtn = document.getElementById('add-line-btn')
            addBtn.click()
            addBtn.click()

            const container = document.getElementById('lines-container')
            const rows = container.querySelectorAll('.line-row')
            const firstRow = rows[0]
            const secondRow = rows[1]

            // Start drag on first row (bubbles up to container)
            const dragstartEvent = new Event('dragstart', { bubbles: true })
            dragstartEvent.dataTransfer = { effectAllowed: '' }
            firstRow.dispatchEvent(dragstartEvent)

            // Dispatch dragover on secondRow so e.target === secondRow when it bubbles to container
            const dragoverEvent = new Event('dragover', { bubbles: true, cancelable: true })
            secondRow.dispatchEvent(dragoverEvent)

            expect(secondRow.classList.contains('drag-over')).toBe(true)
        })

        it('drag & drop: drop reorders rows (drag down: target.after)', async () => {
            await loadApp()

            const addBtn = document.getElementById('add-line-btn')
            addBtn.click()
            addBtn.click()

            const container = document.getElementById('lines-container')
            let rows = container.querySelectorAll('.line-row')
            const firstRow = rows[0]
            const secondRow = rows[1]

            // Set a data attribute to identify the rows after reorder (not .line-pos since renumber overwrites it)
            firstRow.dataset.testId = 'FIRST'
            secondRow.dataset.testId = 'SECOND'

            // Start drag on first row (bubbles up to container)
            const dragstartEvent = new Event('dragstart', { bubbles: true })
            dragstartEvent.dataTransfer = { effectAllowed: '' }
            firstRow.dispatchEvent(dragstartEvent)

            // Dispatch drop on secondRow so e.target === secondRow when it bubbles to container
            const dropEvent = new Event('drop', { bubbles: true, cancelable: true })
            secondRow.dispatchEvent(dropEvent)

            rows = container.querySelectorAll('.line-row')
            // firstRow (draggedIdx=0) < targetIdx (secondRow, idx=1) → target.after(draggedRow)
            // So order should be: secondRow then firstRow
            expect(rows[0].dataset.testId).toBe('SECOND')
            expect(rows[1].dataset.testId).toBe('FIRST')
        })

        it('drag & drop: drop reorders rows (drag up: target.before)', async () => {
            await loadApp()

            const addBtn = document.getElementById('add-line-btn')
            addBtn.click()
            addBtn.click()

            const container = document.getElementById('lines-container')
            let rows = container.querySelectorAll('.line-row')
            const firstRow = rows[0]
            const secondRow = rows[1]

            firstRow.dataset.testId = 'FIRST'
            secondRow.dataset.testId = 'SECOND'

            // Start drag on SECOND row (index 1)
            const dragstartEvent = new Event('dragstart', { bubbles: true })
            dragstartEvent.dataTransfer = { effectAllowed: '' }
            secondRow.dispatchEvent(dragstartEvent)

            // Drop on FIRST row: draggedIdx(1) > targetIdx(0) → target.before(draggedRow)
            const dropEvent = new Event('drop', { bubbles: true, cancelable: true })
            firstRow.dispatchEvent(dropEvent)

            rows = container.querySelectorAll('.line-row')
            // secondRow should now be before firstRow
            expect(rows[0].dataset.testId).toBe('SECOND')
            expect(rows[1].dataset.testId).toBe('FIRST')
        })

        it('initial lines prefill from data-initial-lines', async () => {
            const initialLines = JSON.stringify([
                { pos: 0, title: 'Ligne 1', description: 'Desc 1', priceHt: '100.00', type: 'hosting' },
                { pos: 1, title: 'Ligne 2', description: 'Desc 2', priceHt: '200.00', type: 'other' }
            ])
            document.body.innerHTML = `
                <div id="lines-container" data-initial-lines='${initialLines}'></div>
                <button id="add-line-btn">Ajouter</button>
                <script id="line-template" type="text/html">${lineTemplate}<\/script>
                <div id="total-ht">0.00 EUR</div>
                <form id="devis-form"></form>
            `
            await loadApp()

            await new Promise(r => setTimeout(r, 50))

            const container = document.getElementById('lines-container')
            const rows = container.querySelectorAll('.line-row')
            expect(rows.length).toBe(2)
            expect(rows[0].querySelector('input[name$="[title]"]').value).toBe('Ligne 1')
            expect(rows[1].querySelector('input[name$="[title]"]').value).toBe('Ligne 2')
        })

        it('initial lines with serviceId fetches services and pre-selects', async () => {
            globalThis.fetch = vi.fn(() =>
                Promise.resolve({
                    json: () => Promise.resolve([
                        { id: 7, label: 'Mail Pro' },
                        { id: 8, label: 'Mail Starter' }
                    ])
                })
            )

            const initialLines = JSON.stringify([
                { pos: 0, title: 'Mail', description: '', priceHt: '15.00', type: 'esymail', serviceId: 7 }
            ])
            document.body.innerHTML = `
                <div id="lines-container" data-initial-lines='${initialLines}'></div>
                <button id="add-line-btn">Ajouter</button>
                <script id="line-template" type="text/html">${lineTemplate}<\/script>
                <div id="total-ht">0.00 EUR</div>
                <form id="devis-form"></form>
            `
            await loadApp()

            await new Promise(r => setTimeout(r, 100))

            const container = document.getElementById('lines-container')
            const row = container.querySelector('.line-row')
            expect(row).not.toBeNull()
            const serviceSelect = row.querySelector('.line-service-id')
            expect(serviceSelect.disabled).toBe(false)
            expect(serviceSelect.innerHTML).toContain('Mail Pro')

            // The option with id=7 should be selected
            expect(serviceSelect.value).toBe('7')
        })

        it('handles invalid JSON in data-initial-lines gracefully', async () => {
            document.body.innerHTML = `
                <div id="lines-container" data-initial-lines='not-valid-json'></div>
                <button id="add-line-btn">Ajouter</button>
                <script id="line-template" type="text/html">${lineTemplate}<\/script>
                <div id="total-ht">0.00 EUR</div>
                <form id="devis-form"></form>
            `
            // Should not throw
            await loadApp()
            const container = document.getElementById('lines-container')
            expect(container.querySelectorAll('.line-row').length).toBe(0)
        })

        it('type change fetch error is silently ignored', async () => {
            globalThis.fetch = vi.fn(() => Promise.reject(new Error('Network error')))

            await loadApp()

            document.getElementById('add-line-btn').click()

            const container = document.getElementById('lines-container')
            const row = container.querySelector('.line-row')
            const typeSelect = row.querySelector('.line-type')

            typeSelect.value = 'esymail'
            typeSelect.dispatchEvent(new Event('change', { bubbles: true }))

            // Should not throw — error is silently caught
            await new Promise(r => setTimeout(r, 50))

            const serviceSelect = row.querySelector('.line-service-id')
            expect(serviceSelect.disabled).toBe(true)
        })

        it('drag & drop: dragstart on non-row element returns early', async () => {
            await loadApp()

            document.getElementById('add-line-btn').click()

            const container = document.getElementById('lines-container')
            // Dispatch dragstart directly on the container (not a .line-row) → row = null, early return
            const dragstartEvent = new Event('dragstart', { bubbles: true })
            dragstartEvent.dataTransfer = { effectAllowed: '' }
            container.dispatchEvent(dragstartEvent)

            // No row should have the dragging class since we returned early
            const rows = container.querySelectorAll('.line-row')
            rows.forEach(r => expect(r.classList.contains('dragging')).toBe(false))
        })

        it('drag & drop: dragend on non-row element skips row.classList.remove', async () => {
            await loadApp()

            document.getElementById('add-line-btn').click()

            const container = document.getElementById('lines-container')
            const row = container.querySelector('.line-row')

            // Start drag normally so draggedRow is set
            const dragstartEvent = new Event('dragstart', { bubbles: true })
            dragstartEvent.dataTransfer = { effectAllowed: '' }
            row.dispatchEvent(dragstartEvent)
            expect(row.classList.contains('dragging')).toBe(true)

            // Dispatch dragend directly on the container (not a .line-row) → if (row) false branch
            const dragendEvent = new Event('dragend', { bubbles: true })
            container.dispatchEvent(dragendEvent)

            // draggedRow should now be null (renumber called) — no throw
            expect(container.querySelectorAll('.line-row').length).toBe(1)
        })

        it('drag & drop: drop on same row returns early', async () => {
            await loadApp()

            document.getElementById('add-line-btn').click()
            document.getElementById('add-line-btn').click()

            const container = document.getElementById('lines-container')
            const rows = container.querySelectorAll('.line-row')
            const firstRow = rows[0]
            const secondRow = rows[1]

            firstRow.dataset.testId = 'FIRST'
            secondRow.dataset.testId = 'SECOND'

            // Start drag on firstRow
            const dragstartEvent = new Event('dragstart', { bubbles: true })
            dragstartEvent.dataTransfer = { effectAllowed: '' }
            firstRow.dispatchEvent(dragstartEvent)

            // Drop on the SAME row → target === draggedRow → early return, no reorder
            const dropEvent = new Event('drop', { bubbles: true, cancelable: true })
            firstRow.dispatchEvent(dropEvent)

            const updatedRows = container.querySelectorAll('.line-row')
            expect(updatedRows[0].dataset.testId).toBe('FIRST')
            expect(updatedRows[1].dataset.testId).toBe('SECOND')
        })

        it('drag & drop: drop with no target row returns early', async () => {
            await loadApp()

            document.getElementById('add-line-btn').click()
            document.getElementById('add-line-btn').click()

            const container = document.getElementById('lines-container')
            const rows = container.querySelectorAll('.line-row')
            const firstRow = rows[0]

            firstRow.dataset.testId = 'FIRST'
            rows[1].dataset.testId = 'SECOND'

            // Start drag normally
            const dragstartEvent = new Event('dragstart', { bubbles: true })
            dragstartEvent.dataTransfer = { effectAllowed: '' }
            firstRow.dispatchEvent(dragstartEvent)

            // Drop directly on the container (not a .line-row) → target = null → early return
            const dropEvent = new Event('drop', { bubbles: true, cancelable: true })
            container.dispatchEvent(dropEvent)

            // Order unchanged
            const updatedRows = container.querySelectorAll('.line-row')
            expect(updatedRows[0].dataset.testId).toBe('FIRST')
            expect(updatedRows[1].dataset.testId).toBe('SECOND')
        })

        it('drag & drop: dragover on same row returns early', async () => {
            await loadApp()

            document.getElementById('add-line-btn').click()
            document.getElementById('add-line-btn').click()

            const container = document.getElementById('lines-container')
            const rows = container.querySelectorAll('.line-row')
            const firstRow = rows[0]

            // Start drag on firstRow
            const dragstartEvent = new Event('dragstart', { bubbles: true })
            dragstartEvent.dataTransfer = { effectAllowed: '' }
            firstRow.dispatchEvent(dragstartEvent)

            // Dragover on same row → target === draggedRow → early return, no drag-over class
            const dragoverEvent = new Event('dragover', { bubbles: true, cancelable: true })
            firstRow.dispatchEvent(dragoverEvent)

            expect(firstRow.classList.contains('drag-over')).toBe(false)
        })

        it('quick-price-btn with no lastRow returns early (template has no .line-row class)', async () => {
            document.body.innerHTML = `
                <div id="lines-container"></div>
                <button id="add-line-btn">Ajouter</button>
                <script id="line-template" type="text/html">
                    <div class="not-a-line-row">
                        <span class="line-pos"></span>
                        <input type="hidden" class="line-pos-input" name="lines[__INDEX__][pos]">
                        <input type="text" name="lines[__INDEX__][title]">
                        <textarea name="lines[__INDEX__][description]"></textarea>
                        <input type="number" class="line-price" name="lines[__INDEX__][priceHt]" value="0.00">
                    </div>
                <\/script>
                <div id="total-ht">0.00 EUR</div>
                <form id="devis-form"></form>
                <button class="quick-price-btn" data-title="T" data-price="1.00">Quick</button>
            `
            await loadApp()

            // addLine adds a node without .line-row class → querySelector('.line-row:last-child') → null → early return
            document.querySelector('.quick-price-btn').click()

            const container = document.getElementById('lines-container')
            // The node was added but it has no .line-row class
            expect(container.querySelector('.line-row')).toBeNull()
        })

        it('quick-price-btn with data-line-type sets typeSelect value and dispatches change', async () => {
            globalThis.fetch = vi.fn(() =>
                Promise.resolve({
                    json: () => Promise.resolve([{ id: 3, label: 'Esymail Pro' }])
                })
            )

            document.body.innerHTML = `
                <div id="lines-container"></div>
                <button id="add-line-btn">Ajouter</button>
                <script id="line-template" type="text/html">${lineTemplate}<\/script>
                <div id="total-ht">0.00 EUR</div>
                <form id="devis-form"></form>
                <button class="quick-price-btn"
                    data-title="Mail"
                    data-description="desc"
                    data-price="10.00"
                    data-line-type="esymail">Quick</button>
            `
            await loadApp()

            document.querySelector('.quick-price-btn').click()
            await new Promise(r => setTimeout(r, 200))

            const container = document.getElementById('lines-container')
            const typeSelect = container.querySelector('.line-type')
            expect(typeSelect.value).toBe('esymail')
            // The change event should have triggered fetch for services
            expect(globalThis.fetch).toHaveBeenCalled()
        })

        it('type change with items returned adds options and enables select', async () => {
            globalThis.fetch = vi.fn(() =>
                Promise.resolve({
                    json: () => Promise.resolve([
                        { id: 20, label: 'NDD A' },
                        { id: 21, label: 'NDD B' }
                    ])
                })
            )

            await loadApp()

            document.getElementById('add-line-btn').click()

            const container = document.getElementById('lines-container')
            const row = container.querySelector('.line-row')
            const typeSelect = row.querySelector('.line-type')

            typeSelect.value = 'ndd'
            typeSelect.dispatchEvent(new Event('change', { bubbles: true }))

            await new Promise(r => setTimeout(r, 100))

            const serviceSelect = row.querySelector('.line-service-id')
            expect(serviceSelect.disabled).toBe(false)
            expect(serviceSelect.options.length).toBeGreaterThan(1)
            expect(serviceSelect.innerHTML).toContain('NDD A')
            expect(serviceSelect.innerHTML).toContain('NDD B')
        })

        it('type change to hosting returns early without fetch', async () => {
            globalThis.fetch = vi.fn()

            await loadApp()

            document.getElementById('add-line-btn').click()

            const container = document.getElementById('lines-container')
            const row = container.querySelector('.line-row')
            const typeSelect = row.querySelector('.line-type')

            typeSelect.value = 'hosting'
            typeSelect.dispatchEvent(new Event('change', { bubbles: true }))

            await new Promise(r => setTimeout(r, 50))

            // No fetch should have been called — hosting returns early
            expect(globalThis.fetch).not.toHaveBeenCalled()
            expect(row.querySelector('.line-service-id').disabled).toBe(true)
        })

        it('form validation: esymail with missing service calls preventDefault', async () => {
            await loadApp()

            document.getElementById('add-line-btn').click()

            const container = document.getElementById('lines-container')
            const row = container.querySelector('.line-row')
            const typeSelect = row.querySelector('.line-type')
            const serviceSelect = row.querySelector('.line-service-id')

            typeSelect.value = 'esymail'
            serviceSelect.disabled = false
            const opt = document.createElement('option')
            opt.value = '9'
            opt.textContent = 'Mail Perso'
            serviceSelect.appendChild(opt)
            // Leave serviceSelect.value as '' (empty)

            window.alert = vi.fn()

            const form = document.getElementById('devis-form')
            const submitEvent = new Event('submit', { cancelable: true })
            form.dispatchEvent(submitEvent)

            expect(submitEvent.defaultPrevented).toBe(true)
            expect(window.alert).toHaveBeenCalled()
        })

        it('form validation: hosting type skips service check', async () => {
            await loadApp()

            document.getElementById('add-line-btn').click()

            const container = document.getElementById('lines-container')
            const row = container.querySelector('.line-row')
            const typeSelect = row.querySelector('.line-type')

            typeSelect.value = 'hosting'

            window.alert = vi.fn()

            const form = document.getElementById('devis-form')
            const submitEvent = new Event('submit', { cancelable: true })
            form.dispatchEvent(submitEvent)

            expect(submitEvent.defaultPrevented).toBe(false)
            expect(window.alert).not.toHaveBeenCalled()
        })

        it('form validation: row without .line-type is skipped via continue', async () => {
            await loadApp()

            // Add a line then manually remove the .line-type select from the DOM
            document.getElementById('add-line-btn').click()

            const container = document.getElementById('lines-container')
            const row = container.querySelector('.line-row')
            row.querySelector('.line-type').remove()

            window.alert = vi.fn()

            const form = document.getElementById('devis-form')
            const submitEvent = new Event('submit', { cancelable: true })
            form.dispatchEvent(submitEvent)

            // Should not throw, no alert — the row was skipped
            expect(submitEvent.defaultPrevented).toBe(false)
            expect(window.alert).not.toHaveBeenCalled()
        })

        it('recalc with NaN value ignores the non-numeric input', async () => {
            await loadApp()

            document.getElementById('add-line-btn').click()
            document.getElementById('add-line-btn').click()

            const container = document.getElementById('lines-container')
            const prices = container.querySelectorAll('.line-price')
            prices[0].value = '50.00'
            prices[1].value = 'abc'  // NaN

            prices[0].dispatchEvent(new Event('input', { bubbles: true }))
            prices[1].dispatchEvent(new Event('input', { bubbles: true }))

            // Only the valid price should be counted
            expect(document.getElementById('total-ht').textContent).toBe('50.00 EUR')
        })

        it('remove line button removes row and updates renumber + recalc', async () => {
            await loadApp()

            const addBtn = document.getElementById('add-line-btn')
            addBtn.click()
            addBtn.click()
            addBtn.click()

            const container = document.getElementById('lines-container')
            expect(container.querySelectorAll('.line-row').length).toBe(3)

            // Set prices so recalc can be verified
            const prices = container.querySelectorAll('.line-price')
            prices[0].value = '10.00'
            prices[1].value = '20.00'
            prices[2].value = '30.00'

            // Remove the second row
            const secondRow = container.querySelectorAll('.line-row')[1]
            secondRow.querySelector('.remove-line-btn').dispatchEvent(new MouseEvent('click', { bubbles: true }))

            expect(container.querySelectorAll('.line-row').length).toBe(2)
            // Positions should be renumbered
            expect(container.querySelectorAll('.line-pos')[0].textContent).toBe('#1')
            expect(container.querySelectorAll('.line-pos')[1].textContent).toBe('#2')
            // Recalc: rows remaining have prices 10.00 and 30.00
            expect(document.getElementById('total-ht').textContent).toBe('40.00 EUR')
        })

        it('price input change event triggers recalc', async () => {
            await loadApp()

            document.getElementById('add-line-btn').click()

            const container = document.getElementById('lines-container')
            const priceInput = container.querySelector('.line-price')
            priceInput.value = '123.45'
            priceInput.dispatchEvent(new Event('input', { bubbles: true }))

            expect(document.getElementById('total-ht').textContent).toBe('123.45 EUR')
        })

        it('prefill with serviceId and type fetches services and selects the matching option', async () => {
            globalThis.fetch = vi.fn(() =>
                Promise.resolve({
                    json: () => Promise.resolve([
                        { id: 5, label: 'Esymail Pro' },
                        { id: 6, label: 'Esymail Starter' }
                    ])
                })
            )

            const initialLines = JSON.stringify([
                { pos: 0, title: 'Mail ligne', description: '', priceHt: '20.00', type: 'esymail', serviceId: 5 }
            ])
            document.body.innerHTML = `
                <div id="lines-container" data-initial-lines='${initialLines}'></div>
                <button id="add-line-btn">Ajouter</button>
                <script id="line-template" type="text/html">${lineTemplate}<\/script>
                <div id="total-ht">0.00 EUR</div>
                <form id="devis-form"></form>
            `
            await loadApp()

            await new Promise(r => setTimeout(r, 200))

            const container = document.getElementById('lines-container')
            const row = container.querySelector('.line-row')
            expect(row).not.toBeNull()

            const serviceSelect = row.querySelector('.line-service-id')
            expect(serviceSelect.disabled).toBe(false)
            expect(serviceSelect.innerHTML).toContain('Esymail Pro')
            expect(serviceSelect.value).toBe('5')
        })

        it('prefill with serviceId fetch error covers catch branch', async () => {
            globalThis.fetch = vi.fn(() => Promise.reject(new Error('Network error')))

            const initialLines = JSON.stringify([
                { pos: 0, title: 'Fail line', description: '', priceHt: '10.00', type: 'esymail', serviceId: 99 }
            ])
            document.body.innerHTML = `
                <div id="lines-container" data-initial-lines='${initialLines}'></div>
                <button id="add-line-btn">Ajouter</button>
                <script id="line-template" type="text/html">${lineTemplate}<\/script>
                <div id="total-ht">0.00 EUR</div>
                <form id="devis-form"></form>
            `
            await loadApp()
            await new Promise(r => setTimeout(r, 200))

            const container = document.getElementById('lines-container')
            const row = container.querySelector('.line-row')
            expect(row).not.toBeNull()
            // Service select should remain disabled since fetch failed
            const serviceSelect = row.querySelector('.line-service-id')
            expect(serviceSelect.disabled).toBe(true)
        })

        it('prefill with missing optional fields uses defaults', async () => {
            const initialLines = JSON.stringify([
                { pos: 0 }
            ])
            document.body.innerHTML = `
                <div id="lines-container" data-initial-lines='${initialLines}'></div>
                <button id="add-line-btn">Ajouter</button>
                <script id="line-template" type="text/html">${lineTemplate}<\/script>
                <div id="total-ht">0.00 EUR</div>
                <form id="devis-form"></form>
            `
            await loadApp()

            const container = document.getElementById('lines-container')
            const row = container.querySelector('.line-row')
            expect(row).not.toBeNull()
            expect(row.querySelector('input[name$="[title]"]').value).toBe('')
            expect(row.querySelector('textarea[name$="[description]"]').value).toBe('')
            expect(row.querySelector('.line-price').value).toBe('0.00')
        })

        it('prefill with type but no serviceId does not fetch', async () => {
            globalThis.fetch = vi.fn()

            const initialLines = JSON.stringify([
                { pos: 0, title: 'Host', priceHt: '50.00', type: 'hosting' }
            ])
            document.body.innerHTML = `
                <div id="lines-container" data-initial-lines='${initialLines}'></div>
                <button id="add-line-btn">Ajouter</button>
                <script id="line-template" type="text/html">${lineTemplate}<\/script>
                <div id="total-ht">0.00 EUR</div>
                <form id="devis-form"></form>
            `
            await loadApp()
            await new Promise(r => setTimeout(r, 200))

            // hosting type with no serviceId should not trigger fetch
            expect(globalThis.fetch).not.toHaveBeenCalled()
        })

        it('prefill with invalid JSON catches the error silently', async () => {
            document.body.innerHTML = `
                <div id="lines-container" data-initial-lines='{{invalid json'></div>
                <button id="add-line-btn">Ajouter</button>
                <script id="line-template" type="text/html">${lineTemplate}<\/script>
                <div id="total-ht">0.00 EUR</div>
                <form id="devis-form"></form>
            `
            // Should not throw
            await loadApp()

            const container = document.getElementById('lines-container')
            // No lines added since JSON parsing failed
            expect(container.querySelectorAll('.line-row').length).toBe(0)
        })
    })

    describe('Global search (navbar)', () => {
        beforeEach(() => {
            document.body.innerHTML = `
                <input id="global-search" value="">
                <div id="global-search-results" class="hidden"></div>
            `
        })

        it('hides results when query is shorter than 2 chars', async () => {
            await loadApp()

            const input = document.getElementById('global-search')
            const results = document.getElementById('global-search-results')
            results.classList.remove('hidden')

            input.value = 'a'
            input.dispatchEvent(new Event('input'))

            expect(results.classList.contains('hidden')).toBe(true)
        })

        it('shows results with hits on successful search', async () => {
            globalThis.fetch = vi.fn(() =>
                Promise.resolve({
                    json: () => Promise.resolve([
                        { url: '/admin/clients/1', type: 'client', label: 'Jean Dupont', sub: 'jean@example.com' },
                        { url: '/admin/services/ndd/1', type: 'ndd', label: 'exemple.fr', sub: null }
                    ])
                })
            )

            await loadApp()

            const input = document.getElementById('global-search')
            const results = document.getElementById('global-search-results')

            input.value = 'jean'
            input.dispatchEvent(new Event('input'))

            await new Promise(r => setTimeout(r, 350))

            expect(results.classList.contains('hidden')).toBe(false)
            expect(results.innerHTML).toContain('Jean Dupont')
            expect(results.innerHTML).toContain('Client')
            expect(results.innerHTML).toContain('exemple.fr')
            expect(results.innerHTML).toContain('NDD')
        })

        it('shows "Aucun resultat" when search returns empty', async () => {
            globalThis.fetch = vi.fn(() =>
                Promise.resolve({ json: () => Promise.resolve([]) })
            )

            await loadApp()

            const input = document.getElementById('global-search')
            const results = document.getElementById('global-search-results')

            input.value = 'zzzz'
            input.dispatchEvent(new Event('input'))

            await new Promise(r => setTimeout(r, 350))

            expect(results.classList.contains('hidden')).toBe(false)
            expect(results.innerHTML).toContain('Aucun resultat')
        })

        it('hides results on Escape key', async () => {
            await loadApp()

            const input = document.getElementById('global-search')
            const results = document.getElementById('global-search-results')
            results.classList.remove('hidden')

            input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }))

            expect(results.classList.contains('hidden')).toBe(true)
        })

        it('renders hit with unknown type using raw type string', async () => {
            globalThis.fetch = vi.fn(() =>
                Promise.resolve({
                    json: () => Promise.resolve([
                        { url: '/admin/foo/1', type: 'unknown_type', label: 'Item X', sub: 'detail' }
                    ])
                })
            )

            await loadApp()

            const input = document.getElementById('global-search')
            input.value = 'item'
            input.dispatchEvent(new Event('input'))

            await new Promise(r => setTimeout(r, 350))

            const results = document.getElementById('global-search-results')
            expect(results.innerHTML).toContain('unknown_type')
            expect(results.innerHTML).toContain('Item X')
        })
    })
})
