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

import { useEntityTypes, invalidateEntityTypeCache } from '~/composables/useEntityTypes'

// ---------------------------------------------------------------------------
// Mocks
// ---------------------------------------------------------------------------

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(),
  }),
}))

const mockListModelTypes = vi.fn()
const mockCreateModelType = vi.fn()
const mockUpdateModelType = vi.fn()
const mockDeleteModelType = vi.fn()

vi.mock('~/services/modelTypes', () => ({
  listModelTypes: (...args: any[]) => mockListModelTypes(...args),
  createModelType: (...args: any[]) => mockCreateModelType(...args),
  updateModelType: (...args: any[]) => mockUpdateModelType(...args),
  deleteModelType: (...args: any[]) => mockDeleteModelType(...args),
}))

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

const fakeItem = (id: string, name: string) => ({
  id,
  name,
  code: name.toLowerCase(),
  category: 'COMPONENT' as const,
  structure: null,
  createdAt: '2025-01-01',
  updatedAt: '2025-01-01',
  notes: null,
  description: null,
})

function resetState() {
  // Reset singleton state via public APIs
  const comp = useEntityTypes({ category: 'COMPONENT', label: 'composant' })
  comp.types.value = []
  invalidateEntityTypeCache('COMPONENT')

  const piece = useEntityTypes({ category: 'PIECE', label: 'pièce' })
  piece.types.value = []
  invalidateEntityTypeCache('PIECE')

  const product = useEntityTypes({ category: 'PRODUCT', label: 'produit' })
  product.types.value = []
  invalidateEntityTypeCache('PRODUCT')
}

beforeEach(() => {
  vi.clearAllMocks()
  resetState()
})

// ---------------------------------------------------------------------------
// loadTypes
// ---------------------------------------------------------------------------
describe('loadTypes', () => {
  it('fetches from API and stores in types', async () => {
    mockListModelTypes.mockResolvedValue({
      items: [fakeItem('1', 'Type A'), fakeItem('2', 'Type B')],
      total: 2,
      offset: 0,
      limit: 200,
    })

    const { loadTypes, types } = useEntityTypes({ category: 'COMPONENT', label: 'composant' })
    const result = await loadTypes()

    expect(result.success).toBe(true)
    expect(types.value).toHaveLength(2)
    expect(types.value[0].name).toBe('Type A')
    expect(mockListModelTypes).toHaveBeenCalledOnce()
    expect(mockListModelTypes).toHaveBeenCalledWith({
      category: 'COMPONENT',
      sort: 'name',
      dir: 'asc',
      limit: 200,
    })
  })

  it('returns cached data on second call without force', async () => {
    mockListModelTypes.mockResolvedValue({
      items: [fakeItem('1', 'Cached')],
      total: 1,
      offset: 0,
      limit: 200,
    })

    const { loadTypes, types } = useEntityTypes({ category: 'COMPONENT', label: 'composant' })
    await loadTypes()
    expect(mockListModelTypes).toHaveBeenCalledTimes(1)

    // Second call — should return cache
    const result = await loadTypes()
    expect(result.success).toBe(true)
    expect(types.value).toHaveLength(1)
    expect(mockListModelTypes).toHaveBeenCalledTimes(1) // NOT called again
  })

  it('refetches with force: true', async () => {
    mockListModelTypes
      .mockResolvedValueOnce({ items: [fakeItem('1', 'Old')], total: 1, offset: 0, limit: 200 })
      .mockResolvedValueOnce({ items: [fakeItem('1', 'New')], total: 1, offset: 0, limit: 200 })

    const { loadTypes, types } = useEntityTypes({ category: 'COMPONENT', label: 'composant' })
    await loadTypes()
    expect(types.value[0].name).toBe('Old')

    await loadTypes({ force: true })
    expect(types.value[0].name).toBe('New')
    expect(mockListModelTypes).toHaveBeenCalledTimes(2)
  })

  it('sets loading during fetch', async () => {
    let resolvePromise: (value: any) => void
    mockListModelTypes.mockReturnValue(new Promise((resolve) => { resolvePromise = resolve }))

    const { loadTypes, loading } = useEntityTypes({ category: 'COMPONENT', label: 'composant' })
    expect(loading.value).toBe(false)

    const promise = loadTypes()
    expect(loading.value).toBe(true)

    resolvePromise!({ items: [], total: 0, offset: 0, limit: 200 })
    await promise
    expect(loading.value).toBe(false)
  })

  it('shows error on API failure', async () => {
    mockListModelTypes.mockRejectedValue(new Error('Network error'))

    const { loadTypes, types } = useEntityTypes({ category: 'COMPONENT', label: 'composant' })
    const result = await loadTypes()

    expect(result.success).toBe(false)
    expect(result.error).toBe('Network error')
    expect(types.value).toEqual([])
    expect(mockShowError).toHaveBeenCalledWith(
      'Impossible de charger les types de composant.',
    )
  })

  it('normalizes items with description fallback', async () => {
    mockListModelTypes.mockResolvedValue({
      items: [{ ...fakeItem('1', 'Test'), notes: 'From notes', description: null }],
      total: 1,
      offset: 0,
      limit: 200,
    })

    const { loadTypes, types } = useEntityTypes({ category: 'COMPONENT', label: 'composant' })
    await loadTypes()

    expect(types.value[0].description).toBe('From notes')
  })
})

