import { act, renderHook, waitFor } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { useDSL } from '../use-DSL'

const toastMocks = vi.hoisted(() => ({
  call: vi.fn(),
  dismiss: vi.fn(),
  update: vi.fn(),
  promise: vi.fn(),
}))

vi.mock('@langgenius/dify-ui/toast', () => ({
  toast: Object.assign(toastMocks.call, {
    success: vi.fn((message: string, options?: Record<string, unknown>) => toastMocks.call({ type: 'success', message, ...options })),
    error: vi.fn((message: string, options?: Record<string, unknown>) => toastMocks.call({ type: 'error', message, ...options })),
    warning: vi.fn((message: string, options?: Record<string, unknown>) => toastMocks.call({ type: 'warning', message, ...options })),
    info: vi.fn((message: string, options?: Record<string, unknown>) => toastMocks.call({ type: 'info', message, ...options })),
    dismiss: toastMocks.dismiss,
    update: toastMocks.update,
    promise: toastMocks.promise,
  }),
}))
const mockEventEmitter = { emit: vi.fn() }
vi.mock('@/context/event-emitter', () => ({
  useEventEmitterContextContext: () => ({ eventEmitter: mockEventEmitter }),
}))

const mockDoSyncWorkflowDraft = vi.fn()
vi.mock('../use-nodes-sync-draft', () => ({
  useNodesSyncDraft: () => ({ doSyncWorkflowDraft: mockDoSyncWorkflowDraft }),
}))

const mockGetState = vi.fn()
vi.mock('@/app/components/workflow/store', () => ({
  useWorkflowStore: () => ({ getState: mockGetState }),
}))

const mockExportPipelineConfig = vi.fn()
vi.mock('@/service/use-pipeline', () => ({
  useExportPipelineDSL: () => ({ mutateAsync: mockExportPipelineConfig }),
}))

const mockFetchWorkflowDraft = vi.fn()
vi.mock('@/service/workflow', () => ({
  fetchWorkflowDraft: (...args: unknown[]) => mockFetchWorkflowDraft(...args),
}))

const mockDownloadBlob = vi.fn()
vi.mock('@/utils/download', () => ({
  downloadBlob: (...args: unknown[]) => mockDownloadBlob(...args),
}))
vi.mock('@/app/components/workflow/constants', () => ({
  DSL_EXPORT_CHECK: 'DSL_EXPORT_CHECK',
}))

