/**
 * @jest-environment jsdom
 */

import EnhancedContentApi from '../index'
import { PerProductConfigCache, selectSource } from '../perProductConfig'
import { makeContext, makeSettings, makeResponse } from '../../__tests__/helpers'
import commonSuite from './index.common'
import request from '../../utils/request'

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

const clientId = 'client-id'
const languageCode = 'lang-code'
const enhancedContent = {}
const productWithCdnRoot =
  'https://testenv.test.salsify.com/brand-space-cdn/sdk/client-id/lang-code/BTF/id-type/existing-product/index.html'

const productWithEcPath = `https://salsify-ecdn.com/sdk/${clientId}/${languageCode}/BTF/id-type/existing-product`

let ecApi: EnhancedContentApi
const log = jest.fn()
const settings = makeSettings({ clientId, languageCode, enhancedContent, tracking: true })
const settingsNoTrack = makeSettings({ clientId, languageCode, enhancedContent, tracking: false })
const settingsCdnRoot = makeSettings({
  clientId,
  languageCode,
  enhancedContent,
  tracking: true,
  cdnRoot: 'https://testenv.test.salsify.com/brand-space-cdn',
})

commonSuite('browser')

const PerProductConfigCacheMock = PerProductConfigCache as jest.Mock
const selectSourceMock = selectSource as jest.Mock
const headMock = request.head as jest.Mock

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

