import { AxiosError } from 'axios'
import { screen, waitFor, act, cleanup, within } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { QueryClient } from '@tanstack/react-query'
import { describe, it, expect, vi, beforeEach } from 'vitest'
import CrowdSecConfig from '../CrowdSecConfig'
import * as securityApi from '../../api/security'
import * as crowdsecApi from '../../api/crowdsec'
import * as presetsApi from '../../api/presets'
import * as backupsApi from '../../api/backups'
import * as settingsApi from '../../api/settings'
import { CROWDSEC_PRESETS } from '../../data/crowdsecPresets'
import { renderWithQueryClient, createTestQueryClient } from '../../test-utils/renderWithQueryClient'
import { toast } from '../../utils/toast'
import * as exportUtils from '../../utils/crowdsecExport'

vi.mock('../../api/security')
vi.mock('../../api/crowdsec')
vi.mock('../../api/presets')
vi.mock('../../api/backups')
vi.mock('../../api/settings')
vi.mock('../../utils/crowdsecExport', () => ({
  buildCrowdsecExportFilename: vi.fn(() => 'crowdsec-default.tar.gz'),
  promptCrowdsecFilename: vi.fn(() => 'crowdsec.tar.gz'),
  downloadCrowdsecExport: vi.fn(),
}))
vi.mock('../../utils/toast', () => ({
  toast: {
    success: vi.fn(),
    error: vi.fn(),
    info: vi.fn(),
  },
}))

const baseStatus = {
  cerberus: { enabled: true },
  crowdsec: { enabled: true, mode: 'local' as const, api_url: '' },
  waf: { enabled: true, mode: 'enabled' as const },
  rate_limit: { enabled: true },
  acl: { enabled: true },
}

const disabledStatus = {
  ...baseStatus,
  crowdsec: { ...baseStatus.crowdsec, enabled: true, mode: 'disabled' as const },
}

const presetFromCatalog = CROWDSEC_PRESETS[0]

const axiosError = (status: number, message: string, data?: Record<string, unknown>) =>
  new AxiosError(message, undefined, undefined, undefined, {
    status,
    statusText: String(status),
    headers: {},
    config: {},
    data: data ?? { error: message },
  } as never)

const defaultFileList = ['acquis.yaml', 'collections.yaml']

const renderPage = async (client?: QueryClient) => {
  const result = renderWithQueryClient(<CrowdSecConfig />, { client })
  await waitFor(() => screen.getByText('CrowdSec Configuration'))
  return result
}

