import { Readable } from 'node:stream'
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
import { BASE_URL, ChatClient, DifyClient, WorkflowClient, routes } from './index'

const stubFetch = (): ReturnType<typeof vi.fn> => {
  const fetchMock = vi.fn()
  vi.stubGlobal('fetch', fetchMock)
  return fetchMock
}

const jsonResponse = (body: unknown, init: ResponseInit = {}): Response =>
  new Response(JSON.stringify(body), {
    status: 200,
    ...init,
    headers: {
      'content-type': 'application/json',
      ...init.headers,
    },
  })

describe('Client', () => {
  beforeEach(() => {
    vi.restoreAllMocks()
    vi.unstubAllGlobals()
  })

  it('creates a client with default settings', () => {
    const difyClient = new DifyClient('test')

    expect(difyClient.getHttpClient().getSettings()).toMatchObject({
      apiKey: 'test',
      baseUrl: BASE_URL,
      timeout: 60,
    })
  })

  it('updates the api key', () => {
    const difyClient = new DifyClient('test')
    difyClient.updateApiKey('test2')

    expect(difyClient.getHttpClient().getSettings().apiKey).toBe('test2')
  })
})

describe('Send Requests', () => {
  beforeEach(() => {
    vi.restoreAllMocks()
    vi.unstubAllGlobals()
  })

  it('makes a successful request to the application parameter route', async () => {
    const fetchMock = stubFetch()
    const difyClient = new DifyClient('test')
    const method = 'GET'
    const endpoint = routes.application.url()

    fetchMock.mockResolvedValueOnce(jsonResponse('response'))

    const response = await difyClient.sendRequest(method, endpoint)

    expect(response).toMatchObject({
      status: 200,
      data: 'response',
      headers: {
        'content-type': 'application/json',
      },
    })
    const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
    expect(url).toBe(`${BASE_URL}${endpoint}`)
    expect(init.method).toBe(method)
    expect(init.headers).toMatchObject({
      Authorization: 'Bearer test',
      'User-Agent': 'dify-client-node',
    })
  })

  it('uses the getMeta route configuration', async () => {
    const fetchMock = stubFetch()
    const difyClient = new DifyClient('test')
    fetchMock.mockResolvedValueOnce(jsonResponse({ ok: true }))

    await difyClient.getMeta('end-user')

    const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
    expect(url).toBe(`${BASE_URL}${routes.getMeta.url()}?user=end-user`)
    expect(init.method).toBe(routes.getMeta.method)
    expect(init.headers).toMatchObject({
      Authorization: 'Bearer test',
    })
  })
})

describe('File uploads', () => {
  const OriginalFormData = globalThis.FormData

  beforeAll(() => {
    globalThis.FormData = class FormDataMock extends Readable {
      constructor() {
        super()
      }

      override _read() {}

      append() {}

      getHeaders() {
        return {
          'content-type': 'multipart/form-data; boundary=test',
        }
      }
    } as unknown as typeof FormData
  })

  afterAll(() => {
    globalThis.FormData = OriginalFormData
  })

  beforeEach(() => {
    vi.restoreAllMocks()
    vi.unstubAllGlobals()
  })

  it('does not override multipart boundary headers for legacy FormData', async () => {
    const fetchMock = stubFetch()
    const difyClient = new DifyClient('test')
    const form = new globalThis.FormData()
    fetchMock.mockResolvedValueOnce(jsonResponse({ ok: true }))

    await difyClient.fileUpload(form, 'end-user')

    const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
    expect(url).toBe(`${BASE_URL}${routes.fileUpload.url()}`)
    expect(init.method).toBe(routes.fileUpload.method)
    expect(init.headers).toMatchObject({
      Authorization: 'Bearer test',
      'content-type': 'multipart/form-data; boundary=test',
    })
    expect(init.body).not.toBe(form)
    expect((init as RequestInit & { duplex?: string }).duplex).toBe('half')
  })
})

