import type { Model } from '@shared/data/types/model'
import { act, renderHook } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'

import { usePullReconcileSubmit } from '../usePullReconcileSubmit'

const { reconcileTriggerMock } = vi.hoisted(() => ({
  reconcileTriggerMock: vi.fn()
}))
const useProviderMock = vi.fn()
const updateProviderMock = vi.fn()

vi.mock('@data/hooks/useDataApi', () => ({
  useMutation: () => ({
    trigger: reconcileTriggerMock,
    isLoading: false
  })
}))

vi.mock('@renderer/hooks/useProvider', () => ({
  useProvider: (...args: any[]) => useProviderMock(...args)
}))

vi.mock('../modelSync', () => ({
  toCreateModelDto: (providerId: string, model: Model) => ({
    providerId,
    modelId: model.apiModelId ?? model.id.split('::').at(-1) ?? model.id,
    name: model.name,
    group: model.group
  })
}))

vi.mock('react-i18next', () => ({
  initReactI18next: { type: '3rdParty', init: vi.fn() },
  useTranslation: () => ({
    t: (key: string, values?: Record<string, unknown>) => (values ? `${key}:${JSON.stringify(values)}` : key)
  })
}))

describe('usePullReconcileSubmit', () => {
  beforeEach(() => {
    reconcileTriggerMock.mockReset()
    reconcileTriggerMock.mockResolvedValue([])
    useProviderMock.mockReset()
    updateProviderMock.mockReset()
    updateProviderMock.mockResolvedValue(undefined)
    useProviderMock.mockReturnValue({
      provider: { id: 'cherryin', isEnabled: true },
      updateProvider: updateProviderMock
    })
    window.toast = {
      success: vi.fn(),
      error: vi.fn(),
      warning: vi.fn()
    } as unknown as typeof window.toast
  })

  it('applies pull reconcile through a single reconcile call with the full diff', async () => {
    const onApplyCommitted = vi.fn()
    const { result } = renderHook(() => usePullReconcileSubmit({ providerId: 'cherryin', onApplyCommitted }))
    const toAdd = Array.from(
      { length: 3 },
      (_, index): Model =>
        ({
          id: `cherryin::model-${index}`,
          providerId: 'cherryin',
          apiModelId: `model-${index}`,
          name: `Model ${index}`,
          isEnabled: true,
          isHidden: false
        }) as Model
    )

    await act(async () => {
      await result.current.confirmApply({
        toAdd,
        toRemove: ['cherryin::old-model']
      })
    })

    expect(reconcileTriggerMock).toHaveBeenCalledTimes(1)
    expect(reconcileTriggerMock).toHaveBeenCalledWith({
      params: { providerId: 'cherryin' },
      body: {
        toAdd: [
          { providerId: 'cherryin', modelId: 'model-0', name: 'Model 0', group: undefined },
          { providerId: 'cherryin', modelId: 'model-1', name: 'Model 1', group: undefined },
          { providerId: 'cherryin', modelId: 'model-2', name: 'Model 2', group: undefined }
        ],
        toRemove: ['cherryin::old-model']
      }
    })
    expect(onApplyCommitted).toHaveBeenCalled()
    expect(window.toast.success).toHaveBeenCalled()
    expect(window.toast.error).not.toHaveBeenCalled()
  })

  it('enables a disabled provider when pull reconcile leaves at least one model', async () => {
    useProviderMock.mockReturnValue({
      provider: { id: 'cherryin', isEnabled: false },
      updateProvider: updateProviderMock
    })
    reconcileTriggerMock.mockResolvedValueOnce([{ id: 'cherryin::model-0' }])
    const onApplyCommitted = vi.fn()
    const { result } = renderHook(() => usePullReconcileSubmit({ providerId: 'cherryin', onApplyCommitted }))

    await act(async () => {
      await result.current.confirmApply({
        toAdd: [
          {
            id: 'cherryin::model-0',
            providerId: 'cherryin',
            apiModelId: 'model-0',
            name: 'Model 0',
            capabilities: [],
            supportsStreaming: true,
            isEnabled: true,
            isHidden: false
          } as Model
        ],
        toRemove: []
      })
    })

    expect(updateProviderMock).toHaveBeenCalledWith({ isEnabled: true })
  })

  it('keeps a disabled provider disabled when pull reconcile leaves zero models', async () => {
    useProviderMock.mockReturnValue({
      provider: { id: 'cherryin', isEnabled: false },
      updateProvider: updateProviderMock
    })
    reconcileTriggerMock.mockResolvedValueOnce([])
    const onApplyCommitted = vi.fn()
    const { result } = renderHook(() => usePullReconcileSubmit({ providerId: 'cherryin', onApplyCommitted }))

    await act(async () => {
      await result.current.confirmApply({
        toAdd: [],
        toRemove: ['cherryin::old-model']
      })
    })

    expect(updateProviderMock).not.toHaveBeenCalled()
  })

  it('surfaces reconcile failure without committing the drawer', async () => {
    reconcileTriggerMock.mockRejectedValueOnce(new Error('reconcile failed'))
    const onApplyCommitted = vi.fn()
    const { result } = renderHook(() => usePullReconcileSubmit({ providerId: 'cherryin', onApplyCommitted }))

    await act(async () => {
      await result.current.confirmApply({
        toAdd: [],
        toRemove: ['cherryin::old-model']
      })
    })

    expect(reconcileTriggerMock).toHaveBeenCalled()
    expect(onApplyCommitted).not.toHaveBeenCalled()
    expect(window.toast.error).toHaveBeenCalledWith('settings.models.manage.sync_pull_failed')
  })

  it('keeps the drawer dirty on rejection of a large payload — no partial commit', async () => {
    // Pre-C4 the renderer chunked toAdd via MODELS_BATCH_MAX_ITEMS, so a
    // 2nd-chunk failure left the user with the 1st chunk's adds persisted
    // and a confusing toast. After C4 the renderer sends one atomic POST
    // /providers/:id/models:reconcile; any rejection (mid-server or up-front
    // validation) must roll back to the pre-confirm state and skip the
    // onApplyCommitted callback. This pins the atomic guarantee.
    reconcileTriggerMock.mockRejectedValueOnce(new Error('reconcile rejected'))
    const onApplyCommitted = vi.fn()
    const { result } = renderHook(() => usePullReconcileSubmit({ providerId: 'cherryin', onApplyCommitted }))

    const largeToAdd = Array.from(
      { length: 1200 },
      (_, index): Model =>
        ({
          id: `cherryin::model-${index}`,
          providerId: 'cherryin',
          apiModelId: `model-${index}`,
          name: `Model ${index}`,
          isEnabled: true,
          isHidden: false
        }) as Model
    )

    await act(async () => {
      await result.current.confirmApply({
        toAdd: largeToAdd,
        toRemove: ['cherryin::old-model-1', 'cherryin::old-model-2']
      })
    })

    expect(reconcileTriggerMock).toHaveBeenCalledTimes(1)
    expect(reconcileTriggerMock.mock.calls[0][0]).toMatchObject({
      params: { providerId: 'cherryin' },
      body: {
        toRemove: ['cherryin::old-model-1', 'cherryin::old-model-2']
      }
    })
    expect(reconcileTriggerMock.mock.calls[0][0].body.toAdd).toHaveLength(1200)
    expect(onApplyCommitted).not.toHaveBeenCalled()
    expect(window.toast.success).not.toHaveBeenCalled()
    expect(window.toast.error).toHaveBeenCalledWith('settings.models.manage.sync_pull_failed')
  })

  it('shows a warning toast when models are skipped because they are in use as defaults', async () => {
    // Models in toRemove that still appear in the reconciled response were
    // skipped by the server because they are in use as user defaults.
    reconcileTriggerMock.mockResolvedValueOnce([{ id: 'cherryin::default-model' }])
    const onApplyCommitted = vi.fn()
    const { result } = renderHook(() => usePullReconcileSubmit({ providerId: 'cherryin', onApplyCommitted }))

    await act(async () => {
      await result.current.confirmApply({
        toAdd: [],
        toRemove: ['cherryin::default-model', 'cherryin::other-model']
      })
    })

    expect(onApplyCommitted).toHaveBeenCalled()
    expect(window.toast.success).not.toHaveBeenCalled()
    expect(window.toast.warning).toHaveBeenCalledWith('settings.models.manage.sync_apply_default_in_use')
  })
})