describe('EnhancedContentApi (browser)', () => {
  beforeEach(() => {
    ecApi = new EnhancedContentApi(settings, makeContext(), { log })
    PerProductConfigCacheMock.mock.instances[0].getConfig.mockImplementation(() => undefined)
  })

  afterEach(() => {
    jest.clearAllMocks()
  })

  describe('renderIframe', () => {
    beforeEach(() => {
      headMock.mockImplementation(() => makeResponse(exampleContent))
    })

    // This must all be one test because the iframeResizeListener can only be attached once
    test('iframe resize listener', async () => {
      let eventListenerCallback!: EventListener
      window.addEventListener = jest.fn((_type, callback) => (eventListenerCallback = callback as EventListener))

      const container = document.createElement('div')
      await ecApi.renderIframe(container, 'foo', 'id-type')
      await ecApi.renderIframe(container, 'foo', 'id-type')

      // iframe resize listener is attached once and only once
      expect(window.addEventListener).toHaveBeenCalledTimes(1)

      eventListenerCallback(
        new MessageEvent('message', {
          origin: 'https://salsify-ecdn.com',
          data: { messageType: 'heightUpdateRequest', height: 100 },
        })
      )

      // iframe is not found because container hasn't been appended to document yet
      expect(log).toBeCalledTimes(3) // two ec_render_iframe + one error
      expect(log).toHaveBeenNthCalledWith(3, 'error', {
        errorContext: 'iframeResizeListener',
        errorType: 'dom',
        errorMessage: 'Could not find iframe with selector #salsify-ec-iframe',
      })

      document.querySelector('body')?.append(container)
      const iframe = document.querySelector('#salsify-ec-iframe') as HTMLIFrameElement
      expect(iframe.height).toBe('0')

      eventListenerCallback(
        new MessageEvent('message', {
          origin: 'https://salsify-ecdn.com',
          data: { messageType: 'heightUpdateRequest', height: 100 },
        })
      )

      // iframe is found after appending container, so no additional error is logged
      expect(log).toBeCalledTimes(3)
      expect(iframe.height).toBe('100')
    })

    test('should throw error when no product ID type is specified when calling', async () => {
      const container = document.createElement('div')
      const check = (): Promise<void> | undefined => ecApi.renderIframe(container, 'product')
      return expect(check).rejects.toThrowError('No ID type specified.')
    })

    test('it renders the iframe with the correct src', async () => {
      const container = document.createElement('div')
      await ecApi.renderIframe(container, 'existing-product', 'id-type')
      expect(container.firstElementChild?.getAttribute('src')).toBe(`${productWithEcPath}/index.html`)
      expect(PerProductConfigCacheMock.mock.instances.length).toBe(1)
      expect(PerProductConfigCacheMock.mock.instances[0].getConfig).toHaveBeenCalledTimes(1)
      expect(PerProductConfigCacheMock.mock.instances[0].getConfig).toHaveBeenCalledWith(productWithEcPath)
    })

    test('it renders the iframe with the cdn root url', async () => {
      ecApi = new EnhancedContentApi(settingsCdnRoot, makeContext(), { log })
      const container = document.createElement('div')
      await ecApi.renderIframe(container, 'existing-product', 'id-type')
      expect(container.firstElementChild?.getAttribute('src')).toBe(productWithCdnRoot)
    })

    test('it sends an ec_render_iframe event', async () => {
      PerProductConfigCacheMock.mock.instances[0].getConfig.mockResolvedValue(undefined)

      const container = document.createElement('div')
      await ecApi.renderIframe(container, 'existing-product', 'id-type')

      expect(headMock).toHaveBeenCalledTimes(1)
      expect(headMock).toHaveBeenCalledWith(`${productWithEcPath}/index.html`)
      const renderConfig = {
        idType: 'id-type',
        productId: 'existing-product',
        content: null,
        allContentExists: false,
        source: 'index.html',
        sourceExists: true,
      }
      expect(ecApi.lastRenderConfig).toMatchObject(renderConfig)
      expect(log).toHaveBeenCalledWith('ec_render_iframe', renderConfig)
    })

    describe('when EC is missing', () => {
      beforeEach(() => {
        headMock.mockImplementation(() => makeResponse(emptyContent))
      })

      test('it sends an ec_render_iframe event with sourceExists: false', async () => {
        PerProductConfigCacheMock.mock.instances[0].getConfig.mockResolvedValue(undefined)

        const container = document.createElement('div')
        await ecApi.renderIframe(container, 'existing-product', 'id-type')

        expect(headMock).toHaveBeenCalledTimes(1)
        expect(headMock).toHaveBeenCalledWith(`${productWithEcPath}/index.html`)
        const renderConfig = {
          idType: 'id-type',
          productId: 'existing-product',
          content: null,
          allContentExists: false,
          source: 'index.html',
          sourceExists: false,
        }
        expect(ecApi.lastRenderConfig).toMatchObject(renderConfig)
        expect(log).toHaveBeenCalledWith('ec_render_iframe', renderConfig)
      })
    })

    describe('with config content', () => {
      const content = [
        { source: 'foo.html', weight: 0.4 },
        { source: 'bar.html', weight: 0.4 },
        { source: null, weight: 0.2 },
      ]

      beforeEach(() => {
        PerProductConfigCacheMock.mock.instances[0].getConfig.mockImplementation(() => ({
          content,
        }))
      })

      test('it renders the iframe with the selected src', async () => {
        selectSourceMock.mockImplementation(() => content[1])
        const container = document.createElement('div')
        await ecApi.renderIframe(container, 'existing-product', 'id-type')
        expect(container.firstElementChild?.getAttribute('src')).toBe(`${productWithEcPath}/bar.html`)
        expect(PerProductConfigCacheMock.mock.instances.length).toBe(1)
        expect(PerProductConfigCacheMock.mock.instances[0].getConfig).toHaveBeenCalledTimes(1)
        expect(PerProductConfigCacheMock.mock.instances[0].getConfig).toHaveBeenCalledWith(productWithEcPath)
        expect(headMock).toHaveBeenCalledTimes(2)
        expect(headMock).toHaveBeenCalledWith(`${productWithEcPath}/foo.html`)
        expect(headMock).toHaveBeenCalledWith(`${productWithEcPath}/bar.html`)
        const renderConfig = {
          idType: 'id-type',
          productId: 'existing-product',
          content,
          allContentExists: true,
          source: 'bar.html',
          sourceExists: true,
        }
        expect(ecApi.lastRenderConfig).toMatchObject(renderConfig)
        expect(log).toHaveBeenCalledWith('ec_render_iframe', renderConfig)
      })

      test('it does not render an iframe when a null source is selected', async () => {
        selectSourceMock.mockImplementation(() => content[2])
        const container = document.createElement('div')
        await ecApi.renderIframe(container, 'existing-product', 'id-type')
        expect(container.firstElementChild).toBeNull()
        expect(PerProductConfigCacheMock.mock.instances.length).toBe(1)
        expect(PerProductConfigCacheMock.mock.instances[0].getConfig).toHaveBeenCalledTimes(1)
        expect(PerProductConfigCacheMock.mock.instances[0].getConfig).toHaveBeenCalledWith(productWithEcPath)
        expect(headMock).toHaveBeenCalledTimes(2)
        expect(headMock).toHaveBeenCalledWith(`${productWithEcPath}/foo.html`)
        expect(headMock).toHaveBeenCalledWith(`${productWithEcPath}/bar.html`)
        const renderConfig = {
          idType: 'id-type',
          productId: 'existing-product',
          content,
          allContentExists: true,
          source: null,
          sourceExists: false,
        }
        expect(ecApi.lastRenderConfig).toMatchObject(renderConfig)
        expect(log).toHaveBeenCalledWith('ec_render_iframe', renderConfig)
      })

      describe('when tracking is disabled', () => {
        beforeEach(() => {
          ecApi = new EnhancedContentApi(settingsNoTrack, makeContext(), { log })
          PerProductConfigCacheMock.mock.instances[1].getConfig.mockImplementation(() => ({
            content,
          }))
        })

        test('it falls back to default source', async () => {
          selectSourceMock.mockImplementation(() => content[1])
          const container = document.createElement('div')
          await ecApi.renderIframe(container, 'existing-product', 'id-type')
          expect(container.firstElementChild?.getAttribute('src')).toBe(`${productWithEcPath}/index.html`)
          expect(PerProductConfigCacheMock.mock.instances.length).toBe(2)
          expect(PerProductConfigCacheMock.mock.instances[1].getConfig).toHaveBeenCalledTimes(1)
          expect(PerProductConfigCacheMock.mock.instances[1].getConfig).toHaveBeenCalledWith(productWithEcPath)
          expect(headMock).toHaveBeenCalledTimes(3)
          expect(headMock).toHaveBeenCalledWith(`${productWithEcPath}/foo.html`)
          expect(headMock).toHaveBeenCalledWith(`${productWithEcPath}/bar.html`)
          expect(headMock).toHaveBeenCalledWith(`${productWithEcPath}/index.html`)
          const renderConfig = {
            idType: 'id-type',
            productId: 'existing-product',
            content,
            allContentExists: true,
            source: 'index.html',
            sourceExists: true,
          }
          expect(ecApi.lastRenderConfig).toMatchObject(renderConfig)
          expect(log).toHaveBeenCalledWith('ec_render_iframe', renderConfig)
        })
      })

      describe('when all content is missing', () => {
        beforeEach(() => {
          headMock.mockImplementation(() => makeResponse(emptyContent))
        })

        test('it reports allContentExists: false, sourceExists: false', async () => {
          selectSourceMock.mockImplementation(() => content[1])
          const container = document.createElement('div')
          await ecApi.renderIframe(container, 'existing-product', 'id-type')
          expect(container.firstElementChild?.getAttribute('src')).toBe(`${productWithEcPath}/bar.html`)
          expect(PerProductConfigCacheMock.mock.instances[0].getConfig).toHaveBeenCalledTimes(1)
          expect(PerProductConfigCacheMock.mock.instances[0].getConfig).toHaveBeenCalledWith(productWithEcPath)
          expect(headMock).toHaveBeenCalledWith(`${productWithEcPath}/foo.html`)
          expect(headMock).toHaveBeenCalledWith(`${productWithEcPath}/bar.html`)
          expect(headMock).toHaveBeenCalledTimes(2)
          const renderConfig = {
            idType: 'id-type',
            productId: 'existing-product',
            content,
            allContentExists: false,
            source: 'bar.html',
            sourceExists: false,
          }
          expect(ecApi.lastRenderConfig).toMatchObject(renderConfig)
          expect(log).toHaveBeenCalledWith('ec_render_iframe', renderConfig)
        })
      })

      describe('when some content is missing, but selected source is not', () => {
        beforeEach(() => {
          headMock.mockImplementation((url: string) =>
            makeResponse(url.match(/\/bar\.html$/) ? emptyContent : exampleContent)
          )
        })

        test('it reports allContentExists: false, sourceExists: true', async () => {
          selectSourceMock.mockImplementation(() => content[0])
          const container = document.createElement('div')
          await ecApi.renderIframe(container, 'existing-product', 'id-type')
          expect(container.firstElementChild?.getAttribute('src')).toBe(`${productWithEcPath}/foo.html`)
          expect(PerProductConfigCacheMock.mock.instances[0].getConfig).toHaveBeenCalledTimes(1)
          expect(PerProductConfigCacheMock.mock.instances[0].getConfig).toHaveBeenCalledWith(productWithEcPath)
          expect(headMock).toHaveBeenCalledTimes(2)
          const renderConfig = {
            idType: 'id-type',
            productId: 'existing-product',
            content,
            allContentExists: false,
            source: 'foo.html',
            sourceExists: true,
          }
          expect(ecApi.lastRenderConfig).toMatchObject(renderConfig)
          expect(log).toHaveBeenCalledWith('ec_render_iframe', renderConfig)
        })
      })
    })

    describe('with only null config content', () => {
      const content = [{ source: null, weight: 1 }]

      beforeEach(() => {
        PerProductConfigCacheMock.mock.instances[0].getConfig.mockImplementation(() => ({
          content,
        }))
      })

      test('it does not render an iframe and reports allContentExists: false, sourceExists: false', async () => {
        selectSourceMock.mockImplementation(() => content[0])
        const container = document.createElement('div')
        await ecApi.renderIframe(container, 'existing-product', 'id-type')
        expect(container.firstElementChild).toBeNull()
        expect(PerProductConfigCacheMock.mock.instances.length).toBe(1)
        expect(PerProductConfigCacheMock.mock.instances[0].getConfig).toHaveBeenCalledTimes(1)
        expect(PerProductConfigCacheMock.mock.instances[0].getConfig).toHaveBeenCalledWith(productWithEcPath)
        expect(headMock).toHaveBeenCalledTimes(0)
        const renderConfig = {
          idType: 'id-type',
          productId: 'existing-product',
          content,
          allContentExists: false,
          source: null,
          sourceExists: false,
        }
        expect(ecApi.lastRenderConfig).toMatchObject(renderConfig)
        expect(log).toHaveBeenCalledWith('ec_render_iframe', renderConfig)
      })
    })

    describe('beforeRender', () => {
      let beforeRender: () => void
      let ecApi: EnhancedContentApi

      beforeEach(() => {
        beforeRender = jest.fn()
        ecApi = new EnhancedContentApi(settings, makeContext(), { log: jest.fn() }, { beforeRender })
      })

      test('calls beforeRender when set', async () => {
        const container = document.createElement('div')
        await ecApi.renderIframe(container, 'product', 'foo')
        expect(beforeRender).toHaveBeenCalledTimes(1)
      })
    })
  })

  describe('updateLanguageCode', () => {
    test('it updates the language code used for subsequent requests', async () => {
      const newLanguageCode = 'new-lang-code'
      ecApi.updateLanguageCode(newLanguageCode)

      const container = document.createElement('div')
      await ecApi.renderIframe(container, 'existing-product', 'id-type')

      const productWithNewLanguageCode = `${productWithEcPath}/index.html`.replace('lang-code', newLanguageCode)

      expect(container.firstElementChild?.getAttribute('src')).toBe(productWithNewLanguageCode)
    })
  })
})
