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

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

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

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

// ---------------------------------------------------------------------------
// 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 — usePieces (updatePiece)
// ---------------------------------------------------------------------------

const mockUpdatePiece = vi.fn()

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

// ---------------------------------------------------------------------------
// Mocks — usePieceTypes
// ---------------------------------------------------------------------------

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

vi.mock('~/composables/usePieceTypes', () => ({
  usePieceTypes: () => ({
    pieceTypes: mockPieceTypes,
    loadPieceTypes: mockLoadPieceTypes,
  }),
}))

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

vi.mock('~/composables/useDocuments', () => ({
  useDocuments: () => ({
    loadDocumentsByPiece: vi.fn().mockResolvedValue({ success: true, data: [] }),
    uploadDocuments: vi.fn().mockResolvedValue({ success: true, data: [] }),
    deleteDocument: vi.fn().mockResolvedValue({ success: true }),
    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/modelUtils', () => ({
  formatPieceStructurePreview: () => '',
}))

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

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

vi.mock('~/services/modelTypes', () => ({
  getModelType: vi.fn().mockResolvedValue(null),
}))

vi.mock('~/shared/apiRelations', () => ({
  extractRelationId: (rel: any) => {
    if (typeof rel === 'string') return rel
    if (rel && typeof rel === 'object' && 'id' in rel) return rel.id
    return null
  },
}))

// ---------------------------------------------------------------------------
// Test data
// ---------------------------------------------------------------------------

const PIECE_ID = 'piece-001'

const mockPieceType = {
  id: 'tp-bearing-001',
  name: 'Roulement',
  code: 'ROUL',
  category: 'PIECE',
  structure: {
    products: [
      {
        typeProductId: 'tprod-grease-001',
        typeProductLabel: 'Graisse SKF',
        familyCode: 'LUB',
        role: 'lubrification',
      },
    ],
    customFields: [],
  },
}

function buildPieceWithProducts() {
  return {
    ...mockPieceFromApi,
    id: PIECE_ID,
    '@id': `/api/pieces/${PIECE_ID}`,
    description: 'Roulement haute performance',
    prix: '42.50',
    typePieceId: 'tp-bearing-001',
    productIds: ['prod-001'],
  }
}

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

const tick = () => new Promise(r => setTimeout(r, 0))

async function createAndHydrate(overrides?: Record<string, any>) {
  const pieceData = { ...buildPieceWithProducts(), ...overrides }

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

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

  const composable = usePieceEdit(PIECE_ID)

  await composable.fetchPiece()
  await tick()

  return composable
}

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

beforeEach(() => {
  vi.clearAllMocks()
  mockPieceTypes.value = [mockPieceType]
})

// ---------------------------------------------------------------------------
// fetchPiece — hydration
// ---------------------------------------------------------------------------

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

    expect(composable.editionForm.name).toBe('Roulement 6205')
    expect(composable.editionForm.reference).toBe('ROUL-6205')
    expect(composable.editionForm.description).toBe('Roulement haute performance')
    expect(composable.editionForm.prix).toBe('42.50')
  })

  it('loads piece with product slots', async () => {
    const composable = await createAndHydrate()

    expect(composable.piece.value).not.toBeNull()
    expect(composable.piece.value.productSlots).toHaveLength(1)
    expect(composable.piece.value.productSlots[0].product.id).toBe('prod-001')
  })

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

    expect(mockFetchLinks).toHaveBeenCalledWith('piece', PIECE_ID)
    expect(composable.constructeurLinks.value).toHaveLength(2)
    expect(composable.constructeurLinks.value[0].constructeurId).toBe(mockConstructeurSKF.id)
    expect(composable.constructeurLinks.value[1].constructeurId).toBe(mockConstructeurFAG.id)
  })
})

// ---------------------------------------------------------------------------
// Product selections
// ---------------------------------------------------------------------------

describe('product selections', () => {
  it('setProductSelection updates the correct index', async () => {
    const composable = await createAndHydrate()

    // The structure has 1 product requirement, so productSelections should have 1 entry
    composable.setProductSelection(0, 'prod-new-001')
    await tick()

    expect(composable.productSelections.value[0]).toBe('prod-new-001')
  })

  it('setProductSelection to null does not crash', async () => {
    const composable = await createAndHydrate()

    // Set then clear
    composable.setProductSelection(0, 'prod-001')
    await tick()

    composable.setProductSelection(0, null)
    await tick()

    expect(composable.productSelections.value[0]).toBeNull()
  })
})

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

