import { expect, test } from 'vitest'
import _ from 'lodash'
import * as utils from './utils'
import * as config from './config'
import * as chat from '../src'

const apiUrl = config.get('API_URL')
const encryptionKey = config.get('ENCRYPTION_KEY')
const serverEventsProtocols = ['sse', 'websocket'] as const

type CheckApiCanSendAndReceiveMessagesProps = {
  client: chat.AuthenticatedClient
  conversationId: string
  protocol: chat.ServerEventsProtocol
}

type MessagePayload = chat.AuthenticatedClientRequests['createMessage']['payload']

const checkApiCanSendAndReceiveMessages = async (
  props: CheckApiCanSendAndReceiveMessagesProps,
  payload: MessagePayload
): Promise<MessagePayload> => {
  const { client, conversationId } = props

  const listener = await client.listenConversation({
    id: conversationId,
    protocol: props.protocol,
  })

  const waitForResponsePromise = new Promise<chat.Signals['message_created']>((resolve) => {
    listener.onceOrMore('message_created', (ev) => {
      if (ev.userId === client.user.id) {
        return 'keep-listening'
      }
      resolve(ev)
      return 'stop-listening'
    })
  })

  const createMessageRequest: chat.AuthenticatedClientRequests['createMessage'] = {
    conversationId: conversationId,
    payload,
  }

  const createMessagePromise = client.createMessage(createMessageRequest).then((res) => res.message)

  const [{ isBot, ...messageReceived }, messageSent] = await Promise.all([waitForResponsePromise, createMessagePromise])

  const { messages } = await client
    .listMessages({
      conversationId,
    })
    .then(({ messages }) => ({
      messages: _.sortBy(messages, (m) => new Date(m.createdAt).getTime()),
    }))

  expect(messages.length).toBe(2)
  expect(messages[0]).toEqual(messageSent)
  expect(messages[1]).toEqual(messageReceived)

  return messageReceived.payload
}

test.each(serverEventsProtocols)('api allows sending and receiving messages using botpress IDs', async (protocol) => {
  const client = await chat.Client.connect({ apiUrl })

  const {
    conversation: { id: conversationId },
  } = await client.createConversation({})

  await checkApiCanSendAndReceiveMessages(
    {
      client,
      conversationId,
      protocol,
    },
    {
      type: 'text',
      text: 'hello world',
    }
  )
})

test.each(serverEventsProtocols)('api allows sending and receiving messages using foreign IDs', async (protocol) => {
  const userId = utils.getUserFid()
  const conversationId = utils.getConversationFid()
  const client = await chat.Client.connect({ apiUrl, userId })

  await client.createConversation({ id: conversationId })

  await checkApiCanSendAndReceiveMessages(
    {
      client,
      conversationId,
      protocol,
    },
    {
      type: 'text',
      text: 'hello world',
    }
  )
})

test.each(serverEventsProtocols)(
  'api allows sending and receiving messages using remotly generated JWTs',
  async (protocol) => {
    const userId = utils.getUserFid()
    const conversationId = utils.getConversationFid()

    const client = await chat.Client.connect({ apiUrl, userId, encryptionKey })

    await client.getOrCreateConversation({ id: conversationId })

    await checkApiCanSendAndReceiveMessages(
      {
        client,
        conversationId,
        protocol,
      },
      {
        type: 'text',
        text: 'hello world',
      }
    )
  }
)