describe('useDSL', () => {
  let mockLink: { href: string, download: string, click: ReturnType<typeof vi.fn>, style: { display: string }, remove: ReturnType<typeof vi.fn> }
  let originalCreateElement: typeof document.createElement
  let originalAppendChild: typeof document.body.appendChild
  let mockCreateObjectURL: ReturnType<typeof vi.spyOn>
  let mockRevokeObjectURL: ReturnType<typeof vi.spyOn>

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

    mockLink = {
      href: '',
      download: '',
      click: vi.fn(),
      style: { display: '' },
      remove: vi.fn(),
    }

    originalCreateElement = document.createElement.bind(document)
    document.createElement = vi.fn((tagName: string) => {
      if (tagName === 'a') {
        return mockLink as unknown as HTMLElement
      }
      return originalCreateElement(tagName)
    }) as typeof document.createElement

    originalAppendChild = document.body.appendChild.bind(document.body)
    document.body.appendChild = vi.fn(<T extends Node>(node: T): T => node) as typeof document.body.appendChild

    mockCreateObjectURL = vi.spyOn(window.URL, 'createObjectURL').mockReturnValue('blob:test-url')
    mockRevokeObjectURL = vi.spyOn(window.URL, 'revokeObjectURL').mockImplementation(() => {})

    mockGetState.mockReturnValue({
      pipelineId: 'test-pipeline-id',
      knowledgeName: 'Test Knowledge Base',
    })

    mockDoSyncWorkflowDraft.mockResolvedValue(undefined)
    mockExportPipelineConfig.mockResolvedValue({ data: 'yaml-content' })
    mockFetchWorkflowDraft.mockResolvedValue({ environment_variables: [] })
  })

  afterEach(() => {
    document.createElement = originalCreateElement
    document.body.appendChild = originalAppendChild
    mockCreateObjectURL.mockRestore()
    mockRevokeObjectURL.mockRestore()
    vi.clearAllMocks()
  })

  describe('handleExportDSL', () => {
    it('should return early when pipelineId is not set', async () => {
      mockGetState.mockReturnValue({ pipelineId: null, knowledgeName: 'test' })

      const { result } = renderHook(() => useDSL())

      await act(async () => {
        await result.current.handleExportDSL()
      })

      expect(mockDoSyncWorkflowDraft).not.toHaveBeenCalled()
    })

    it('should create and download file', async () => {
      const { result } = renderHook(() => useDSL())

      await act(async () => {
        await result.current.handleExportDSL()
      })

      expect(mockDownloadBlob).toHaveBeenCalled()
    })

    it('should set correct download filename', async () => {
      const { result } = renderHook(() => useDSL())

      await act(async () => {
        await result.current.handleExportDSL()
      })

      expect(mockDownloadBlob).toHaveBeenCalledWith(
        expect.objectContaining({
          fileName: 'Test Knowledge Base.pipeline',
        }),
      )
    })

    it('should pass blob data to downloadBlob', async () => {
      const { result } = renderHook(() => useDSL())

      await act(async () => {
        await result.current.handleExportDSL()
      })

      expect(mockDownloadBlob).toHaveBeenCalledWith(
        expect.objectContaining({
          data: expect.any(Blob),
        }),
      )
    })

    it('should handle export error', async () => {
      mockExportPipelineConfig.mockRejectedValue(new Error('Export failed'))

      const { result } = renderHook(() => useDSL())

      await act(async () => {
        await result.current.handleExportDSL()
      })

      await waitFor(() => {
        expect(toastMocks.call).toHaveBeenCalledWith({
          type: 'error',
          message: 'app.exportFailed',
        })
      })
    })

    it('should pass include parameter', async () => {
      const { result } = renderHook(() => useDSL())

      await act(async () => {
        await result.current.handleExportDSL(true)
      })

      await waitFor(() => {
        expect(mockExportPipelineConfig).toHaveBeenCalledWith({
          pipelineId: 'test-pipeline-id',
          include: true,
        })
      })
    })
  })

  describe('exportCheck', () => {
    it('should return early when pipelineId is not set', async () => {
      mockGetState.mockReturnValue({ pipelineId: null })

      const { result } = renderHook(() => useDSL())

      await act(async () => {
        await result.current.exportCheck()
      })

      expect(mockFetchWorkflowDraft).not.toHaveBeenCalled()
    })

    it('should call handleExportDSL directly when no secret variables', async () => {
      mockFetchWorkflowDraft.mockResolvedValue({ environment_variables: [] })

      const { result } = renderHook(() => useDSL())

      await act(async () => {
        await result.current.exportCheck()
      })

      await waitFor(() => {
        expect(mockFetchWorkflowDraft).toHaveBeenCalledWith('/rag/pipelines/test-pipeline-id/workflows/draft')
        expect(mockDoSyncWorkflowDraft).toHaveBeenCalled()
      })
    })

    it('should emit event when secret variables exist', async () => {
      const secretVars = [{ value_type: 'secret', name: 'API_KEY' }]
      mockFetchWorkflowDraft.mockResolvedValue({ environment_variables: secretVars })

      const { result } = renderHook(() => useDSL())

      await act(async () => {
        await result.current.exportCheck()
      })

      await waitFor(() => {
        expect(mockEventEmitter.emit).toHaveBeenCalledWith({
          type: expect.any(String),
          payload: {
            data: secretVars,
          },
        })
      })
    })

    it('should handle export check error', async () => {
      mockFetchWorkflowDraft.mockRejectedValue(new Error('Fetch failed'))

      const { result } = renderHook(() => useDSL())

      await act(async () => {
        await result.current.exportCheck()
      })

      await waitFor(() => {
        expect(toastMocks.call).toHaveBeenCalledWith({
          type: 'error',
          message: 'app.exportFailed',
        })
      })
    })
  })
})