// ---------------------------------------------------------------------------
// createType
// ---------------------------------------------------------------------------
describe('createType', () => {
  it('creates and pushes to types array', async () => {
    mockCreateModelType.mockResolvedValue(fakeItem('new-1', 'Created'))

    const { createType, types } = useEntityTypes({ category: 'COMPONENT', label: 'composant' })
    const result = await createType({ name: 'Created' })

    expect(result.success).toBe(true)
    expect(types.value).toHaveLength(1)
    expect(types.value[0].name).toBe('Created')
    expect(mockShowSuccess).toHaveBeenCalledWith(expect.stringContaining('Created'))
  })

  it('sends correct payload with auto-generated code', async () => {
    mockCreateModelType.mockResolvedValue(fakeItem('new-1', 'Ma Catégorie'))

    const { createType } = useEntityTypes({ category: 'COMPONENT', label: 'composant' })
    await createType({ name: 'Ma Catégorie' })

    expect(mockCreateModelType).toHaveBeenCalledWith(
      expect.objectContaining({
        name: 'Ma Catégorie',
        code: 'ma-categorie',
        category: 'COMPONENT',
      }),
    )
  })

  it('uses provided code if available', async () => {
    mockCreateModelType.mockResolvedValue(fakeItem('new-1', 'Test'))

    const { createType } = useEntityTypes({ category: 'COMPONENT', label: 'composant' })
    await createType({ name: 'Test', code: 'custom-code' })

    expect(mockCreateModelType).toHaveBeenCalledWith(
      expect.objectContaining({ code: 'custom-code' }),
    )
  })

  it('shows error on API failure without modifying types', async () => {
    mockCreateModelType.mockRejectedValue(new Error('Create failed'))

    const { createType, types } = useEntityTypes({ category: 'COMPONENT', label: 'composant' })
    const result = await createType({ name: 'Fail' })

    expect(result.success).toBe(false)
    expect(types.value).toEqual([])
    expect(mockShowError).toHaveBeenCalled()
  })
})

// ---------------------------------------------------------------------------
// updateType
// ---------------------------------------------------------------------------
describe('updateType', () => {
  it('updates the existing item in types array', async () => {
    // Pre-populate
    mockListModelTypes.mockResolvedValue({
      items: [fakeItem('1', 'Original')],
      total: 1,
      offset: 0,
      limit: 200,
    })
    const { loadTypes, updateType, types } = useEntityTypes({ category: 'COMPONENT', label: 'composant' })
    await loadTypes()

    mockUpdateModelType.mockResolvedValue(fakeItem('1', 'Updated'))
    const result = await updateType('1', { name: 'Updated' })

    expect(result.success).toBe(true)
    expect(types.value).toHaveLength(1)
    expect(types.value[0].name).toBe('Updated')
    expect(mockShowSuccess).toHaveBeenCalledWith(expect.stringContaining('Updated'))
  })

  it('shows error on API failure without modifying types', async () => {
    mockListModelTypes.mockResolvedValue({
      items: [fakeItem('1', 'Original')],
      total: 1,
      offset: 0,
      limit: 200,
    })
    const { loadTypes, updateType, types } = useEntityTypes({ category: 'COMPONENT', label: 'composant' })
    await loadTypes()

    mockUpdateModelType.mockRejectedValue(new Error('Update failed'))
    const result = await updateType('1', { name: 'Bad' })

    expect(result.success).toBe(false)
    expect(types.value[0].name).toBe('Original')
    expect(mockShowError).toHaveBeenCalled()
  })
})