test.each(serverEventsProtocols)('api allows deleting a message', async (protocol) => {
  const client = await chat.Client.connect({ apiUrl })
  const { conversation } = await client.createConversation({})

  const signalListener = await client.listenConversation({ id: conversation.id, protocol })

  const [{ isBot, ...createdMessage }] = await Promise.all([
    utils.waitFor(signalListener, 'message_created'),
    client.createMessage({
      conversationId: conversation.id,
      payload: {
        type: 'text',
        text: 'hello world',
      },
    }),
  ])

  const { message: fetchedMessage } = await client.getMessage({
    id: createdMessage.id,
  })

  expect(fetchedMessage).toEqual(createdMessage)

  const [deletedMessage] = await Promise.all([
    utils.waitFor(signalListener, 'message_deleted'),
    client.deleteMessage({
      id: createdMessage.id,
    }),
  ])

  expect(deletedMessage.id).toEqual(createdMessage.id)

  await expect(
    client.getMessage({
      id: createdMessage.id,
    })
  ).rejects.toThrow(chat.ResourceNotFoundError)
})

test.each(serverEventsProtocols)('api allows sending and receiving messages with metadata', async (protocol) => {
  type Message = Awaited<ReturnType<chat.Client['listMessages']>>['messages'][number]
  const metadata = { foo: 'bar' }

  const client = await chat.Client.connect({ apiUrl })

  const { conversation } = await client.createConversation({})
  const { id: conversationId } = conversation

  const sendMessagePromise = client.createMessage({
    conversationId,
    payload: { type: 'text', text: 'metadata' }, // hello-bot forwards metadata when message is 'metadata'
    metadata,
  })

  const listener = await client.listenConversation({ id: conversationId, protocol })

  const receiveSelfMessagePromise = new Promise<Message>((resolve) =>
    listener.on('message_created', (m) => {
      if (m.userId === client.user.id) {
        resolve(m)
      }
    })
  )

  const receiveBotMessagePromise = new Promise<Message>((resolve) =>
    listener.on('message_created', (m) => {
      if (m.userId !== client.user.id) {
        resolve(m)
      }
    })
  )

  const [sentMessage, receivedSelfMessage, receivedBotMessage] = await Promise.all([
    sendMessagePromise,
    receiveSelfMessagePromise,
    receiveBotMessagePromise,
  ])

  expect(sentMessage.message.metadata).toEqual(metadata)
  expect(receivedSelfMessage.metadata).toEqual(metadata)
  expect(receivedBotMessage.metadata).toEqual(metadata)
  expect(sentMessage.message.payload).not.toHaveProperty('metadata')
  expect(receivedSelfMessage.payload).not.toHaveProperty('metadata')
  expect(receivedBotMessage.payload).not.toHaveProperty('metadata')

  const fetchedMessages = await client.listMessages({ conversationId }).then((r) => r.messages)

  const fetchedSelfMessages = fetchedMessages.filter((m) => m.userId === client.user.id)
  expect(fetchedSelfMessages.length).toBe(1)

  const [fetchedSelfMessage] = fetchedSelfMessages
  expect(fetchedSelfMessage!.metadata).toEqual(metadata)
})

test.each(serverEventsProtocols)('api allows sending bloc messages', async (protocol) => {
  const client = await chat.Client.connect({ apiUrl })

  const {
    conversation: { id: conversationId },
  } = await client.createConversation({})

  await checkApiCanSendAndReceiveMessages(
    {
      client,
      conversationId,
      protocol,
    },
    {
      type: 'bloc',
      items: [
        {
          type: 'text',
          text: 'hello world',
        },
        {
          type: 'image',
          imageUrl: 'https://fastly.picsum.photos/id/329/200/300.jpg?hmac=_yLyj0EqdpQ-cX84OlMxz3YzOjjd7liq6b25ldkVSpA',
        },
      ],
    }
  )
})

test.each(serverEventsProtocols)('api allows receiving bloc messages from bot', async (protocol) => {
  const client = await chat.Client.connect({ apiUrl })

  const {
    conversation: { id: conversationId },
  } = await client.createConversation({})

  const responsePayload: MessagePayload = await checkApiCanSendAndReceiveMessages(
    { client, conversationId, protocol },
    { type: 'text', text: 'bloc' }
  )

  expect(responsePayload.type).toEqual('bloc')
  expect(responsePayload.items.length).toEqual(3)
})