describe('Workflow client', () => {
  beforeEach(() => {
    vi.restoreAllMocks()
    vi.unstubAllGlobals()
  })

  it('uses tasks stop path for workflow stop', async () => {
    const fetchMock = stubFetch()
    const workflowClient = new WorkflowClient('test')
    fetchMock.mockResolvedValueOnce(jsonResponse({ result: 'success' }))

    await workflowClient.stop('task-1', 'end-user')

    const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
    expect(url).toBe(`${BASE_URL}${routes.stopWorkflow.url('task-1')}`)
    expect(init.method).toBe(routes.stopWorkflow.method)
    expect(init.headers).toMatchObject({
      Authorization: 'Bearer test',
      'Content-Type': 'application/json',
    })
    expect(init.body).toBe(JSON.stringify({ user: 'end-user' }))
  })

  it('maps workflow log filters to service api params', async () => {
    const fetchMock = stubFetch()
    const workflowClient = new WorkflowClient('test')
    fetchMock.mockResolvedValueOnce(jsonResponse({ ok: true }))

    await workflowClient.getLogs({
      createdAtAfter: '2024-01-01T00:00:00Z',
      createdAtBefore: '2024-01-02T00:00:00Z',
      createdByEndUserSessionId: 'sess-1',
      createdByAccount: 'acc-1',
      page: 2,
      limit: 10,
    })

    const [url] = fetchMock.mock.calls[0] as [string, RequestInit]
    const parsedUrl = new URL(url)
    expect(parsedUrl.origin + parsedUrl.pathname).toBe(`${BASE_URL}/workflows/logs`)
    expect(parsedUrl.searchParams.get('created_at__before')).toBe('2024-01-02T00:00:00Z')
    expect(parsedUrl.searchParams.get('created_at__after')).toBe('2024-01-01T00:00:00Z')
    expect(parsedUrl.searchParams.get('created_by_end_user_session_id')).toBe('sess-1')
    expect(parsedUrl.searchParams.get('created_by_account')).toBe('acc-1')
    expect(parsedUrl.searchParams.get('page')).toBe('2')
    expect(parsedUrl.searchParams.get('limit')).toBe('10')
  })
})

describe('Chat client', () => {
  beforeEach(() => {
    vi.restoreAllMocks()
    vi.unstubAllGlobals()
  })

  it('places user in query for suggested messages', async () => {
    const fetchMock = stubFetch()
    const chatClient = new ChatClient('test')
    fetchMock.mockResolvedValueOnce(jsonResponse({ result: 'success', data: [] }))

    await chatClient.getSuggested('msg-1', 'end-user')

    const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
    expect(url).toBe(`${BASE_URL}${routes.getSuggested.url('msg-1')}?user=end-user`)
    expect(init.method).toBe(routes.getSuggested.method)
    expect(init.headers).toMatchObject({
      Authorization: 'Bearer test',
    })
  })

  it('uses last_id when listing conversations', async () => {
    const fetchMock = stubFetch()
    const chatClient = new ChatClient('test')
    fetchMock.mockResolvedValueOnce(jsonResponse({ ok: true }))

    await chatClient.getConversations('end-user', 'last-1', 10)

    const [url] = fetchMock.mock.calls[0] as [string, RequestInit]
    expect(url).toBe(
      `${BASE_URL}${routes.getConversations.url()}?user=end-user&last_id=last-1&limit=10`,
    )
  })

  it('lists app feedbacks without user params', async () => {
    const fetchMock = stubFetch()
    const chatClient = new ChatClient('test')
    fetchMock.mockResolvedValueOnce(jsonResponse({ data: [] }))

    await chatClient.getAppFeedbacks(1, 20)

    const [url] = fetchMock.mock.calls[0] as [string, RequestInit]
    expect(url).toBe(`${BASE_URL}/app/feedbacks?page=1&limit=20`)
  })
})
