import * as React from 'react'
import ReactDOM, { unmountComponentAtNode } from 'react-dom'
import getDisplayName from './getDisplayName'
import { injectStylesBeforeElement } from './utils'

const rootId = 'cypress-root'

const isComponentSpec = () => Cypress.spec.specType === 'component'

function checkMountModeEnabled () {
  if (!isComponentSpec()) {
    throw new Error(
      `In order to use mount or unmount functions please place the spec in component folder`,
    )
  }
}

/**
 * Inject custom style text or CSS file or 3rd party style resources
 */
const injectStyles = (options: MountOptions) => {
  return () => {
    const document = cy.state('document')
    const el = document.getElementById(rootId)

    return injectStylesBeforeElement(options, document, el)
  }
}

/**
 * Mount a React component in a blank document; register it as an alias
 * To access: use an alias or original component reference
 * @function   mount
 * @param      {React.ReactElement}  jsx - component to mount
 * @param      {MountOptions}  [options] - options, like alias, styles
 * @see https://github.com/bahmutov/@cypress/react
 * @see https://glebbahmutov.com/blog/my-vision-for-component-tests/
 * @example
 ```
  import Hello from './hello.jsx'
  import {mount} from '@cypress/react'
  it('works', () => {
    mount(<Hello onClick={cy.stub()} />)
    // use Cypress commands
    cy.contains('Hello').click()
  })
 ```
 **/
export const mount = (jsx: React.ReactElement, options: MountOptions = {}) => {
  checkMountModeEnabled()

  // Get the display name property via the component constructor
  // @ts-ignore FIXME
  const componentName = getDisplayName(jsx.type, options.alias)
  const displayName = options.alias || componentName
  const message = options.alias
    ? `<${componentName} ... /> as "${options.alias}"`
    : `<${componentName} ... />`
  let logInstance: Cypress.Log

  return cy
  .then(() => {
    if (options.log !== false) {
      logInstance = Cypress.log({
        name: 'mount',
        message: [message],
      })
    }
  })
  .then(injectStyles(options))
  .then(() => {
    const document = cy.state('document') as Document
    const reactDomToUse = options.ReactDom || ReactDOM

    const el = document.getElementById(rootId)

    if (!el) {
      throw new Error(
        [
          '[@cypress/react] 🔥 Hmm, cannot find root element to mount the component.',
          'Did you forget to include the support file?',
          'Check https://github.com/bahmutov/cypress-react-unit-test#install please',
        ].join(' '),
      )
    }

    const key =
        // @ts-ignore provide unique key to the the wrapped component to make sure we are rerendering between tests
        (Cypress?.mocha?.getRunner()?.test?.title || '') + Math.random()
    const props = {
      key,
    }

    const reactComponent = React.createElement(
      options.strict ? React.StrictMode : React.Fragment,
      props,
      jsx,
    )
    // since we always surround the component with a fragment
    // let's get back the original component
    // @ts-ignore
    const userComponent = reactComponent.props.children

    reactDomToUse.render(reactComponent, el)

    if (logInstance) {
      const logConsoleProps = {
        props: jsx.props,
        description: 'Mounts React component',
        home: 'https://github.com/bahmutov/cypress-react-unit-test',
      }
      const componentElement = el.children[0]

      if (componentElement) {
        // @ts-ignore
        logConsoleProps.yielded = reactDomToUse.findDOMNode(componentElement)
      }

      logInstance.set('consoleProps', () => logConsoleProps)

      if (el.children.length) {
        logInstance.set(
          '$el',
            (el.children.item(0) as unknown) as JQuery<HTMLElement>,
        )
      }
    }

    return (
      cy
      .wrap(userComponent, { log: false })
      .as(displayName)
      // by waiting, we give the component's hook a chance to run
      // https://github.com/bahmutov/cypress-react-unit-test/issues/200
      .wait(1, { log: false })
      .then(() => {
        if (logInstance) {
          logInstance.snapshot('mounted')
          logInstance.end()
        }

        // by returning undefined we keep the previous subject
        // which is the mounted component
        return undefined
      })
    )
  })
}

/**
 * Removes the mounted component. Notice this command automatically
 * queues up the `unmount` into Cypress chain, thus you don't need `.then`
 * to call it.
 * @see https://github.com/bahmutov/@cypress/react/tree/main/cypress/component/basic/unmount
 * @example
  ```
  import { mount, unmount } from '@cypress/react'
  it('works', () => {
    mount(...)
    // interact with the component using Cypress commands
    // whenever you want to unmount
    unmount()
  })
  ```
 */
export const unmount = () => {
  checkMountModeEnabled()

  return cy.then(() => {
    cy.log('unmounting...')
    const selector = `#${rootId}`

    return cy.get(selector, { log: false }).then(($el) => {
      unmountComponentAtNode($el[0])
    })
  })
}

/**
 * Creates new instance of `mount` function with default options
 * @function   createMount
 * @param      {MountOptions}  [defaultOptions] - defaultOptions for returned `mount` function
 * @returns    new instance of `mount` with assigned options
 * @example
 * ```
 * import Hello from './hello.jsx'
 * import { createMount } from '@cypress/react'
 *
 * const mount = createMount({ strict: true, cssFile: 'path/to/any/css/file.css' })
 *
 * it('works', () => {
 *   mount(<Hello />)
 *   // use Cypress commands
 *   cy.get('button').should('have.css', 'color', 'rgb(124, 12, 109)')
 * })
 ```
 **/
export const createMount = (defaultOptions: MountOptions) => {
  return (
    element: React.ReactElement,
    options?: MountOptions,
  ) => {
    return mount(element, { ...defaultOptions, ...options })
  }
}

/** @deprecated Should be removed in the next major version */
export default mount
