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

import {
  mockComponentFromApi,
  mockLinkSKF,
  mockLinkFAG,
  mockConstructeurSKF,
  wrapCollection,
} from '../fixtures/mockData'

// ---------------------------------------------------------------------------
// Import under test (AFTER all vi.mock calls)
// ---------------------------------------------------------------------------

import { useComponentEdit } from '~/composables/useComponentEdit'

// ---------------------------------------------------------------------------
// Mocks — API layer
// ---------------------------------------------------------------------------

const mockGet = vi.fn()
const mockPost = vi.fn()
const mockPatch = vi.fn()
const mockDel = vi.fn()
const mockPostFormData = vi.fn()

vi.mock('~/composables/useApi', () => ({
  useApi: () => ({
    get: mockGet,
    post: mockPost,
    patch: mockPatch,
    put: vi.fn(),
    delete: mockDel,
    postFormData: mockPostFormData,
  }),
}))

// ---------------------------------------------------------------------------
// Mocks — Toast
// ---------------------------------------------------------------------------

const mockShowSuccess = vi.fn()
const mockShowError = vi.fn()

vi.mock('~/composables/useToast', () => ({
  useToast: () => ({
    showSuccess: mockShowSuccess,
    showError: mockShowError,
    showInfo: vi.fn(),
    showToast: vi.fn(),
    toasts: { value: [] },
    clearAll: vi.fn(),
  }),
}))

// ---------------------------------------------------------------------------
// Mocks — useComposants (updateComposant)
// ---------------------------------------------------------------------------

const mockUpdateComposant = vi.fn()

vi.mock('~/composables/useComposants', () => ({
  useComposants: () => ({
    updateComposant: mockUpdateComposant,
    composants: { value: [] },
    loading: { value: false },
  }),
}))

// ---------------------------------------------------------------------------
// Mocks — usePieces, useProducts
// ---------------------------------------------------------------------------

vi.mock('~/composables/usePieces', () => ({
  usePieces: () => ({
    pieces: { value: [] },
    loading: { value: false },
  }),
}))

vi.mock('~/composables/useProducts', () => ({
  useProducts: () => ({
    products: { value: [] },
    loading: { value: false },
  }),
}))

// ---------------------------------------------------------------------------
// Mocks — useComponentTypes, usePieceTypes, useProductTypes
// ---------------------------------------------------------------------------

const mockLoadComponentTypes = vi.fn().mockResolvedValue(undefined)
const mockComponentTypes = { value: [] as any[] }

vi.mock('~/composables/useComponentTypes', () => ({
  useComponentTypes: () => ({
    componentTypes: mockComponentTypes,
    loadComponentTypes: mockLoadComponentTypes,
    loadingComponentTypes: { value: false },
  }),
}))

vi.mock('~/composables/usePieceTypes', () => ({
  usePieceTypes: () => ({
    pieceTypes: { value: [] },
    loadPieceTypes: vi.fn().mockResolvedValue(undefined),
  }),
}))

vi.mock('~/composables/useProductTypes', () => ({
  useProductTypes: () => ({
    productTypes: { value: [] },
    loadProductTypes: vi.fn().mockResolvedValue(undefined),
  }),
}))

// ---------------------------------------------------------------------------
// Mocks — useDocuments
// ---------------------------------------------------------------------------

const mockLoadDocumentsByComponent = vi.fn().mockResolvedValue({ success: true, data: [] })
const mockUploadDocuments = vi.fn().mockResolvedValue({ success: true, data: [] })
const mockDeleteDocument = vi.fn().mockResolvedValue({ success: true })

vi.mock('~/composables/useDocuments', () => ({
  useDocuments: () => ({
    loadDocumentsByComponent: mockLoadDocumentsByComponent,
    uploadDocuments: mockUploadDocuments,
    deleteDocument: mockDeleteDocument,
    documents: { value: [] },
    loading: { value: false },
  }),
}))

