// @flow

import { createSlotVNodes } from './create-slot-vnodes'
import addMocks from './add-mocks'
import { addEventLogger } from './log-events'
import { addStubs } from './add-stubs'
import { compileTemplate } from 'shared/compile-template'
import extractInstanceOptions from './extract-instance-options'
import { componentNeedsCompiling, isConstructor } from 'shared/validators'
import createScopedSlots from './create-scoped-slots'
import { createStubsFromStubsObject } from './create-component-stubs'
import { patchCreateElement } from './patch-create-element'
import { keys } from 'shared/util'

function createContext(options, scopedSlots, currentProps) {
  const on = {
    ...(options.context && options.context.on),
    ...options.listeners
  }
  return {
    attrs: {
      ...options.attrs,
      // pass as attrs so that inheritAttrs works correctly
      // props should take precedence over attrs
      ...currentProps
    },
    ...(options.context || {}),
    on,
    scopedSlots
  }
}

function createChildren(vm, h, { slots, context }) {
  const slotVNodes = slots ? createSlotVNodes(vm, slots) : undefined
  return (
    (context &&
      context.children &&
      context.children.map(x => (typeof x === 'function' ? x(h) : x))) ||
    slotVNodes
  )
}

function getValuesFromCallableOption(optionValue) {
  if (typeof optionValue === 'function') {
    return optionValue.call(this)
  }
  return optionValue
}

export default function createInstance(
  component: Component,
  options: NormalizedOptions,
  _Vue: Component
): Component {
  const componentOptions = isConstructor(component)
    ? component.options
    : component

  // instance options are options that are passed to the
  // root instance when it's instantiated
  const instanceOptions = extractInstanceOptions(options)

  const globalComponents = _Vue.options.components || {}
  const componentsToStub = Object.assign(
    Object.create(globalComponents),
    componentOptions.components
  )

  const stubComponentsObject = createStubsFromStubsObject(
    componentsToStub,
    // $FlowIgnore
    options.stubs,
    _Vue
  )

  addEventLogger(_Vue)
  addMocks(_Vue, options.mocks)
  addStubs(_Vue, stubComponentsObject)
  patchCreateElement(_Vue, stubComponentsObject, options.shouldProxy)

  if (componentNeedsCompiling(componentOptions)) {
    compileTemplate(componentOptions)
  }

  // used to identify extended component using constructor
  componentOptions.$_vueTestUtils_original = component

  // watchers provided in mounting options should override preexisting ones
  if (componentOptions.watch && instanceOptions.watch) {
    const componentWatchers = keys(componentOptions.watch)
    const instanceWatchers = keys(instanceOptions.watch)

    for (let i = 0; i < instanceWatchers.length; i++) {
      const k = instanceWatchers[i]
      // override the componentOptions with the one provided in mounting options
      if (componentWatchers.includes(k)) {
        componentOptions.watch[k] = instanceOptions.watch[k]
      }
    }
  }

  // make sure all extends are based on this instance
  const Constructor = _Vue.extend(componentOptions).extend(instanceOptions)
  Constructor.options._base = _Vue

  const scopedSlots = createScopedSlots(options.scopedSlots, _Vue)

  const parentComponentOptions = options.parentComponent || {}

  const originalParentComponentProvide = parentComponentOptions.provide
  parentComponentOptions.provide = function () {
    return {
      ...getValuesFromCallableOption.call(this, originalParentComponentProvide),
      // $FlowIgnore
      ...getValuesFromCallableOption.call(this, options.provide)
    }
  }

  const originalParentComponentData = parentComponentOptions.data
  parentComponentOptions.data = function () {
    return {
      ...getValuesFromCallableOption.call(this, originalParentComponentData),
      vueTestUtils_childProps: { ...options.propsData }
    }
  }

  parentComponentOptions.$_doNotStubChildren = true
  parentComponentOptions.$_isWrapperParent = true
  parentComponentOptions._isFunctionalContainer = componentOptions.functional
  parentComponentOptions.render = function (h) {
    return h(
      Constructor,
      createContext(options, scopedSlots, this.vueTestUtils_childProps),
      createChildren(this, h, options)
    )
  }

  // options  "propsData" can only be used during instance creation with the `new` keyword
  // "data" should be set only on component under test to avoid reactivity issues
  const { propsData, data, ...rest } = options // eslint-disable-line
  const Parent = _Vue.extend({
    ...rest,
    ...parentComponentOptions
  })

  return new Parent()
}