// ---------------------------------------------------------------------------
// deleteType
// ---------------------------------------------------------------------------
describe('deleteType', () => {
  it('removes item from types array', async () => {
    mockListModelTypes.mockResolvedValue({
      items: [fakeItem('1', 'A'), fakeItem('2', 'B')],
      total: 2,
      offset: 0,
      limit: 200,
    })
    const { loadTypes, deleteType, types } = useEntityTypes({ category: 'COMPONENT', label: 'composant' })
    await loadTypes()
    expect(types.value).toHaveLength(2)

    mockDeleteModelType.mockResolvedValue(undefined)
    const result = await deleteType('1')

    expect(result.success).toBe(true)
    expect(types.value).toHaveLength(1)
    expect(types.value[0].id).toBe('2')
    expect(mockShowSuccess).toHaveBeenCalled()
  })

  it('shows error on API failure without removing item', async () => {
    mockListModelTypes.mockResolvedValue({
      items: [fakeItem('1', 'Keep')],
      total: 1,
      offset: 0,
      limit: 200,
    })
    const { loadTypes, deleteType, types } = useEntityTypes({ category: 'COMPONENT', label: 'composant' })
    await loadTypes()

    mockDeleteModelType.mockRejectedValue(new Error('Delete failed'))
    const result = await deleteType('1')

    expect(result.success).toBe(false)
    expect(types.value).toHaveLength(1)
    expect(mockShowError).toHaveBeenCalled()
  })
})

// ---------------------------------------------------------------------------
// invalidateEntityTypeCache
// ---------------------------------------------------------------------------
describe('invalidateEntityTypeCache', () => {
  it('forces next loadTypes to refetch', async () => {
    mockListModelTypes.mockResolvedValue({
      items: [fakeItem('1', 'Cached')],
      total: 1,
      offset: 0,
      limit: 200,
    })

    const { loadTypes } = useEntityTypes({ category: 'COMPONENT', label: 'composant' })
    await loadTypes()
    expect(mockListModelTypes).toHaveBeenCalledTimes(1)

    // Without invalidation, wouldn't refetch
    invalidateEntityTypeCache('COMPONENT')

    mockListModelTypes.mockResolvedValue({
      items: [fakeItem('1', 'Fresh')],
      total: 1,
      offset: 0,
      limit: 200,
    })

    const { loadTypes: loadAgain, types } = useEntityTypes({ category: 'COMPONENT', label: 'composant' })
    await loadAgain()
    expect(mockListModelTypes).toHaveBeenCalledTimes(2)
    expect(types.value[0].name).toBe('Fresh')
  })

  it('does not crash for unknown category', () => {
    expect(() => invalidateEntityTypeCache('COMPONENT')).not.toThrow()
  })
})

// ---------------------------------------------------------------------------
// Singleton per category
// ---------------------------------------------------------------------------
describe('singleton per category', () => {
  it('shares state between same category calls', async () => {
    mockListModelTypes.mockResolvedValue({
      items: [fakeItem('1', 'Shared')],
      total: 1,
      offset: 0,
      limit: 200,
    })

    const a = useEntityTypes({ category: 'COMPONENT', label: 'composant' })
    await a.loadTypes()

    const b = useEntityTypes({ category: 'COMPONENT', label: 'composant' })
    expect(b.types.value).toHaveLength(1)
    expect(b.types.value[0].name).toBe('Shared')
  })

  it('isolates state between different categories', async () => {
    mockListModelTypes
      .mockResolvedValueOnce({ items: [fakeItem('c1', 'Component')], total: 1, offset: 0, limit: 200 })
      .mockResolvedValueOnce({ items: [fakeItem('p1', 'Piece')], total: 1, offset: 0, limit: 200 })

    const comp = useEntityTypes({ category: 'COMPONENT', label: 'composant' })
    const piece = useEntityTypes({ category: 'PIECE', label: 'pièce' })

    await comp.loadTypes()
    await piece.loadTypes()

    expect(comp.types.value).toHaveLength(1)
    expect(comp.types.value[0].name).toBe('Component')
    expect(piece.types.value).toHaveLength(1)
    expect(piece.types.value[0].name).toBe('Piece')
  })

  it('invalidateEntityTypeCache only affects target category', async () => {
    mockListModelTypes
      .mockResolvedValueOnce({ items: [fakeItem('c1', 'Comp')], total: 1, offset: 0, limit: 200 })
      .mockResolvedValueOnce({ items: [fakeItem('p1', 'Piece')], total: 1, offset: 0, limit: 200 })

    const comp = useEntityTypes({ category: 'COMPONENT', label: 'composant' })
    const piece = useEntityTypes({ category: 'PIECE', label: 'pièce' })
    await comp.loadTypes()
    await piece.loadTypes()
    expect(mockListModelTypes).toHaveBeenCalledTimes(2)

    // Invalidate only COMPONENT
    invalidateEntityTypeCache('COMPONENT')

    // PIECE should still use cache
    await piece.loadTypes()
    expect(mockListModelTypes).toHaveBeenCalledTimes(2) // No extra call

    // COMPONENT should refetch
    mockListModelTypes.mockResolvedValueOnce({ items: [fakeItem('c1', 'Refreshed')], total: 1, offset: 0, limit: 200 })
    await comp.loadTypes()
    expect(mockListModelTypes).toHaveBeenCalledTimes(3)
  })
})