// ---------------------------------------------------------------------------
// Mocks — useConstructeurLinks
// ---------------------------------------------------------------------------

const mockFetchLinks = vi.fn().mockResolvedValue([])
const mockSyncLinks = vi.fn().mockResolvedValue(undefined)

vi.mock('~/composables/useConstructeurLinks', () => ({
  useConstructeurLinks: () => ({
    fetchLinks: mockFetchLinks,
    syncLinks: mockSyncLinks,
  }),
}))

// ---------------------------------------------------------------------------
// Mocks — useCustomFieldInputs
// ---------------------------------------------------------------------------

const mockSaveAll = vi.fn().mockResolvedValue([])
const mockRefreshCF = vi.fn()

vi.mock('~/composables/useCustomFieldInputs', () => ({
  useCustomFieldInputs: () => ({
    fields: { value: [] },
    requiredFilled: { value: true },
    saveAll: mockSaveAll,
    refresh: mockRefreshCF,
  }),
}))

// ---------------------------------------------------------------------------
// Mocks — usePermissions (auto-imported in Nuxt)
// ---------------------------------------------------------------------------

vi.stubGlobal('usePermissions', () => ({
  canEdit: { value: true },
  canManage: { value: true },
  isAdmin: { value: false },
  isGranted: () => true,
}))

// ---------------------------------------------------------------------------
// Mocks — useConstructeurs
// ---------------------------------------------------------------------------

vi.mock('~/composables/useConstructeurs', () => ({
  useConstructeurs: () => ({
    ensureConstructeurs: vi.fn().mockResolvedValue([]),
  }),
}))

// ---------------------------------------------------------------------------
// Mocks — useEntityHistory
// ---------------------------------------------------------------------------

vi.mock('~/composables/useEntityHistory', () => ({
  useEntityHistory: () => ({
    history: { value: [] },
    loading: { value: false },
    error: { value: null },
    loadHistory: vi.fn().mockResolvedValue([]),
  }),
}))

// ---------------------------------------------------------------------------
// Mocks — shared utils
// ---------------------------------------------------------------------------

vi.mock('~/shared/utils/structureDisplayUtils', () => ({
  getStructurePieces: (s: any) => Array.isArray(s?.pieces) ? s.pieces : [],
  getStructureProducts: (s: any) => Array.isArray(s?.products) ? s.products : [],
  resolvePieceLabel: (p: any) => p?.name ?? '',
  resolveProductLabel: (p: any) => p?.name ?? '',
  resolveSubcomponentLabel: (p: any) => p?.name ?? '',
  fetchModelTypeNames: vi.fn().mockResolvedValue({}),
  buildTypeLabelMap: () => ({}),
}))

vi.mock('~/shared/modelUtils', () => ({
  formatStructurePreview: () => '',
  normalizeStructureForEditor: (s: any) => s,
}))

vi.mock('~/shared/constructeurUtils', () => ({
  uniqueConstructeurIds: (ids: string[]) => [...new Set(ids)],
  constructeurIdsFromLinks: (links: any[]) => links.map((l: any) => l.constructeurId),
}))

vi.mock('~/shared/utils/structureSelectionUtils', () => ({
  collectStructureSelections: () => ({ pieces: [], products: [], components: [] }),
}))

vi.mock('~/utils/documentPreview', () => ({
  canPreviewDocument: () => false,
}))

// ---------------------------------------------------------------------------
// Test data — component with structure containing slots
// ---------------------------------------------------------------------------

const COMPONENT_ID = 'cl-comp-1'

