/**
 * @jest-environment jsdom
 */

import { createLogger } from '../utils/logger'
import SdkApi, { Context } from '../api'
import * as EnhancedContentApi from '../enhancedContent'
import { getCookie, setCookie } from '../utils/cookies'
import { makeResponse } from '../__tests__/helpers'
import request from '../utils/request'

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

const headMock = request.head as jest.Mock
const getMock = request.get as jest.Mock

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

const logMock = jest.fn()
jest.mock('../utils/logger')
let context: Context | undefined
;(createLogger as jest.Mock).mockImplementation(function (ctx: Context) {
  context = ctx
  return { log: logMock }
})

interface Cookies {
  [key: string]: string | undefined
}
let cookies: Cookies = {}
jest.mock('../utils/cookies', () => {
  return {
    setCookie: jest.fn((key: string, val: string) => (cookies[key] = val)),
    getCookie: jest.fn((key: string) => cookies[key]),
    deleteCookie: jest.fn((key: string) => delete cookies[key]),
  }
})

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

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}$/

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

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

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

  test('constructed uninitialized', () => {
    expect(sdk.initialized).toBe(false)

    const accessEc = (): EnhancedContentApi.default => sdk.enhancedContent
    expect(accessEc).toThrow('Salsify Experiences SDK has not been initialized.')
  })

  test('can only be initialized once', () => {
    sdk.init(defaultOptions)

    const reinit = (): void => sdk.init(defaultOptions)
    expect(reinit).toThrow('Salsify Experiences SDK has already been initialized.')
  })

  test('allows API access once initialized', () => {
    sdk.init(defaultOptions)
    expect(sdk.enhancedContent).toBeTruthy()
  })

  test('allow events API access once initialized', () => {
    sdk.init(defaultOptions)
    expect(sdk.events).toBeTruthy()
  })

  test('cookie is not set when tracking is false', () => {
    sdk.init({ ...defaultOptions, ...{ tracking: false } })
    expect(getCookie('salsify_session_id')).toBeUndefined()
  })

  test('cookie is set by default', () => {
    sdk.init(defaultOptions)
    expect(getCookie('salsify_session_id')).toBeDefined()
  })

  test('cookie is set when tracking is true', () => {
    sdk.init({ ...defaultOptions, ...{ tracking: true } })
    expect(getCookie('salsify_session_id')).toBeDefined()
  })

  test('cookie is remove if already set when tracking is set to false', () => {
    setCookie('salsify_session_id', 'abc')
    sdk.init({ ...defaultOptions, ...{ tracking: false } })
    expect(getCookie('salsify_session_id')).toBeUndefined()
  })

  test('iframe context listener is attached', () => {
    window.addEventListener = jest.fn()

    sdk.init({ ...defaultOptions, ...{ tracking: false } })

    expect(window.addEventListener).toHaveBeenCalledTimes(2)
  })

  test('iframe context listener called with tracking: false', () => {
    const attachIframeContextListenerSpy = jest.spyOn(EnhancedContentApi, 'attachIframeContextListener')

    sdk.init({ ...defaultOptions, ...{ tracking: false } })

    expect(attachIframeContextListenerSpy).toHaveBeenCalledWith(
      expect.objectContaining({
        tracking: false,
        sessionId: 'NOT_TRACKED',
        pageSessionId: expect.stringMatching(uuidRegex),
      })
    )
  })

  test('iframe context listener called with tracking: true', () => {
    const attachIframeContextListenerSpy = jest.spyOn(EnhancedContentApi, 'attachIframeContextListener')

    sdk.init({ ...defaultOptions, ...{ tracking: true } })

    expect(attachIframeContextListenerSpy).toHaveBeenCalledWith(
      expect.objectContaining({
        tracking: true,
        sessionId: expect.stringMatching(uuidRegex),
        pageSessionId: expect.stringMatching(uuidRegex),
      })
    )
  })

  test('it sends and restarts time-on-page on navigation event', async () => {
    sdk.init({ ...defaultOptions, ...{ tracking: true } })
    await sdk.enhancedContent.renderIframe(document.createElement('div'), 'foo', 'bar')
    sdk.events.navigation({ productIdType: 'foo', productId: 'bar' })
    expect(logMock.mock.calls.filter(([event]) => event === 'time_on_page')).toEqual([
      ['time_on_page', expect.objectContaining({ timeOnPage: expect.any(Number) })],
    ])
  })

  describe('crypto.randomUUID not available (e.g. insecure context)', () => {
    const { randomUUID } = crypto

    beforeEach(() => {
      // @ts-ignore
      crypto.randomUUID = undefined
    })

    afterEach(() => {
      crypto.randomUUID = randomUUID
    })

    test('sessionId is undefined', () => {
      sdk.init(defaultOptions)
      expect(context).toMatchObject({ sessionId: undefined })
    })

    test('pageSessionId is undefined', () => {
      sdk.init(defaultOptions)
      expect(context).toMatchObject({ pageSessionId: undefined })
    })

    test('pageSessionId is still undefined after reset', () => {
      sdk.init(defaultOptions)
      sdk.events.navigation({ productIdType: 'foo', productId: 'bar' })
      expect(context).toMatchObject({ pageSessionId: undefined })
    })
  })
})
