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

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

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

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

// ---------------------------------------------------------------------------
// 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 (createComposant)
// ---------------------------------------------------------------------------

const mockCreateComposant = vi.fn()

vi.mock('~/composables/useComposants', () => ({
  useComposants: () => ({
    createComposant: mockCreateComposant,
    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 (uploadDocuments)
// ---------------------------------------------------------------------------

const mockUploadDocuments = vi.fn()

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

// ---------------------------------------------------------------------------
// Mocks — useConstructeurLinks (syncLinks)
// ---------------------------------------------------------------------------

const mockSyncLinks = vi.fn().mockResolvedValue(undefined)

vi.mock('~/composables/useConstructeurLinks', () => ({
  useConstructeurLinks: () => ({
    fetchLinks: vi.fn().mockResolvedValue([]),
    syncLinks: mockSyncLinks,
  }),
}))

// ---------------------------------------------------------------------------
// Mocks — useCustomFieldInputs (saveAll)
// ---------------------------------------------------------------------------

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)
// ---------------------------------------------------------------------------

// usePermissions is Nuxt auto-imported (no explicit import in source),
// so we stub it as a global function.
vi.stubGlobal('usePermissions', () => ({
  canEdit: { value: true },
  canManage: { value: true },
  isAdmin: { value: false },
  isGranted: () => true,
}))

// ---------------------------------------------------------------------------
// Mocks — useConstructeurs (used by useComposants internally)
// ---------------------------------------------------------------------------

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

// ---------------------------------------------------------------------------
// Mocks — shared utils that touch structure
// ---------------------------------------------------------------------------

const mockHasAssignments = vi.fn().mockReturnValue(false)
const mockSerializeStructureAssignments = vi.fn().mockReturnValue(null)
const mockIsAssignmentNodeComplete = vi.fn().mockReturnValue(true)

vi.mock('~/shared/utils/structureAssignmentHelpers', () => ({
  hasAssignments: (...args: any[]) => mockHasAssignments(...args),
  initializeStructureAssignments: () => null,
  isAssignmentNodeComplete: (...args: any[]) => mockIsAssignmentNodeComplete(...args),
  serializeStructureAssignments: (...args: any[]) => mockSerializeStructureAssignments(...args),
}))

vi.mock('~/shared/utils/structureDisplayUtils', () => ({
  getStructurePieces: () => [],
  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/utils/errorMessages', () => ({
  humanizeError: (msg: string) => msg,
}))

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

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

/** A minimal ModelType matching the `COMPONENT` category filter. */
const mockModelType = {
  id: 'tc-moteur',
  name: 'Moteur électrique',
  category: 'COMPONENT',
  structure: null,
}

beforeEach(() => {
  vi.clearAllMocks()
  // Provide at least one COMPONENT type so selectedType resolves
  mockComponentTypes.value = [mockModelType]
})

// ---------------------------------------------------------------------------
// submitCreation — payload completeness
// ---------------------------------------------------------------------------

describe('submitCreation — payload completeness', () => {
  it('includes all form fields in createComposant payload', async () => {
    const createdComp = { id: 'comp-new-001', name: 'Moteur principal' }
    mockCreateComposant.mockResolvedValue({ success: true, data: createdComp })

    const composable = useComponentCreate()

    // Select a type
    composable.selectedTypeId.value = 'tc-moteur'
    // Wait a tick so watchers fire
    await new Promise(r => setTimeout(r, 0))

    // Fill form fields
    composable.creationForm.name = 'Moteur principal'
    composable.creationForm.description = 'Un moteur triphasé'
    composable.creationForm.reference = 'MOT-001'
    composable.creationForm.prix = '1500'

    await composable.submitCreation()

    expect(mockCreateComposant).toHaveBeenCalledTimes(1)
    const payload = mockCreateComposant.mock.calls[0]![0]
    expect(payload).toMatchObject({
      name: 'Moteur principal',
      description: 'Un moteur triphasé',
      reference: 'MOT-001',
      prix: '1500',
      typeComposantId: 'tc-moteur',
    })
  })

  it('saves custom fields after component creation (saveAll is called)', async () => {
    const createdComp = { id: 'comp-cf-001', name: 'Composant CF' }
    mockCreateComposant.mockResolvedValue({ success: true, data: createdComp })

    const composable = useComponentCreate()
    composable.selectedTypeId.value = 'tc-moteur'
    await new Promise(r => setTimeout(r, 0))

    composable.creationForm.name = 'Composant CF'

    await composable.submitCreation()

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

  it('syncs constructeur links after creation with correct entity type and ID', async () => {
    const createdComp = { id: 'comp-link-001', name: 'Composant Links' }
    mockCreateComposant.mockResolvedValue({ success: true, data: createdComp })

    const composable = useComponentCreate()
    composable.selectedTypeId.value = 'tc-moteur'
    await new Promise(r => setTimeout(r, 0))

    composable.creationForm.name = 'Composant Links'
    // Add constructeur links
    composable.constructeurLinks.value = [mockLinkSKF, mockLinkFAG]

    await composable.submitCreation()

    expect(mockSyncLinks).toHaveBeenCalledTimes(1)
    expect(mockSyncLinks).toHaveBeenCalledWith(
      'composant',
      'comp-link-001',
      [],
      [mockLinkSKF, mockLinkFAG],
    )
  })

  it('uploads documents with correct composantId context', async () => {
    const createdComp = { id: 'comp-doc-001', name: 'Composant Docs' }
    mockCreateComposant.mockResolvedValue({ success: true, data: createdComp })
    mockUploadDocuments.mockResolvedValue({ success: true, data: [] })

    const composable = useComponentCreate()
    composable.selectedTypeId.value = 'tc-moteur'
    await new Promise(r => setTimeout(r, 0))

    composable.creationForm.name = 'Composant Docs'

    // Simulate selected documents
    const fakeFile = new File(['content'], 'schema.pdf', { type: 'application/pdf' })
    composable.selectedDocuments.value = [fakeFile]

    await composable.submitCreation()

    expect(mockUploadDocuments).toHaveBeenCalledTimes(1)
    expect(mockUploadDocuments).toHaveBeenCalledWith(
      {
        files: [fakeFile],
        context: { composantId: 'comp-doc-001' },
      },
      { updateStore: false },
    )
  })

  it('does not crash with zero constructeurs', async () => {
    const createdComp = { id: 'comp-no-cstr', name: 'Composant Simple' }
    mockCreateComposant.mockResolvedValue({ success: true, data: createdComp })

    const composable = useComponentCreate()
    composable.selectedTypeId.value = 'tc-moteur'
    await new Promise(r => setTimeout(r, 0))

    composable.creationForm.name = 'Composant Simple'
    // Ensure no constructeur links
    composable.constructeurLinks.value = []

    await composable.submitCreation()

    expect(mockCreateComposant).toHaveBeenCalledTimes(1)
    expect(mockSyncLinks).not.toHaveBeenCalled()
    expect(mockShowSuccess).toHaveBeenCalledWith('Composant créé avec succès')
  })
})

// ---------------------------------------------------------------------------
// Structure serialization in payload
// ---------------------------------------------------------------------------

describe('submitCreation — structure serialization in payload', () => {
  it('includes structure key with serialized data when assignments exist', async () => {
    const createdComp = { id: 'comp-struct-001', name: 'Composant Structure' }
    mockCreateComposant.mockResolvedValue({ success: true, data: createdComp })

    const fakeSerializedStructure = {
      path: 'root',
      definition: { typeComposantId: 'tc-moteur' },
      pieces: [{ path: 'root:piece-0', definition: { typePieceId: 'tp-001' }, selectedPieceId: 'piece-abc' }],
    }

    mockHasAssignments.mockReturnValue(true)
    mockSerializeStructureAssignments.mockReturnValue(fakeSerializedStructure)

    const composable = useComponentCreate()
    composable.selectedTypeId.value = 'tc-moteur'
    await new Promise(r => setTimeout(r, 0))

    composable.creationForm.name = 'Composant Structure'
    // Set a non-null structureAssignments so the composable considers it present
    composable.structureAssignments.value = {
      path: 'root',
      definition: {} as any,
      selectedComponentId: '',
      pieces: [{ path: 'root:piece-0', definition: {} as any, selectedPieceId: 'piece-abc' }],
      products: [],
      subcomponents: [],
    }

    await composable.submitCreation()

    expect(mockCreateComposant).toHaveBeenCalledTimes(1)
    const payload = mockCreateComposant.mock.calls[0]![0]
    expect(payload.structure).toEqual(fakeSerializedStructure)
  })

  it('does not include structure key when no assignments exist', async () => {
    const createdComp = { id: 'comp-nostruct-001', name: 'Composant No Structure' }
    mockCreateComposant.mockResolvedValue({ success: true, data: createdComp })

    // Reset to default: no assignments
    mockHasAssignments.mockReturnValue(false)
    mockSerializeStructureAssignments.mockReturnValue(null)

    const composable = useComponentCreate()
    composable.selectedTypeId.value = 'tc-moteur'
    await new Promise(r => setTimeout(r, 0))

    composable.creationForm.name = 'Composant No Structure'

    await composable.submitCreation()

    expect(mockCreateComposant).toHaveBeenCalledTimes(1)
    const payload = mockCreateComposant.mock.calls[0]![0]
    expect(payload.structure).toBeUndefined()
  })

  it('does not include structure key when serializeStructureAssignments returns null', async () => {
    const createdComp = { id: 'comp-sernull-001', name: 'Composant Serialize Null' }
    mockCreateComposant.mockResolvedValue({ success: true, data: createdComp })

    mockHasAssignments.mockReturnValue(true)
    mockSerializeStructureAssignments.mockReturnValue(null)

    const composable = useComponentCreate()
    composable.selectedTypeId.value = 'tc-moteur'
    await new Promise(r => setTimeout(r, 0))

    composable.creationForm.name = 'Composant Serialize Null'
    composable.structureAssignments.value = {
      path: 'root',
      definition: {} as any,
      selectedComponentId: '',
      pieces: [],
      products: [],
      subcomponents: [],
    }

    await composable.submitCreation()

    expect(mockCreateComposant).toHaveBeenCalledTimes(1)
    const payload = mockCreateComposant.mock.calls[0]![0]
    expect(payload.structure).toBeUndefined()
  })
})

// ---------------------------------------------------------------------------
// Prix / reference null handling
// ---------------------------------------------------------------------------

describe('submitCreation — prix and reference null handling', () => {
  it('does not send prix when prix is an empty string', async () => {
    const createdComp = { id: 'comp-noprix-001', name: 'Composant No Prix' }
    mockCreateComposant.mockResolvedValue({ success: true, data: createdComp })

    // Reset structure mocks to default
    mockHasAssignments.mockReturnValue(false)
    mockSerializeStructureAssignments.mockReturnValue(null)

    const composable = useComponentCreate()
    composable.selectedTypeId.value = 'tc-moteur'
    await new Promise(r => setTimeout(r, 0))

    composable.creationForm.name = 'Composant No Prix'
    composable.creationForm.prix = ''

    await composable.submitCreation()

    const payload = mockCreateComposant.mock.calls[0]![0]
    expect(payload).not.toHaveProperty('prix')
  })

  it('does not send prix when prix is non-numeric (avoids NaN)', async () => {
    const createdComp = { id: 'comp-nanprix-001', name: 'Composant NaN Prix' }
    mockCreateComposant.mockResolvedValue({ success: true, data: createdComp })

    mockHasAssignments.mockReturnValue(false)
    mockSerializeStructureAssignments.mockReturnValue(null)

    const composable = useComponentCreate()
    composable.selectedTypeId.value = 'tc-moteur'
    await new Promise(r => setTimeout(r, 0))

    composable.creationForm.name = 'Composant NaN Prix'
    composable.creationForm.prix = 'not-a-number'

    await composable.submitCreation()

    const payload = mockCreateComposant.mock.calls[0]![0]
    expect(payload).not.toHaveProperty('prix')
  })

  it('sends prix as stringified number when valid numeric string', async () => {
    const createdComp = { id: 'comp-validprix-001', name: 'Composant Valid Prix' }
    mockCreateComposant.mockResolvedValue({ success: true, data: createdComp })

    mockHasAssignments.mockReturnValue(false)
    mockSerializeStructureAssignments.mockReturnValue(null)

    const composable = useComponentCreate()
    composable.selectedTypeId.value = 'tc-moteur'
    await new Promise(r => setTimeout(r, 0))

    composable.creationForm.name = 'Composant Valid Prix'
    composable.creationForm.prix = '  42.5  '

    await composable.submitCreation()

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

  it('does not send reference when reference is an empty string', async () => {
    const createdComp = { id: 'comp-noref-001', name: 'Composant No Ref' }
    mockCreateComposant.mockResolvedValue({ success: true, data: createdComp })

    mockHasAssignments.mockReturnValue(false)
    mockSerializeStructureAssignments.mockReturnValue(null)

    const composable = useComponentCreate()
    composable.selectedTypeId.value = 'tc-moteur'
    await new Promise(r => setTimeout(r, 0))

    composable.creationForm.name = 'Composant No Ref'
    composable.creationForm.reference = ''

    await composable.submitCreation()

    const payload = mockCreateComposant.mock.calls[0]![0]
    expect(payload).not.toHaveProperty('reference')
  })

  it('does not send reference when reference is whitespace only', async () => {
    const createdComp = { id: 'comp-wsref-001', name: 'Composant WS Ref' }
    mockCreateComposant.mockResolvedValue({ success: true, data: createdComp })

    mockHasAssignments.mockReturnValue(false)
    mockSerializeStructureAssignments.mockReturnValue(null)

    const composable = useComponentCreate()
    composable.selectedTypeId.value = 'tc-moteur'
    await new Promise(r => setTimeout(r, 0))

    composable.creationForm.name = 'Composant WS Ref'
    composable.creationForm.reference = '   '

    await composable.submitCreation()

    const payload = mockCreateComposant.mock.calls[0]![0]
    expect(payload).not.toHaveProperty('reference')
  })
})

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

describe('submitCreation — error paths', () => {
  beforeEach(() => {
    mockHasAssignments.mockReturnValue(false)
    mockSerializeStructureAssignments.mockReturnValue(null)
  })

  it('does not save custom fields when createComposant returns success: false', async () => {
    mockCreateComposant.mockResolvedValue({ success: false, error: 'Duplicate name' })

    const composable = useComponentCreate()
    composable.selectedTypeId.value = 'tc-moteur'
    await new Promise(r => setTimeout(r, 0))

    composable.creationForm.name = 'Composant Fail'

    await composable.submitCreation()

    expect(mockCreateComposant).toHaveBeenCalledTimes(1)
    expect(mockSaveAll).not.toHaveBeenCalled()
    expect(mockShowError).toHaveBeenCalledWith('Duplicate name')
  })

  it('shows toast error when createComposant returns success: false with error message', async () => {
    mockCreateComposant.mockResolvedValue({ success: false, error: 'Server validation failed' })

    const composable = useComponentCreate()
    composable.selectedTypeId.value = 'tc-moteur'
    await new Promise(r => setTimeout(r, 0))

    composable.creationForm.name = 'Composant Error'

    await composable.submitCreation()

    expect(mockShowError).toHaveBeenCalledWith('Server validation failed')
    expect(mockShowSuccess).not.toHaveBeenCalled()
  })

  it('shows warning for failed custom fields but still navigates (composant exists)', async () => {
    const createdComp = { id: 'comp-cf-warn-001', name: 'Composant CF Warn' }
    mockCreateComposant.mockResolvedValue({ success: true, data: createdComp })
    mockSaveAll.mockResolvedValue(['Tension nominale', 'Certifié CE'])

    const composable = useComponentCreate()
    composable.selectedTypeId.value = 'tc-moteur'
    await new Promise(r => setTimeout(r, 0))

    composable.creationForm.name = 'Composant CF Warn'

    await composable.submitCreation()

    // Custom field error toast is shown
    expect(mockShowError).toHaveBeenCalledWith(
      'Erreur sur les champs : Tension nominale, Certifié CE',
    )
    // But creation success toast is also shown (composant was created)
    expect(mockShowSuccess).toHaveBeenCalledWith('Composant créé avec succès')
  })

  it('catches thrown exceptions and shows humanized error', async () => {
    mockCreateComposant.mockRejectedValue(new Error('Network timeout'))

    const composable = useComponentCreate()
    composable.selectedTypeId.value = 'tc-moteur'
    await new Promise(r => setTimeout(r, 0))

    composable.creationForm.name = 'Composant Throw'

    await composable.submitCreation()

    expect(mockShowError).toHaveBeenCalledWith('Network timeout')
    expect(mockSaveAll).not.toHaveBeenCalled()
  })

  it('resets submitting flag after failure', async () => {
    mockCreateComposant.mockResolvedValue({ success: false, error: 'fail' })

    const composable = useComponentCreate()
    composable.selectedTypeId.value = 'tc-moteur'
    await new Promise(r => setTimeout(r, 0))

    composable.creationForm.name = 'Composant Reset Flag'

    await composable.submitCreation()

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

// ---------------------------------------------------------------------------
// ProductId from structure
// ---------------------------------------------------------------------------

describe('submitCreation — productId from structure', () => {
  beforeEach(() => {
    mockHasAssignments.mockReturnValue(false)
    mockSerializeStructureAssignments.mockReturnValue(null)
  })

  it('includes productId in payload when root product selection exists', async () => {
    const createdComp = { id: 'comp-prodid-001', name: 'Composant ProductId' }
    mockCreateComposant.mockResolvedValue({ success: true, data: createdComp })

    const composable = useComponentCreate()
    composable.selectedTypeId.value = 'tc-moteur'
    await new Promise(r => setTimeout(r, 0))

    composable.creationForm.name = 'Composant ProductId'
    // Set structure assignments with a root product selection
    composable.structureAssignments.value = {
      path: 'root',
      definition: {} as any,
      selectedComponentId: '',
      pieces: [],
      products: [
        {
          path: 'root:product-0',
          definition: { typeProductId: 'tprod-001' } as any,
          selectedProductId: 'prod-selected-123',
        },
      ],
      subcomponents: [],
    }

    await composable.submitCreation()

    const payload = mockCreateComposant.mock.calls[0]![0]
    expect(payload.productId).toBe('prod-selected-123')
  })

  it('does not include productId when no root product is selected', async () => {
    const createdComp = { id: 'comp-noprodid-001', name: 'Composant No ProductId' }
    mockCreateComposant.mockResolvedValue({ success: true, data: createdComp })

    const composable = useComponentCreate()
    composable.selectedTypeId.value = 'tc-moteur'
    await new Promise(r => setTimeout(r, 0))

    composable.creationForm.name = 'Composant No ProductId'
    composable.structureAssignments.value = {
      path: 'root',
      definition: {} as any,
      selectedComponentId: '',
      pieces: [],
      products: [],
      subcomponents: [],
    }

    await composable.submitCreation()

    const payload = mockCreateComposant.mock.calls[0]![0]
    expect(payload).not.toHaveProperty('productId')
  })

  it('does not include productId when product selection is empty string', async () => {
    const createdComp = { id: 'comp-emptyprod-001', name: 'Composant Empty Product' }
    mockCreateComposant.mockResolvedValue({ success: true, data: createdComp })

    const composable = useComponentCreate()
    composable.selectedTypeId.value = 'tc-moteur'
    await new Promise(r => setTimeout(r, 0))

    composable.creationForm.name = 'Composant Empty Product'
    composable.structureAssignments.value = {
      path: 'root',
      definition: {} as any,
      selectedComponentId: '',
      pieces: [],
      products: [
        {
          path: 'root:product-0',
          definition: { typeProductId: 'tprod-001' } as any,
          selectedProductId: '',
        },
      ],
      subcomponents: [],
    }

    await composable.submitCreation()

    const payload = mockCreateComposant.mock.calls[0]![0]
    expect(payload).not.toHaveProperty('productId')
  })
})