describe('submitEdition — no data loss', () => {
  it('sends all form fields in update payload', async () => {
    mockUpdatePiece.mockResolvedValue({ success: true, data: { id: PIECE_ID } })

    const composable = await createAndHydrate()

    composable.editionForm.name = 'Roulement modifie'
    composable.editionForm.description = 'Nouvelle description'
    composable.editionForm.reference = 'REF-MOD-001'
    composable.editionForm.prix = '99.99'

    // Ensure product selection is filled so submit proceeds
    composable.setProductSelection(0, 'prod-001')
    await tick()

    await composable.submitEdition()

    expect(mockUpdatePiece).toHaveBeenCalledTimes(1)
    const payload = mockUpdatePiece.mock.calls[0]![1]
    expect(payload).toMatchObject({
      name: 'Roulement modifie',
      description: 'Nouvelle description',
      reference: 'REF-MOD-001',
      prix: '99.99',
    })
  })

  it('saves custom fields after piece update', async () => {
    mockUpdatePiece.mockResolvedValue({ success: true, data: { id: PIECE_ID } })

    const composable = await createAndHydrate()
    composable.setProductSelection(0, 'prod-001')
    await tick()

    await composable.submitEdition()

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

  it('syncs constructeur links', async () => {
    mockUpdatePiece.mockResolvedValue({ success: true, data: { id: PIECE_ID } })

    const composable = await createAndHydrate()
    composable.setProductSelection(0, 'prod-001')
    await tick()

    await composable.submitEdition()

    expect(mockSyncLinks).toHaveBeenCalledTimes(1)
    const [entityType, entityId, origLinks, formLinks] = mockSyncLinks.mock.calls[0]!
    expect(entityType).toBe('piece')
    expect(entityId).toBe(PIECE_ID)
    expect(origLinks).toHaveLength(2)
    expect(formLinks).toHaveLength(2)
  })

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

    const composable = await createAndHydrate()

    // Only edit name
    composable.editionForm.name = 'Nouveau nom piece'
    composable.setProductSelection(0, 'prod-001')
    await tick()

    await composable.submitEdition()

    expect(mockUpdatePiece).toHaveBeenCalledTimes(1)
    const payload = mockUpdatePiece.mock.calls[0]![1]
    expect(payload.name).toBe('Nouveau nom piece')

    // syncLinks still called with constructeur links preserved
    expect(mockSyncLinks).toHaveBeenCalledTimes(1)
    const [, , origLinks, formLinks] = mockSyncLinks.mock.calls[0]!
    expect(origLinks).toHaveLength(2)
    expect(formLinks).toHaveLength(2)
    expect(formLinks[0].constructeurId).toBe(mockConstructeurSKF.id)
    expect(formLinks[1].constructeurId).toBe(mockConstructeurFAG.id)
  })

  it('editing name does not lose product slots', async () => {
    mockUpdatePiece.mockResolvedValue({ success: true, data: { id: PIECE_ID } })

    const composable = await createAndHydrate()

    // Set product selection
    composable.setProductSelection(0, 'prod-001')
    await tick()

    // Now edit only name
    composable.editionForm.name = 'Autre nom'
    await tick()

    await composable.submitEdition()

    const payload = mockUpdatePiece.mock.calls[0]![1]
    expect(payload.name).toBe('Autre nom')
    // productIds should still contain the selection
    expect(payload.productIds).toContain('prod-001')
  })

  it('adding a constructeur preserves existing ones', async () => {
    mockUpdatePiece.mockResolvedValue({ success: true, data: { id: PIECE_ID } })

    const composable = await createAndHydrate()
    composable.setProductSelection(0, 'prod-001')
    await tick()

    // Initially has SKF + FAG from fetchLinks
    expect(composable.constructeurLinks.value).toHaveLength(2)

    // Add a third constructeur
    const newLink = {
      linkId: null as string | null,
      constructeurId: 'cstr-new-003',
      constructeur: { id: 'cstr-new-003', name: 'NEW Corp', email: null, phone: null },
      supplierReference: 'NEW-REF-001',
    }
    composable.constructeurLinks.value = [
      ...composable.constructeurLinks.value,
      newLink,
    ]
    await tick()

    await composable.submitEdition()

    expect(mockSyncLinks).toHaveBeenCalledTimes(1)
    const [, , origLinks, formLinks] = mockSyncLinks.mock.calls[0]!
    // Original had 2 (SKF + FAG)
    expect(origLinks).toHaveLength(2)
    // Form now has 3 (SKF + FAG + NEW)
    expect(formLinks).toHaveLength(3)
    expect(formLinks[0].constructeurId).toBe(mockConstructeurSKF.id)
    expect(formLinks[1].constructeurId).toBe(mockConstructeurFAG.id)
    expect(formLinks[2].constructeurId).toBe('cstr-new-003')
  })

  it('sends both productId and productIds in payload', async () => {
    mockUpdatePiece.mockResolvedValue({ success: true, data: { id: PIECE_ID } })

    const composable = await createAndHydrate()

    composable.setProductSelection(0, 'prod-001')
    await tick()

    await composable.submitEdition()

    expect(mockUpdatePiece).toHaveBeenCalledTimes(1)
    const payload = mockUpdatePiece.mock.calls[0]![1]
    expect(payload.productId).toBe('prod-001')
    expect(payload.productIds).toEqual(['prod-001'])
  })

  it('productId is the first product selection when multiple exist', async () => {
    // Override the piece type to have 2 product requirements
    const multiProductType = {
      ...mockPieceType,
      structure: {
        ...mockPieceType.structure,
        products: [
          {
            typeProductId: 'tprod-grease-001',
            typeProductLabel: 'Graisse SKF',
            familyCode: 'LUB',
            role: 'lubrification',
          },
          {
            typeProductId: 'tprod-oil-002',
            typeProductLabel: 'Huile',
            familyCode: 'LUB',
            role: 'lubrification secondaire',
          },
        ],
        customFields: [],
      },
    }
    mockPieceTypes.value = [multiProductType]

    mockUpdatePiece.mockResolvedValue({ success: true, data: { id: PIECE_ID } })

    const composable = await createAndHydrate({
      productIds: ['prod-001', 'prod-002'],
    })

    composable.setProductSelection(0, 'prod-001')
    composable.setProductSelection(1, 'prod-002')
    await tick()

    await composable.submitEdition()

    expect(mockUpdatePiece).toHaveBeenCalledTimes(1)
    const payload = mockUpdatePiece.mock.calls[0]![1]
    expect(payload.productId).toBe('prod-001')
    expect(payload.productIds).toEqual(['prod-001', 'prod-002'])
  })
})