function buildComponentWithStructure() {
  return {
    ...mockComponentFromApi,
    id: COMPONENT_ID,
    '@id': `/api/composants/${COMPONENT_ID}`,
    description: 'Un moteur triphas\u00e9 haute performance',
    prix: '1500.00',
    typeComposantId: 'tc-moteur',
    structure: {
      pieces: [
        {
          slotId: 'ps-001',
          typePieceId: 'tp-bearing-001',
          selectedPieceId: 'piece-001',
          selectedPieceName: 'Roulement 6205',
          quantity: 2,
          position: 0,
        },
        {
          slotId: 'ps-002',
          typePieceId: 'tp-seal-002',
          selectedPieceId: 'piece-002',
          selectedPieceName: 'Joint torique',
          quantity: 1,
          position: 1,
        },
      ],
      products: [
        {
          slotId: 'prs-001',
          typeProductId: 'tprod-grease-001',
          selectedProductId: 'prod-001',
          selectedProductName: 'Graisse LGMT2',
          familyCode: 'LUB',
          position: 0,
        },
      ],
      subcomponents: [
        {
          slotId: 'scs-001',
          typeComposantId: 'tc-sub-001',
          selectedComponentId: 'comp-sub-001',
          selectedComponentName: 'Palier avant',
          alias: 'Palier avant',
          familyCode: 'PAL',
          position: 0,
        },
      ],
      customFields: [],
    },
  }
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

/** Wait for next tick + micro-tasks so watchers fire. */
const tick = () => new Promise(r => setTimeout(r, 0))

/**
 * Create the composable AND hydrate it by resolving the mocked get.
 * Returns the composable instance after fetch + watcher hydration.
 */
async function createAndHydrate(overrides?: Partial<ReturnType<typeof buildComponentWithStructure>>) {
  const comp = { ...buildComponentWithStructure(), ...overrides }

  mockGet.mockImplementation((url: string) => {
    if (url.includes(`/composants/${COMPONENT_ID}`)) {
      return Promise.resolve({ success: true, data: structuredClone(comp) })
    }
    return Promise.resolve({ success: true, data: wrapCollection([]) })
  })

  mockFetchLinks.mockResolvedValue([
    { ...mockLinkSKF },
  ])

  const composable = useComponentEdit(COMPONENT_ID)

  // fetchComponent is called, then the watcher hydrates editionForm
  await composable.fetchComponent()
  await tick()

  return composable
}

// ---------------------------------------------------------------------------
// beforeEach
// ---------------------------------------------------------------------------

beforeEach(() => {
  vi.clearAllMocks()
  mockComponentTypes.value = [
    { id: 'tc-moteur', name: 'Moteur \u00e9lectrique', category: 'COMPONENT', structure: null },
  ]
})

// ---------------------------------------------------------------------------
// fetchComponent — hydration
// ---------------------------------------------------------------------------

describe('fetchComponent — hydration', () => {
  it('loads simple fields into editionForm (name, reference, description, prix)', async () => {
    const composable = await createAndHydrate()

    expect(composable.editionForm.name).toBe('Moteur principal')
    expect(composable.editionForm.reference).toBe('COMP-MOT-001')
    expect(composable.editionForm.description).toBe('Un moteur triphas\u00e9 haute performance')
    expect(composable.editionForm.prix).toBe('1500.00')
  })

  it('loads component object with structure containing slots', async () => {
    const composable = await createAndHydrate()

    expect(composable.component.value).not.toBeNull()
    expect(composable.component.value.structure).toBeDefined()
    expect(composable.component.value.structure.pieces).toHaveLength(2)
    expect(composable.component.value.structure.products).toHaveLength(1)
    expect(composable.component.value.structure.subcomponents).toHaveLength(1)
    expect(composable.component.value.customFieldValues).toBeDefined()
    expect(Array.isArray(composable.component.value.customFieldValues)).toBe(true)
  })

  it('loads constructeur links via fetchLinks', async () => {
    const composable = await createAndHydrate()

    expect(mockFetchLinks).toHaveBeenCalledWith('composant', COMPONENT_ID)
    expect(composable.constructeurLinks.value).toHaveLength(1)
    expect(composable.constructeurLinks.value[0].constructeurId).toBe(mockConstructeurSKF.id)
  })
})

// ---------------------------------------------------------------------------
// Slot operations — no data loss
// ---------------------------------------------------------------------------

describe('slot operations — no data loss', () => {
  it('setting piece slot selection preserves product and subcomponent slots', async () => {
    const composable = await createAndHydrate()

    // Record initial product and subcomponent slot entries
    const initialProductSlots = composable.productSlotEntries.value
    const initialSubSlots = composable.subcomponentSlotEntries.value

    expect(initialProductSlots).toHaveLength(1)
    expect(initialSubSlots).toHaveLength(1)

    // Change a piece slot selection
    composable.setPieceSlotSelection('ps-001', 'piece-999')
    await tick()

    // Piece slot changed
    const pieceSlots = composable.pieceSlotEntries.value
    expect(pieceSlots.find(s => s.slotId === 'ps-001')?.selectedPieceId).toBe('piece-999')

    // Product and subcomponent slots untouched
    expect(composable.productSlotEntries.value).toHaveLength(1)
    expect(composable.productSlotEntries.value[0].selectedProductId).toBe('prod-001')
    expect(composable.subcomponentSlotEntries.value).toHaveLength(1)
    expect(composable.subcomponentSlotEntries.value[0].selectedComponentId).toBe('comp-sub-001')
  })

  it('setting product slot selection preserves piece slots', async () => {
    const composable = await createAndHydrate()

    // Change a product slot
    composable.setProductSlotSelection('prs-001', 'prod-new-001')
    await tick()

    // Product changed
    expect(composable.productSlotEntries.value[0].selectedProductId).toBe('prod-new-001')

    // Piece slots untouched
    expect(composable.pieceSlotEntries.value).toHaveLength(2)
    expect(composable.pieceSlotEntries.value[0].selectedPieceId).toBe('piece-001')
    expect(composable.pieceSlotEntries.value[1].selectedPieceId).toBe('piece-002')
  })

  it('setting subcomponent slot selection preserves piece and product slots', async () => {
    const composable = await createAndHydrate()

    // Change a subcomponent slot
    composable.setSubcomponentSlotSelection('scs-001', 'comp-new-sub')
    await tick()

    // Subcomponent changed
    expect(composable.subcomponentSlotEntries.value[0].selectedComponentId).toBe('comp-new-sub')

    // Piece and product slots untouched
    expect(composable.pieceSlotEntries.value[0].selectedPieceId).toBe('piece-001')
    expect(composable.productSlotEntries.value[0].selectedProductId).toBe('prod-001')
  })

  it('setting slot quantity preserves selectedPieceId', async () => {
    const composable = await createAndHydrate()

    // Set a piece selection first
    composable.setPieceSlotSelection('ps-001', 'piece-special')
    await tick()

    // Now change quantity on the same slot
    composable.setSlotQuantity('ps-001', 5)
    await tick()

    const slot = composable.pieceSlotEntries.value.find(s => s.slotId === 'ps-001')
    expect(slot?.selectedPieceId).toBe('piece-special')
    expect(slot?.quantity).toBe(5)
  })
})

// ---------------------------------------------------------------------------
// submitEdition — no data loss
// ---------------------------------------------------------------------------

describe('submitEdition — no data loss', () => {
  it('sends all form fields in PATCH payload via updateComposant', async () => {
    mockUpdateComposant.mockResolvedValue({ success: true, data: { id: COMPONENT_ID } })

    const composable = await createAndHydrate()

    // Modify form fields
    composable.editionForm.name = 'Moteur modifi\u00e9'
    composable.editionForm.description = 'Nouvelle description'
    composable.editionForm.reference = 'REF-MOD-001'
    composable.editionForm.prix = '2500'

    await composable.submitEdition()

    expect(mockUpdateComposant).toHaveBeenCalledTimes(1)
    const payload = mockUpdateComposant.mock.calls[0]![1]
    expect(payload).toMatchObject({
      name: 'Moteur modifi\u00e9',
      description: 'Nouvelle description',
      reference: 'REF-MOD-001',
      prix: '2500',
    })
  })

  it('saves custom fields after patch', async () => {
    mockUpdateComposant.mockResolvedValue({ success: true, data: { id: COMPONENT_ID } })

    const composable = await createAndHydrate()

    await composable.submitEdition()

    expect(mockUpdateComposant).toHaveBeenCalledTimes(1)
    expect(mockSaveAll).toHaveBeenCalledTimes(1)
  })

  it('patches slot edits to correct endpoints', async () => {
    mockUpdateComposant.mockResolvedValue({ success: true, data: { id: COMPONENT_ID } })
    mockPatch.mockResolvedValue({ success: true, data: {} })

    const composable = await createAndHydrate()

    // Make slot edits
    composable.setPieceSlotSelection('ps-001', 'piece-new')
    composable.setSlotQuantity('ps-002', 3)
    composable.setProductSlotSelection('prs-001', 'prod-new')
    composable.setSubcomponentSlotSelection('scs-001', 'comp-new')

    await composable.submitEdition()

    // Verify piece slot patches
    expect(mockPatch).toHaveBeenCalledWith('/composant-piece-slots/ps-001', { selectedPieceId: 'piece-new' })
    expect(mockPatch).toHaveBeenCalledWith('/composant-piece-slots/ps-002', { quantity: 3 })

    // Verify product slot patch
    expect(mockPatch).toHaveBeenCalledWith('/composant-product-slots/prs-001', { selectedProductId: 'prod-new' })

    // Verify subcomponent slot patch
    expect(mockPatch).toHaveBeenCalledWith('/composant-subcomponent-slots/scs-001', { selectedComposantId: 'comp-new' })
  })

  it('syncs constructeur links with diff', async () => {
    mockUpdateComposant.mockResolvedValue({ success: true, data: { id: COMPONENT_ID } })

    const composable = await createAndHydrate()

    // Add a second constructeur link
    composable.constructeurLinks.value = [
      { ...mockLinkSKF },
      { ...mockLinkFAG },
    ]

    await composable.submitEdition()

    expect(mockSyncLinks).toHaveBeenCalledTimes(1)
    // originalConstructeurLinks was set to [mockLinkSKF] from fetchLinks
    // formLinks is now [mockLinkSKF, mockLinkFAG]
    const [entityType, entityId, origLinks, formLinks] = mockSyncLinks.mock.calls[0]!
    expect(entityType).toBe('composant')
    expect(entityId).toBe(COMPONENT_ID)
    expect(origLinks).toHaveLength(1)
    expect(formLinks).toHaveLength(2)
  })

  it('editing name does not lose constructeur links', async () => {
    mockUpdateComposant.mockResolvedValue({ success: true, data: { id: COMPONENT_ID } })

    const composable = await createAndHydrate()

    // Only edit name
    composable.editionForm.name = 'Nouveau nom moteur'

    await composable.submitEdition()

    // updateComposant was called with name change
    expect(mockUpdateComposant).toHaveBeenCalledTimes(1)
    const payload = mockUpdateComposant.mock.calls[0]![1]
    expect(payload.name).toBe('Nouveau nom moteur')

    // syncLinks was still called (preserving links)
    expect(mockSyncLinks).toHaveBeenCalledTimes(1)
    const [, , origLinks, formLinks] = mockSyncLinks.mock.calls[0]!
    // Both should contain the original SKF link
    expect(origLinks).toHaveLength(1)
    expect(formLinks).toHaveLength(1)
    expect(formLinks[0].constructeurId).toBe(mockConstructeurSKF.id)
  })
})

// ---------------------------------------------------------------------------
// Document operations
// ---------------------------------------------------------------------------

describe('document operations', () => {
  it('populates componentDocuments from fetchComponent response', async () => {
    const docFixtures = [
      { id: 'doc-1', name: 'photo.jpg', type: 'photo' },
      { id: 'doc-2', name: 'schema.pdf', type: 'schema' },
    ]
    const composable = await createAndHydrate({ documents: docFixtures } as any)

    expect(composable.componentDocuments.value).toHaveLength(2)
    expect(composable.componentDocuments.value[0].id).toBe('doc-1')
    expect(composable.componentDocuments.value[1].id).toBe('doc-2')
  })

  it('sets componentDocuments to empty array when response has no documents', async () => {
    const composable = await createAndHydrate({ documents: undefined } as any)

    expect(composable.componentDocuments.value).toEqual([])
  })

  it('handleFilesAdded calls uploadDocuments with composantId context', async () => {
    mockUploadDocuments.mockResolvedValue({ success: true, data: [] })
    mockLoadDocumentsByComponent.mockResolvedValue({ success: true, data: [] })

    const composable = await createAndHydrate()

    const files = [new File(['content'], 'test.pdf', { type: 'application/pdf' })]
    await composable.handleFilesAdded(files)

    expect(mockUploadDocuments).toHaveBeenCalledTimes(1)
    const callArgs = mockUploadDocuments.mock.calls[0]![0]
    expect(callArgs.files).toBe(files)
    expect(callArgs.context.composantId).toBe(COMPONENT_ID)
  })

  it('handleFilesAdded does nothing when files array is empty', async () => {
    const composable = await createAndHydrate()

    await composable.handleFilesAdded([])

    expect(mockUploadDocuments).not.toHaveBeenCalled()
  })

  it('handleFilesAdded refreshes documents after successful upload', async () => {
    const refreshedDocs = [{ id: 'doc-new', name: 'uploaded.pdf' }]
    mockUploadDocuments.mockResolvedValue({ success: true, data: [] })
    mockLoadDocumentsByComponent.mockResolvedValue({ success: true, data: refreshedDocs })

    const composable = await createAndHydrate()

    const files = [new File(['data'], 'uploaded.pdf')]
    await composable.handleFilesAdded(files)

    expect(mockLoadDocumentsByComponent).toHaveBeenCalledWith(COMPONENT_ID, { updateStore: false })
    expect(composable.componentDocuments.value).toHaveLength(1)
    expect(composable.componentDocuments.value[0].id).toBe('doc-new')
  })

  it('removeDocument calls deleteDocument and removes from local list', async () => {
    mockDeleteDocument.mockResolvedValue({ success: true })

    const composable = await createAndHydrate({
      documents: [
        { id: 'doc-a', name: 'a.pdf' },
        { id: 'doc-b', name: 'b.pdf' },
      ],
    } as any)

    expect(composable.componentDocuments.value).toHaveLength(2)

    await composable.removeDocument('doc-a')

    expect(mockDeleteDocument).toHaveBeenCalledWith('doc-a', { updateStore: false })
    expect(composable.componentDocuments.value).toHaveLength(1)
    expect(composable.componentDocuments.value[0].id).toBe('doc-b')
  })

  it('removeDocument does nothing when documentId is falsy', async () => {
    const composable = await createAndHydrate()

    await composable.removeDocument(null)
    await composable.removeDocument(undefined)

    expect(mockDeleteDocument).not.toHaveBeenCalled()
  })
})

// ---------------------------------------------------------------------------
// Null field handling in PATCH payload
// ---------------------------------------------------------------------------

describe('null field handling in PATCH payload', () => {
  it('empty prix string sends null in payload', async () => {
    mockUpdateComposant.mockResolvedValue({ success: true, data: { id: COMPONENT_ID } })

    const composable = await createAndHydrate()
    composable.editionForm.prix = ''

    await composable.submitEdition()

    const payload = mockUpdateComposant.mock.calls[0]![1]
    expect(payload.prix).toBeNull()
  })

  it('whitespace-only prix string sends null in payload', async () => {
    mockUpdateComposant.mockResolvedValue({ success: true, data: { id: COMPONENT_ID } })

    const composable = await createAndHydrate()
    composable.editionForm.prix = '   '

    await composable.submitEdition()

    const payload = mockUpdateComposant.mock.calls[0]![1]
    expect(payload.prix).toBeNull()
  })

  it('valid prix string sends stringified number', async () => {
    mockUpdateComposant.mockResolvedValue({ success: true, data: { id: COMPONENT_ID } })

    const composable = await createAndHydrate()
    composable.editionForm.prix = '42.50'

    await composable.submitEdition()

    const payload = mockUpdateComposant.mock.calls[0]![1]
    expect(payload.prix).toBe('42.5')
  })

  it('empty reference string sends null in payload', async () => {
    mockUpdateComposant.mockResolvedValue({ success: true, data: { id: COMPONENT_ID } })

    const composable = await createAndHydrate()
    composable.editionForm.reference = ''

    await composable.submitEdition()

    const payload = mockUpdateComposant.mock.calls[0]![1]
    expect(payload.reference).toBeNull()
  })

  it('empty description string sends null in payload', async () => {
    mockUpdateComposant.mockResolvedValue({ success: true, data: { id: COMPONENT_ID } })

    const composable = await createAndHydrate()
    composable.editionForm.description = ''

    await composable.submitEdition()

    const payload = mockUpdateComposant.mock.calls[0]![1]
    expect(payload.description).toBeNull()
  })

  it('whitespace-only description sends null in payload', async () => {
    mockUpdateComposant.mockResolvedValue({ success: true, data: { id: COMPONENT_ID } })

    const composable = await createAndHydrate()
    composable.editionForm.description = '   '

    await composable.submitEdition()

    const payload = mockUpdateComposant.mock.calls[0]![1]
    expect(payload.description).toBeNull()
  })

  it('name is trimmed but never null', async () => {
    mockUpdateComposant.mockResolvedValue({ success: true, data: { id: COMPONENT_ID } })

    const composable = await createAndHydrate()
    composable.editionForm.name = '  Moteur  '

    await composable.submitEdition()

    const payload = mockUpdateComposant.mock.calls[0]![1]
    expect(payload.name).toBe('Moteur')
  })
})

// ---------------------------------------------------------------------------
// Error paths
// ---------------------------------------------------------------------------

describe('error paths', () => {
  it('does not save custom fields when updateComposant returns { success: false }', async () => {
    mockUpdateComposant.mockResolvedValue({ success: false })

    const composable = await createAndHydrate()
    composable.editionForm.name = 'Test'

    await composable.submitEdition()

    expect(mockUpdateComposant).toHaveBeenCalledTimes(1)
    expect(mockSaveAll).not.toHaveBeenCalled()
    expect(mockPatch).not.toHaveBeenCalled()
    expect(mockSyncLinks).not.toHaveBeenCalled()
  })

  it('does not patch slots when updateComposant returns { success: false }', async () => {
    mockUpdateComposant.mockResolvedValue({ success: false })

    const composable = await createAndHydrate()
    composable.setPieceSlotSelection('ps-001', 'piece-new')
    composable.setProductSlotSelection('prs-001', 'prod-new')

    await composable.submitEdition()

    expect(mockPatch).not.toHaveBeenCalled()
  })

  it('does not sync constructeur links when updateComposant fails', async () => {
    mockUpdateComposant.mockResolvedValue({ success: false })

    const composable = await createAndHydrate()

    await composable.submitEdition()

    expect(mockSyncLinks).not.toHaveBeenCalled()
  })

  it('shows error toast when saveAllCustomFields returns failed fields', async () => {
    mockUpdateComposant.mockResolvedValue({ success: true, data: { id: COMPONENT_ID } })
    mockSaveAll.mockResolvedValue(['Tension nominale', 'Indice de protection'])

    const composable = await createAndHydrate()

    await composable.submitEdition()

    expect(mockShowError).toHaveBeenCalledTimes(1)
    expect(mockShowError.mock.calls[0]![0]).toContain('Tension nominale')
    expect(mockShowError.mock.calls[0]![0]).toContain('Indice de protection')
  })

  it('still saves slots and syncs links even when custom fields fail', async () => {
    mockUpdateComposant.mockResolvedValue({ success: true, data: { id: COMPONENT_ID } })
    mockSaveAll.mockResolvedValue(['Tension nominale'])
    mockPatch.mockResolvedValue({ success: true, data: {} })

    const composable = await createAndHydrate()
    composable.setPieceSlotSelection('ps-001', 'piece-after-cf-fail')

    await composable.submitEdition()

    // Slots still patched despite custom field failure
    expect(mockPatch).toHaveBeenCalledWith('/composant-piece-slots/ps-001', { selectedPieceId: 'piece-after-cf-fail' })
    // Links still synced
    expect(mockSyncLinks).toHaveBeenCalledTimes(1)
    // Success toast still shown (alongside the error toast for CF)
    expect(mockShowSuccess).toHaveBeenCalledTimes(1)
  })

  it('shows error toast when submitEdition throws', async () => {
    mockUpdateComposant.mockRejectedValue(new Error('Network failure'))

    const composable = await createAndHydrate()

    await composable.submitEdition()

    expect(mockShowError).toHaveBeenCalledTimes(1)
    expect(mockShowError.mock.calls[0]![0]).toContain('Network failure')
    expect(composable.saving.value).toBe(false)
  })

  it('resets saving flag even when updateComposant throws', async () => {
    mockUpdateComposant.mockRejectedValue(new Error('Server error'))

    const composable = await createAndHydrate()

    await composable.submitEdition()

    expect(composable.saving.value).toBe(false)
  })
})

// ---------------------------------------------------------------------------
// Custom field save verification
// ---------------------------------------------------------------------------

describe('custom field save verification', () => {
  it('saveAllCustomFields is called after successful updateComposant', async () => {
    mockUpdateComposant.mockResolvedValue({ success: true, data: { id: COMPONENT_ID } })
    mockSaveAll.mockResolvedValue([])

    const composable = await createAndHydrate()

    await composable.submitEdition()

    expect(mockSaveAll).toHaveBeenCalledTimes(1)
  })

  it('does not show error toast when saveAll returns empty array (no failures)', async () => {
    mockUpdateComposant.mockResolvedValue({ success: true, data: { id: COMPONENT_ID } })
    mockSaveAll.mockResolvedValue([])

    const composable = await createAndHydrate()

    await composable.submitEdition()

    // showError should NOT have been called (only showSuccess)
    expect(mockShowError).not.toHaveBeenCalled()
    expect(mockShowSuccess).toHaveBeenCalledTimes(1)
  })

  it('shows error with all failed field names joined', async () => {
    mockUpdateComposant.mockResolvedValue({ success: true, data: { id: COMPONENT_ID } })
    mockSaveAll.mockResolvedValue(['Champ A', 'Champ B', 'Champ C'])

    const composable = await createAndHydrate()

    await composable.submitEdition()

    expect(mockShowError).toHaveBeenCalledTimes(1)
    const errorMsg = mockShowError.mock.calls[0]![0] as string
    expect(errorMsg).toContain('Champ A')
    expect(errorMsg).toContain('Champ B')
    expect(errorMsg).toContain('Champ C')
  })

  it('submitEdition does nothing when component is null', async () => {
    const composable = await createAndHydrate()

    // Force component to null
    composable.component.value = null

    await composable.submitEdition()

    expect(mockUpdateComposant).not.toHaveBeenCalled()
    expect(mockSaveAll).not.toHaveBeenCalled()
  })
})