describe('CrowdSecConfig coverage', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(baseStatus)
    vi.mocked(crowdsecApi.listCrowdsecFiles).mockResolvedValue({ files: defaultFileList })
    vi.mocked(crowdsecApi.readCrowdsecFile).mockResolvedValue({ content: 'file-content' })
    vi.mocked(crowdsecApi.writeCrowdsecFile).mockResolvedValue(undefined)
    vi.mocked(crowdsecApi.listCrowdsecDecisions).mockResolvedValue({ decisions: [] })
    vi.mocked(crowdsecApi.banIP).mockResolvedValue(undefined)
    vi.mocked(crowdsecApi.unbanIP).mockResolvedValue(undefined)
    vi.mocked(crowdsecApi.exportCrowdsecConfig).mockResolvedValue(new Blob(['data']))
    vi.mocked(crowdsecApi.importCrowdsecConfig).mockResolvedValue(undefined)
    vi.mocked(presetsApi.listCrowdsecPresets).mockResolvedValue({
      presets: [
        {
          slug: presetFromCatalog.slug,
          title: presetFromCatalog.title,
          summary: presetFromCatalog.description,
          source: 'hub',
          requires_hub: false,
          available: true,
          cached: false,
          cache_key: 'cache-123',
          etag: 'etag-123',
          retrieved_at: '2024-01-01T00:00:00Z',
        },
      ],
    })
    vi.mocked(presetsApi.pullCrowdsecPreset).mockResolvedValue({
      status: 'pulled',
      slug: presetFromCatalog.slug,
      preview: presetFromCatalog.content,
      cache_key: 'cache-123',
      etag: 'etag-123',
      retrieved_at: '2024-01-01T00:00:00Z',
      source: 'hub',
    })
    vi.mocked(presetsApi.applyCrowdsecPreset).mockResolvedValue({
      status: 'applied',
      backup: '/tmp/backup.tar.gz',
      reload_hint: true,
      used_cscli: true,
      cache_key: 'cache-123',
      slug: presetFromCatalog.slug,
    })
    vi.mocked(presetsApi.getCrowdsecPresetCache).mockResolvedValue({
      preview: 'cached-preview',
      cache_key: 'cache-123',
      etag: 'etag-123',
    })
    vi.mocked(backupsApi.createBackup).mockResolvedValue({ filename: 'backup.tar.gz' })
    vi.mocked(settingsApi.updateSetting).mockResolvedValue()
  })

  it('renders loading and error boundaries', async () => {
    vi.mocked(securityApi.getSecurityStatus).mockReturnValue(new Promise(() => {}))
    renderWithQueryClient(<CrowdSecConfig />)
    expect(await screen.findByText('Loading CrowdSec configuration...')).toBeInTheDocument()

    cleanup()

    vi.mocked(securityApi.getSecurityStatus).mockRejectedValue(new Error('boom'))
    renderWithQueryClient(<CrowdSecConfig />)
    expect(await screen.findByText(/Failed to load security status/)).toBeInTheDocument()
  })

  it('handles missing status and missing crowdsec sections', async () => {
    vi.mocked(securityApi.getSecurityStatus).mockRejectedValueOnce(new Error('data is undefined'))
    renderWithQueryClient(<CrowdSecConfig />)
    expect(await screen.findByText(/Failed to load security status/)).toBeInTheDocument()

    cleanup()

    vi.mocked(securityApi.getSecurityStatus).mockResolvedValueOnce({ cerberus: { enabled: true } } as never)
    renderWithQueryClient(<CrowdSecConfig />)
    expect(await screen.findByText('CrowdSec configuration not found in security status')).toBeInTheDocument()
  })

  it('renders disabled mode message and bans control disabled', async () => {
    vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(disabledStatus)
    await renderPage(createTestQueryClient())
    expect(screen.getByText('Enable CrowdSec to manage banned IPs')).toBeInTheDocument()
    expect(screen.getByRole('button', { name: /Ban IP/ })).toBeDisabled()
  })

  it('toggles mode success and error', async () => {
    await renderPage()
    const toggle = screen.getByTestId('crowdsec-mode-toggle')
    await userEvent.click(toggle)
    await waitFor(() => expect(settingsApi.updateSetting).toHaveBeenCalledWith('security.crowdsec.mode', 'disabled', 'security', 'string'))
    expect(toast.success).toHaveBeenCalledWith('CrowdSec disabled')

    vi.mocked(settingsApi.updateSetting).mockRejectedValueOnce(new Error('nope'))
    await userEvent.click(toggle)
    await waitFor(() => expect(toast.error).toHaveBeenCalledWith('nope'))
  })

  it('guards import without a file and shows error on import failure', async () => {
    await renderPage()
    const importBtn = screen.getByTestId('import-btn')
    await userEvent.click(importBtn)
    expect(backupsApi.createBackup).not.toHaveBeenCalled()

    const fileInput = screen.getByTestId('import-file') as HTMLInputElement
    const file = new File(['data'], 'cfg.tar.gz')
    await userEvent.upload(fileInput, file)
    vi.mocked(crowdsecApi.importCrowdsecConfig).mockRejectedValueOnce(new Error('bad import'))
    await userEvent.click(importBtn)
    await waitFor(() => expect(toast.error).toHaveBeenCalledWith('bad import'))
  })

  it('imports configuration after creating a backup', async () => {
    await renderPage()
    const fileInput = screen.getByTestId('import-file') as HTMLInputElement
    await userEvent.upload(fileInput, new File(['data'], 'cfg.tar.gz'))
    await userEvent.click(screen.getByTestId('import-btn'))
    await waitFor(() => expect(backupsApi.createBackup).toHaveBeenCalled())
    await waitFor(() => expect(crowdsecApi.importCrowdsecConfig).toHaveBeenCalled())
  })

  it('exports configuration success and failure', async () => {
    await renderPage()
    await userEvent.click(screen.getByRole('button', { name: 'Export' }))
    await waitFor(() => expect(crowdsecApi.exportCrowdsecConfig).toHaveBeenCalled())
    expect(exportUtils.downloadCrowdsecExport).toHaveBeenCalled()
    expect(toast.success).toHaveBeenCalledWith('CrowdSec configuration exported')

    vi.mocked(exportUtils.promptCrowdsecFilename).mockReturnValueOnce('crowdsec.tar.gz')
    vi.mocked(crowdsecApi.exportCrowdsecConfig).mockRejectedValueOnce(new Error('fail'))
    await userEvent.click(screen.getByRole('button', { name: 'Export' }))
    await waitFor(() => expect(toast.error).toHaveBeenCalledWith('Failed to export CrowdSec configuration'))
  })

  it('auto-selects first preset and pulls preview', async () => {
    await renderPage()
    const select = screen.getByTestId('preset-select') as HTMLSelectElement
    expect(select.value).toBe(presetFromCatalog.slug)
    await waitFor(() => expect(presetsApi.pullCrowdsecPreset).toHaveBeenCalledWith(presetFromCatalog.slug))
    const previewText = screen.getByTestId('preset-preview').textContent?.replace(/\s+/g, ' ')
    expect(previewText).toContain('crowdsecurity/http-cve')
    expect(screen.getByTestId('preset-meta')).toHaveTextContent('cache-123')
  })

  it('handles pull validation, hub unavailable, and generic errors', async () => {
    vi.mocked(presetsApi.pullCrowdsecPreset).mockRejectedValueOnce(axiosError(400, 'slug invalid', { error: 'slug invalid' }))
    await renderPage()
    expect(await screen.findByTestId('preset-validation-error')).toHaveTextContent('slug invalid')

    vi.mocked(presetsApi.pullCrowdsecPreset).mockRejectedValueOnce(axiosError(503, 'hub down', { error: 'hub down' }))
    await userEvent.click(screen.getByText('Pull Preview'))
    await waitFor(() => expect(screen.getByTestId('preset-hub-unavailable')).toBeInTheDocument())

    vi.mocked(presetsApi.pullCrowdsecPreset).mockRejectedValueOnce(axiosError(500, 'boom', { error: 'boom' }))
    await userEvent.click(screen.getByText('Pull Preview'))
    await waitFor(() => expect(screen.getByTestId('preset-status')).toHaveTextContent('boom'))
  })

  it('loads cached preview and reports cache errors', async () => {
    vi.mocked(presetsApi.listCrowdsecPresets).mockResolvedValueOnce({
      presets: [
        {
          slug: presetFromCatalog.slug,
          title: presetFromCatalog.title,
          summary: presetFromCatalog.description,
          source: 'hub',
          requires_hub: false,
          available: true,
          cached: true,
          cache_key: 'cache-123',
          etag: 'etag-123',
          retrieved_at: '2024-01-01T00:00:00Z',
        },
      ],
    })
    await renderPage()
    await userEvent.click(screen.getByText('Pull Preview'))
    await waitFor(() => {
      const preview = screen.getByTestId('preset-preview').textContent?.replace(/\s+/g, ' ')
      expect(preview).toContain('crowdsecurity/http-cve')
    })
    await userEvent.click(screen.getByText('Load cached preview'))
    await waitFor(() => expect(screen.getByTestId('preset-preview')).toHaveTextContent('cached-preview'))

    vi.mocked(presetsApi.getCrowdsecPresetCache).mockRejectedValueOnce(axiosError(500, 'cache-miss'))
    await userEvent.click(screen.getByText('Load cached preview'))
    await waitFor(() => expect(toast.error).toHaveBeenCalledWith('cache-miss'))
  })

  it('sets apply info on backend success', async () => {
    await renderPage()
    await userEvent.click(screen.getByTestId('apply-preset-btn'))
    await waitFor(() => expect(screen.getByTestId('preset-apply-info')).toHaveTextContent('Backup: /tmp/backup.tar.gz'))
  })

  it('falls back to local apply on 501 and covers validation/hub/offline branches', async () => {
    vi.mocked(crowdsecApi.writeCrowdsecFile).mockResolvedValue({})
    vi.mocked(presetsApi.applyCrowdsecPreset).mockRejectedValueOnce(axiosError(501, 'not implemented'))
    await renderPage()
    const applyBtn = screen.getByTestId('apply-preset-btn')
    await userEvent.click(applyBtn)
    await waitFor(() => expect(toast.info).toHaveBeenCalledWith('Preset apply is not available on the server; applying locally instead'))
    await waitFor(() => expect(crowdsecApi.writeCrowdsecFile).toHaveBeenCalled())

    vi.mocked(presetsApi.applyCrowdsecPreset).mockRejectedValueOnce(axiosError(400, 'bad', { error: 'validation failed' }))
    await userEvent.click(applyBtn)
    await waitFor(() => expect(screen.getByTestId('preset-validation-error')).toHaveTextContent('validation failed'))

    vi.mocked(presetsApi.applyCrowdsecPreset).mockRejectedValueOnce(axiosError(503, 'hub'))
    await userEvent.click(applyBtn)
    await waitFor(() => expect(screen.getByTestId('preset-hub-unavailable')).toBeInTheDocument())

    vi.mocked(presetsApi.applyCrowdsecPreset).mockRejectedValueOnce(axiosError(500, 'not cached', { error: 'Pull the preset first' }))
    await userEvent.click(applyBtn)
    await waitFor(() => expect(screen.getByTestId('preset-validation-error')).toHaveTextContent('Preset must be pulled'))
  })

  it('records backup info on apply failure and generic errors', async () => {
    vi.mocked(presetsApi.applyCrowdsecPreset).mockRejectedValueOnce(axiosError(500, 'failed', { error: 'boom', backup: '/tmp/backup' }))
    await renderPage()
    await userEvent.click(screen.getByTestId('apply-preset-btn'))
    await waitFor(() => expect(screen.getByTestId('preset-apply-info')).toHaveTextContent('/tmp/backup'))

    cleanup()

    vi.mocked(presetsApi.applyCrowdsecPreset).mockRejectedValueOnce(new Error('unexpected'))
    await renderPage()
    await userEvent.click(screen.getByTestId('apply-preset-btn'))
    await waitFor(() => expect(toast.error).toHaveBeenCalledWith('Failed to apply preset'))
  })

  it('disables apply when hub is unavailable for hub-only preset', async () => {
    vi.mocked(presetsApi.listCrowdsecPresets).mockResolvedValueOnce({
      presets: [
        {
          slug: 'hub-only',
          title: 'Hub Only',
          summary: 'needs hub',
          source: 'hub',
          requires_hub: true,
          available: true,
          cached: true,
          cache_key: 'cache-hub',
          etag: 'etag-hub',
        },
      ],
    })
    vi.mocked(presetsApi.pullCrowdsecPreset).mockRejectedValueOnce(axiosError(503, 'hub'))
    await renderPage()
    await waitFor(() => expect(screen.getByTestId('preset-hub-unavailable')).toBeInTheDocument())
    expect((screen.getByTestId('apply-preset-btn') as HTMLButtonElement).disabled).toBe(true)
  })

  it('guards local apply prerequisites and succeeds when content exists', async () => {
    vi.mocked(crowdsecApi.listCrowdsecFiles).mockResolvedValueOnce({ files: [] })
    vi.mocked(presetsApi.applyCrowdsecPreset).mockRejectedValueOnce(axiosError(501, 'not implemented'))
    await renderPage()
    await userEvent.click(screen.getByTestId('apply-preset-btn'))
    await waitFor(() => expect(toast.error).toHaveBeenCalledWith('Select a configuration file to apply the preset'))

    cleanup()
  vi.mocked(toast.error).mockClear()

    vi.mocked(crowdsecApi.listCrowdsecFiles).mockResolvedValue({ files: ['acquis.yaml'] })
    vi.mocked(presetsApi.listCrowdsecPresets).mockResolvedValue({
      presets: [
        {
          slug: 'custom-empty',
          title: 'Empty',
          summary: 'empty preset',
          source: 'hub',
          requires_hub: false,
          available: true,
          cached: false,
          cache_key: 'cache-empty',
          etag: 'etag-empty',
        },
      ],
    })
    vi.mocked(presetsApi.pullCrowdsecPreset).mockResolvedValue({
      status: 'pulled',
      slug: 'custom-empty',
      preview: '',
      cache_key: 'cache-empty',
    })
    vi.mocked(presetsApi.applyCrowdsecPreset).mockRejectedValueOnce(axiosError(501, 'not implemented'))
    await renderPage()
    await userEvent.selectOptions(screen.getByTestId('crowdsec-file-select'), 'acquis.yaml')
    await userEvent.click(screen.getByTestId('apply-preset-btn'))
    await waitFor(() => expect(toast.error).toHaveBeenCalledWith('Preset preview is unavailable; retry pulling before applying'))

    cleanup()
  vi.mocked(toast.error).mockClear()

    vi.mocked(presetsApi.pullCrowdsecPreset).mockResolvedValue({
      status: 'pulled',
      slug: presetFromCatalog.slug,
      preview: 'content',
      cache_key: 'cache-123',
    })
    vi.mocked(presetsApi.applyCrowdsecPreset).mockRejectedValueOnce(axiosError(501, 'not implemented'))
    vi.mocked(crowdsecApi.writeCrowdsecFile).mockResolvedValue({})
    await renderPage()
    await userEvent.selectOptions(screen.getByTestId('crowdsec-file-select'), 'acquis.yaml')
    await userEvent.click(screen.getByTestId('apply-preset-btn'))
    await waitFor(() => expect(crowdsecApi.writeCrowdsecFile).toHaveBeenCalled())
  })

  it('reads, edits, saves, and closes files', async () => {
    await renderPage()
    await userEvent.selectOptions(screen.getByTestId('crowdsec-file-select'), 'acquis.yaml')
    await waitFor(() => expect(crowdsecApi.readCrowdsecFile).toHaveBeenCalledWith('acquis.yaml'))
    const textarea = screen.getByRole('textbox') as HTMLTextAreaElement
    expect(textarea.value).toBe('file-content')
    await userEvent.clear(textarea)
    await userEvent.type(textarea, 'updated')
    await userEvent.click(screen.getByText('Save'))
    await waitFor(() => expect(backupsApi.createBackup).toHaveBeenCalled())
    await waitFor(() => expect(crowdsecApi.writeCrowdsecFile).toHaveBeenCalledWith('acquis.yaml', 'updated'))

    await userEvent.click(screen.getByText('Close'))
    expect((screen.getByTestId('crowdsec-file-select') as HTMLSelectElement).value).toBe('')
  })

  it('shows decisions table, handles loading/error/empty states, and unban errors', async () => {
    vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(disabledStatus)
    await renderPage()
    expect(screen.getByText('Enable CrowdSec to manage banned IPs')).toBeInTheDocument()

    cleanup()

    vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(baseStatus)
    vi.mocked(crowdsecApi.listCrowdsecDecisions).mockReturnValue(new Promise(() => {}))
    await renderPage()
    expect(screen.getByText('Loading banned IPs...')).toBeInTheDocument()

    cleanup()

    vi.mocked(crowdsecApi.listCrowdsecDecisions).mockRejectedValueOnce(new Error('decisions'))
    await renderPage()
    expect(await screen.findByText('Failed to load banned IPs')).toBeInTheDocument()

    cleanup()

    vi.mocked(crowdsecApi.listCrowdsecDecisions).mockResolvedValueOnce({ decisions: [] })
    await renderPage()
    expect(await screen.findByText('No banned IPs')).toBeInTheDocument()

    cleanup()

    vi.mocked(crowdsecApi.listCrowdsecDecisions).mockResolvedValueOnce({
      decisions: [
        { id: '1', ip: '1.1.1.1', reason: 'bot', duration: '24h', created_at: '2024-01-01T00:00:00Z', source: 'manual' },
      ],
    })
    await renderPage()
    expect(await screen.findByText('1.1.1.1')).toBeInTheDocument()

    vi.mocked(crowdsecApi.unbanIP).mockRejectedValueOnce(new Error('unban fail'))
    await userEvent.click(screen.getAllByText('Unban')[0])
    const confirmModal = screen.getByText('Confirm Unban').closest('div') as HTMLElement
    await userEvent.click(within(confirmModal).getByRole('button', { name: 'Unban' }))
    await waitFor(() => expect(toast.error).toHaveBeenCalledWith('unban fail'))
  })

  it('bans and unbans IPs with overlay messaging', async () => {
    vi.mocked(crowdsecApi.listCrowdsecDecisions).mockResolvedValue({
      decisions: [
        { id: '1', ip: '1.1.1.1', reason: 'bot', duration: '24h', created_at: '2024-01-01T00:00:00Z', source: 'manual' },
      ],
    })
    await renderPage()
    await userEvent.click(screen.getByRole('button', { name: /Ban IP/ }))
    const banModal = screen.getByText('Ban IP Address').closest('div') as HTMLElement
    const ipInput = within(banModal).getByPlaceholderText('192.168.1.100') as HTMLInputElement
    await userEvent.type(ipInput, '2.2.2.2')
    await userEvent.click(within(banModal).getByRole('button', { name: 'Ban IP' }))
    await waitFor(() => expect(crowdsecApi.banIP).toHaveBeenCalledWith('2.2.2.2', '24h', ''))

    // keep ban pending to assert overlay message
    let resolveBan: (() => void) | undefined
    vi.mocked(crowdsecApi.banIP).mockImplementationOnce(
      () =>
        new Promise<void>((resolve) => {
          resolveBan = () => resolve()
        }),
    )
    await userEvent.click(screen.getByRole('button', { name: /Ban IP/ }))
    const banModalSecond = screen.getByText('Ban IP Address').closest('div') as HTMLElement
    await userEvent.type(within(banModalSecond).getByPlaceholderText('192.168.1.100'), '3.3.3.3')
    await userEvent.click(within(banModalSecond).getByRole('button', { name: 'Ban IP' }))
    expect(await screen.findByText('Guardian raises shield...')).toBeInTheDocument()
    resolveBan?.()

    vi.mocked(crowdsecApi.unbanIP).mockImplementationOnce(() => new Promise(() => {}))
    const unbanButtons = await screen.findAllByText('Unban')
    await userEvent.click(unbanButtons[0])
    const confirmDialog = screen.getByText('Confirm Unban').closest('div') as HTMLElement
    await userEvent.click(within(confirmDialog).getByRole('button', { name: 'Unban' }))
    expect(await screen.findByText('Guardian lowers shield...')).toBeInTheDocument()
  })

  it('shows overlay messaging for preset pull, apply, import, write, and mode updates', async () => {
    // pull pending
    vi.mocked(presetsApi.pullCrowdsecPreset).mockImplementation(() => new Promise(() => {}))
    await renderPage()
    await userEvent.click(screen.getByText('Pull Preview'))
    expect(await screen.findByText('Fetching preset...')).toBeInTheDocument()

    cleanup()
    vi.mocked(presetsApi.pullCrowdsecPreset).mockReset()
    vi.mocked(presetsApi.pullCrowdsecPreset).mockResolvedValue({
      status: 'pulled',
      slug: presetFromCatalog.slug,
      preview: presetFromCatalog.content,
      cache_key: 'cache-123',
    })

    // apply pending
    vi.mocked(presetsApi.pullCrowdsecPreset).mockResolvedValueOnce({
      status: 'pulled',
      slug: presetFromCatalog.slug,
      preview: presetFromCatalog.content,
      cache_key: 'cache-123',
    })
    let resolveApply: (() => void) | undefined
    vi.mocked(presetsApi.applyCrowdsecPreset).mockImplementationOnce(
      () =>
        new Promise((resolve) => {
          resolveApply = () => resolve({ status: 'applied', cache_key: 'cache-123' } as never)
        }),
    )
    await renderPage()
    await userEvent.click(screen.getAllByTestId('apply-preset-btn')[0])
    expect(await screen.findByText('Loading preset...')).toBeInTheDocument()
    resolveApply?.()

    cleanup()

    // import pending
    vi.mocked(presetsApi.pullCrowdsecPreset).mockResolvedValueOnce({
      status: 'pulled',
      slug: presetFromCatalog.slug,
      preview: presetFromCatalog.content,
      cache_key: 'cache-123',
    })
    let resolveImport: (() => void) | undefined
    vi.mocked(crowdsecApi.importCrowdsecConfig).mockImplementationOnce(
      () =>
        new Promise((resolve) => {
          resolveImport = () => resolve({})
        }),
    )
    const { queryClient } = await renderPage(createTestQueryClient())
    await waitFor(() => expect(screen.getByTestId('preset-preview')).toBeInTheDocument())
    const fileInput = screen.getByTestId('import-file') as HTMLInputElement
    await userEvent.upload(fileInput, new File(['data'], 'cfg.tar.gz'))
    await userEvent.click(screen.getByTestId('import-btn'))
    expect(await screen.findByText('Summoning the guardian...')).toBeInTheDocument()
    resolveImport?.()
    await act(async () => queryClient.cancelQueries())

    cleanup()

    // write pending
    let resolveWrite: (() => void) | undefined
    vi.mocked(crowdsecApi.writeCrowdsecFile).mockImplementationOnce(
      () =>
        new Promise((resolve) => {
          resolveWrite = () => resolve({})
        }),
    )
    await renderPage()
    await waitFor(() => expect(screen.getByTestId('preset-preview')).toBeInTheDocument())
    await userEvent.selectOptions(screen.getByTestId('crowdsec-file-select'), 'acquis.yaml')
    const textarea = screen.getByRole('textbox') as HTMLTextAreaElement
    await userEvent.type(textarea, 'x')
    await userEvent.click(screen.getByText('Save'))
    expect(await screen.findByText('Guardian inscribes...')).toBeInTheDocument()
    resolveWrite?.()

    cleanup()

    // mode update pending
    vi.mocked(settingsApi.updateSetting).mockImplementationOnce(() => new Promise(() => {}))
    await renderPage()
    await userEvent.click(screen.getByTestId('crowdsec-mode-toggle'))
    expect(await screen.findByText('Three heads turn...')).toBeInTheDocument()
  })
})