// ---------------------------------------------------------------------------
// submitEdition — null field handling
// ---------------------------------------------------------------------------

describe('submitEdition — null field handling', () => {
  it('empty prix sends null', async () => {
    mockUpdatePiece.mockResolvedValue({ success: true, data: { id: PIECE_ID } })

    const composable = await createAndHydrate()

    composable.editionForm.prix = ''
    composable.setProductSelection(0, 'prod-001')
    await tick()

    await composable.submitEdition()

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

  it('whitespace-only prix sends null', async () => {
    mockUpdatePiece.mockResolvedValue({ success: true, data: { id: PIECE_ID } })

    const composable = await createAndHydrate()

    composable.editionForm.prix = '   '
    composable.setProductSelection(0, 'prod-001')
    await tick()

    await composable.submitEdition()

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

  it('empty reference sends null', async () => {
    mockUpdatePiece.mockResolvedValue({ success: true, data: { id: PIECE_ID } })

    const composable = await createAndHydrate()

    composable.editionForm.reference = ''
    composable.setProductSelection(0, 'prod-001')
    await tick()

    await composable.submitEdition()

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

  it('valid prix is sent as string number', async () => {
    mockUpdatePiece.mockResolvedValue({ success: true, data: { id: PIECE_ID } })

    const composable = await createAndHydrate()

    composable.editionForm.prix = '99.50'
    composable.setProductSelection(0, 'prod-001')
    await tick()

    await composable.submitEdition()

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

// ---------------------------------------------------------------------------
// submitEdition — error paths
// ---------------------------------------------------------------------------

describe('submitEdition — error paths', () => {
  it('does not save custom fields when updatePiece fails', async () => {
    mockUpdatePiece.mockResolvedValue({ success: false, error: 'Server error' })

    const composable = await createAndHydrate()

    composable.setProductSelection(0, 'prod-001')
    await tick()

    await composable.submitEdition()

    expect(mockUpdatePiece).toHaveBeenCalledTimes(1)
    expect(mockSaveAll).not.toHaveBeenCalled()
    expect(mockSyncLinks).not.toHaveBeenCalled()
  })

  it('does not save custom fields when updatePiece throws', async () => {
    mockUpdatePiece.mockRejectedValue(new Error('Network failure'))

    const composable = await createAndHydrate()

    composable.setProductSelection(0, 'prod-001')
    await tick()

    await composable.submitEdition()

    expect(mockUpdatePiece).toHaveBeenCalledTimes(1)
    expect(mockSaveAll).not.toHaveBeenCalled()
    expect(mockSyncLinks).not.toHaveBeenCalled()
    expect(mockShowError).toHaveBeenCalledWith('Network failure')
  })

  it('shows error toast when product selection is not filled', async () => {
    const composable = await createAndHydrate()

    // Clear product selection
    composable.setProductSelection(0, null)
    await tick()

    await composable.submitEdition()

    expect(mockUpdatePiece).not.toHaveBeenCalled()
    expect(mockShowError).toHaveBeenCalledWith('Sélectionnez un produit conforme au squelette.')
  })
})
