/**
 * @jest-environment jsdom
 * @jest-environment-options {"url": "https://salsify-ecdn.com/sdk/client-id/lang-code/BTF/id-type/existing-product/index.html"}
 */

import SdkApi from '../api'
import { MessageChannel as WorkerThreadsMessageChannel } from 'worker_threads'
import { makeResponse } from '../__tests__/helpers'
import request from '../utils/request'
import { createLogger } from '../utils/logger'

jest.mock('../utils/request')
jest.mock('../utils/logger')

const headMock = request.head as jest.Mock
const getMock = request.get as jest.Mock
const logMock = jest.fn()
;(createLogger as jest.Mock).mockReturnValue({ log: logMock })

const exampleContent = '<div>enhanced-content</div>'
const emptyContent = ''

const uuidRegex = /^[0-9a-f]{8}\b-[0-9a-f]{4}\b-[0-9a-f]{4}\b-[0-9a-f]{4}\b-[0-9a-f]{12}$/

const defaultOptions = {
  clientId: 'client-id',
}

function expectPageSessionId(messageListener: jest.Mock, pageSessionId: unknown): void {
  expect(messageListener).toHaveBeenLastCalledWith(
    expect.objectContaining({
      data: expect.objectContaining({
        pageSessionId,
      }),
    })
  )
}

function expectPageSessionIdUpdated(messageListener: jest.Mock, lastPageSessionId: string): void {
  expectPageSessionId(messageListener, expect.stringMatching(uuidRegex))
  expect(messageListener).not.toHaveBeenLastCalledWith(
    expect.objectContaining({
      data: expect.objectContaining({
        pageSessionId: lastPageSessionId,
      }),
    })
  )
}

describe('SdkApi', () => {
  let sdk: SdkApi

  beforeEach(() => {
    sdk = new SdkApi('npm')
    headMock.mockImplementation(() => makeResponse(exampleContent))
    getMock.mockImplementation(() => makeResponse(emptyContent))
  })

  afterEach(() => {
    headMock.mockClear()
    getMock.mockClear()
  })

  describe('Render', () => {
    let channel: MessageChannel | undefined

    // workaround for https://github.com/jsdom/jsdom/issues/2745
    const messageMonkeyPatch = (event: MessageEvent): void => {
      if (event.origin === '' && event.data.messageType === 'contextRequest') {
        event.stopImmediatePropagation()
        const eventWithOriginInitDict: MessageEventInit = {
          data: event.data,
          origin: 'https://salsify-ecdn.com',
          source: window,
        }
        if (channel) {
          eventWithOriginInitDict.ports = [channel.port2]
        }
        const eventWithOrigin: MessageEvent = new MessageEvent('message', eventWithOriginInitDict)
        window.dispatchEvent(eventWithOrigin)
      }
    }

    beforeAll(() => {
      window.addEventListener('message', messageMonkeyPatch)
    })

    afterAll(() => {
      window.removeEventListener('message', messageMonkeyPatch)
    })

    test('context pageSessionId resets on subsequent renders unless navigation event has occurred', async () => {
      channel = new WorkerThreadsMessageChannel() as unknown as MessageChannel
      let contextReceived: (value?: unknown) => void
      const waitForContext = (): Promise<unknown> => new Promise(resolve => (contextReceived = resolve))
      let pageSessionId = ''
      let lastPageSessionId = ''
      const messageListener = jest.fn().mockImplementation((event: MessageEvent) => {
        if (event.data.pageSessionId) {
          pageSessionId = event.data.pageSessionId
          contextReceived()
        }
      })
      sdk.init(defaultOptions)
      channel!.port1.onmessage = messageListener
      window.postMessage({ messageType: 'contextRequest' }, '*', [channel!.port2])
      await waitForContext()

      expect(messageListener).toHaveBeenCalledTimes(1)
      expectPageSessionId(messageListener, expect.stringMatching(uuidRegex))

      lastPageSessionId = pageSessionId

      // no render or navigation (pageSessionId should NOT have updated)

      window.postMessage({ messageType: 'contextRequest' }, '*', [channel!.port2])
      await waitForContext()

      expect(messageListener).toHaveBeenCalledTimes(2)
      expectPageSessionId(messageListener, lastPageSessionId)

      lastPageSessionId = pageSessionId

      // first render (pageSessionId should NOT have updated)

      const container = document.createElement('div')
      await sdk.enhancedContent.renderIframe(container, 'foo', 'bar')
      window.postMessage({ messageType: 'contextRequest' }, '*', [channel!.port2])
      await waitForContext()

      expect(messageListener).toHaveBeenCalledTimes(3)
      expectPageSessionId(messageListener, lastPageSessionId)

      lastPageSessionId = pageSessionId

      // second render without navigation (pageSessionId should have updated)

      await sdk.enhancedContent.renderIframe(container, 'foo', 'bar')
      window.postMessage({ messageType: 'contextRequest' }, '*', [channel!.port2])
      await waitForContext()

      expect(messageListener).toHaveBeenCalledTimes(4)
      expectPageSessionIdUpdated(messageListener, lastPageSessionId)

      lastPageSessionId = pageSessionId

      // third render without navigation (pageSessionId should have updated)

      await sdk.enhancedContent.renderIframe(container, 'foo', 'bar')
      window.postMessage({ messageType: 'contextRequest' }, '*', [channel!.port2])
      await waitForContext()

      expect(messageListener).toHaveBeenCalledTimes(5)
      expectPageSessionIdUpdated(messageListener, lastPageSessionId)

      lastPageSessionId = pageSessionId

      // navigation (pageSessionId should have updated)

      sdk.events.navigation({ productIdType: 'foo', productId: 'bar' })
      window.postMessage({ messageType: 'contextRequest' }, '*', [channel!.port2])
      await waitForContext()

      expect(messageListener).toHaveBeenCalledTimes(6)
      expectPageSessionIdUpdated(messageListener, lastPageSessionId)

      lastPageSessionId = pageSessionId

      // first render after navigation (pageSessionId should NOT have updated)

      await sdk.enhancedContent.renderIframe(container, 'foo', 'bar')
      window.postMessage({ messageType: 'contextRequest' }, '*', [channel!.port2])
      await waitForContext()

      expect(messageListener).toHaveBeenCalledTimes(7)
      expectPageSessionId(messageListener, lastPageSessionId)

      lastPageSessionId = pageSessionId

      // second render after navigation (pageSessionId should have updated)

      await sdk.enhancedContent.renderIframe(container, 'foo', 'bar')
      window.postMessage({ messageType: 'contextRequest' }, '*', [channel!.port2])
      await waitForContext()

      expect(messageListener).toHaveBeenCalledTimes(8)
      expectPageSessionIdUpdated(messageListener, lastPageSessionId)

      channel!.port1.onmessage = null
      channel = undefined
    })
  })
})
