import { act, renderHook, waitFor } from '@testing-library/react'
import { DSL_EXPORT_CHECK } from '@/app/components/workflow/constants'
import { useDSLByCanEdit } 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 mockEmit = vi.fn()
const mockDoSyncWorkflowDraft = vi.fn()
const mockExportAppConfig = vi.fn()
const mockFetchWorkflowDraft = vi.fn()
const mockDownloadBlob = vi.fn()

let appStoreState: {
  appDetail?: {
    id: string
    name: string
  }
}

vi.mock('@/context/event-emitter', () => ({
  useEventEmitterContextContext: () => ({
    eventEmitter: {
      emit: mockEmit,
    },
  }),
}))

vi.mock('@/app/components/app/store', () => ({
  useStore: <T>(selector: (state: typeof appStoreState) => T) => selector(appStoreState),
}))

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

vi.mock('@/service/apps', () => ({
  exportAppConfig: (...args: unknown[]) => mockExportAppConfig(...args),
}))

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

vi.mock('@/utils/download', () => ({
  downloadBlob: (...args: unknown[]) => mockDownloadBlob(...args),
}))

const createDeferred = <T>() => {
  let resolve!: (value: T) => void
  const promise = new Promise<T>((res) => {
    resolve = res
  })
  return { promise, resolve }
}

describe('useDSLByCanEdit', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    appStoreState = {
      appDetail: {
        id: 'app-1',
        name: 'Workflow App',
      },
    }
    mockDoSyncWorkflowDraft.mockResolvedValue(undefined)
    mockExportAppConfig.mockResolvedValue({ data: 'yaml-content' })
    mockFetchWorkflowDraft.mockResolvedValue({ environment_variables: [] })
  })

  it('should export workflow dsl and download the yaml blob when no secret env is present', async () => {
    const { result } = renderHook(() => useDSLByCanEdit(true))

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

    expect(mockFetchWorkflowDraft).toHaveBeenCalledWith('/apps/app-1/workflows/draft')
    expect(mockDoSyncWorkflowDraft).toHaveBeenCalled()
    expect(mockExportAppConfig).toHaveBeenCalledWith({
      appID: 'app-1',
      include: false,
      workflowID: undefined,
    })
    expect(mockDownloadBlob).toHaveBeenCalledWith(
      expect.objectContaining({
        data: expect.any(Blob),
        fileName: 'Workflow App.yml',
      }),
    )
  })

  it('should download workflow DSL containing portable Agent packages unchanged', async () => {
    const agentDSL = `version: 0.7.0
agent_packages:
  agent_1:
    schema_version: 1
workflow:
  graph:
    nodes:
      - data:
          type: agent
          version: '2'
          agent_binding:
            binding_type: inline_agent
            package_ref: agent_1
`
    mockExportAppConfig.mockResolvedValue({ data: agentDSL })
    const { result } = renderHook(() => useDSLByCanEdit(true))

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

    const [{ data, fileName }] = mockDownloadBlob.mock.calls[0] as [
      { data: Blob; fileName: string },
    ]
    expect(await data.text()).toBe(agentDSL)
    expect(fileName).toBe('Workflow App.yml')
  })

  it('should forward include and workflow id arguments when exporting dsl directly', async () => {
    const { result } = renderHook(() => useDSLByCanEdit(true))

    await act(async () => {
      await result.current.handleExportDSL(true, 'workflow-1')
    })

    expect(mockExportAppConfig).toHaveBeenCalledWith({
      appID: 'app-1',
      include: true,
      workflowID: 'workflow-1',
    })
  })

  it('should emit DSL_EXPORT_CHECK when secret environment variables exist', async () => {
    const secretVars = [{ id: 'env-1', value_type: 'secret', value: 'secret-token' }]
    mockFetchWorkflowDraft.mockResolvedValue({ environment_variables: secretVars })

    const { result } = renderHook(() => useDSLByCanEdit(true))

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

    expect(mockEmit).toHaveBeenCalledWith({
      type: DSL_EXPORT_CHECK,
      payload: {
        data: secretVars,
      },
    })
    expect(mockExportAppConfig).not.toHaveBeenCalled()
  })

  it('should return early when app detail is unavailable', async () => {
    appStoreState = {}

    const { result } = renderHook(() => useDSLByCanEdit(true))

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

    expect(mockFetchWorkflowDraft).not.toHaveBeenCalled()
    expect(mockDoSyncWorkflowDraft).not.toHaveBeenCalled()
    expect(mockExportAppConfig).not.toHaveBeenCalled()
    expect(mockEmit).not.toHaveBeenCalled()
  })

  it('should notify when export fails', async () => {
    mockExportAppConfig.mockRejectedValue(new Error('export failed'))

    const { result } = renderHook(() => useDSLByCanEdit(true))

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

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

  it('should notify when exportCheck cannot load the workflow draft', async () => {
    mockFetchWorkflowDraft.mockRejectedValue(new Error('draft fetch failed'))

    const { result } = renderHook(() => useDSLByCanEdit(true))

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

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

  it('should ignore repeated export attempts while an export is already in progress', async () => {
    const deferred = createDeferred<{ data: string }>()
    mockExportAppConfig.mockReturnValue(deferred.promise)

    const { result } = renderHook(() => useDSLByCanEdit(true))
    let firstExportPromise!: Promise<void>

    act(() => {
      firstExportPromise = result.current.handleExportDSL()
    })

    await waitFor(() => {
      expect(mockDoSyncWorkflowDraft).toHaveBeenCalledTimes(1)
      expect(mockExportAppConfig).toHaveBeenCalledTimes(1)
    })

    act(() => {
      void result.current.handleExportDSL()
    })

    expect(mockExportAppConfig).toHaveBeenCalledTimes(1)

    await act(async () => {
      deferred.resolve({ data: 'yaml-content' })
      await firstExportPromise
    })
  })
})
