import setupCore from '@baserow/modules/core/plugin'
import axios from 'axios'
import setupClient from '@baserow/modules/core/plugins/clientHandler'
import setupDatabasePlugin from '@baserow/modules/database/plugin'
import { bootstrapVueContext } from '@baserow/test/helpers/components'
import MockAdapter from 'axios-mock-adapter'
import _ from 'lodash'
import { MockServer } from '@baserow/test/fixtures/mockServer'
import flushPromises from 'flush-promises'

/**
 * Uses the real baserow plugins to setup a Vuex store and baserow registry
 * correctly.
 */
function _createBaserowStoreAndRegistry(app, vueContext) {
  const store = new vueContext.vuex.Store({})

  setupCore({ store, app }, (name, dep) => {
    app[`$${name}`] = dep
  })
  setupClient({ store, app }, (key, value) => {
    app[key] = value
  })
  app.$client = app.client
  store.$registry = app.$registry
  store.$client = app.client
  store.app = app
  app.$store = store
  setupDatabasePlugin({
    store,
    app,
  })
  return store
}

/**
 * An acceptance testing framework for testing Baserow components and surrounding logic
 * like stores.
 * TestApp sets up baserow components, registries and stores so they work out of the
 * box and can be tested without having to:
 *  - wait 30+ seconds for a Nuxt server to startup and build
 *  - mock out stores, registries or carve arbitrary boundaries in
 *    the tests causing problems when store and component logic is tightly
 *    coupled.
 *
 * To use create an instance of this class in your beforeAll
 * test suite hook and make sure to call testApp.afterEach() in the afterEach hook.
 *
 * The following attributes are exposed for use in your tests:
 * testApp.mockServer : a helper class providing methods to initialize a fake
 *                      baserow server with consistent test data.
 * testApp.mock       : a mock axios adapter used to mock out HTTP calls to the server,
 *                      also used by testApp.mockServer to actually do the server call
 *                      mocking.
 * testApp.store      : a Vuex store populated with Baserow's stores ready for you to
 *                      commit, get and dispatch to.
 * UIHelpers          : a collection of methods which know how to perform common actions
 *                      on Baserow's components.
 *
 */
export class TestApp {
  constructor() {
    this.mock = new MockAdapter(axios, { onNoMatch: 'throwException' })
    // In the future we can extend this stub realtime implementation to perform
    // useful testing of realtime interaction in the frontend!
    this._realtime = {
      registerEvent(e, f) {},
      subscribe(e, f) {},
    }
    // Various stub and mock attributes which will be injected into components
    // mounted using TestApp.
    this._app = {
      $realtime: this._realtime,
      $cookies: {
        set(name, id, value) {},
      },
      $env: {
        PUBLIC_WEB_FRONTEND_URL: 'https://localhost/',
      },
      i18n: {
        t: (key) => key,
        tc: (key) => key,
      },
    }
    this._vueContext = bootstrapVueContext()
    this.store = _createBaserowStoreAndRegistry(this._app, this._vueContext)
    this._initialCleanStoreState = _.cloneDeep(this.store.state)
    this.mockServer = new MockServer(this.mock, this.store)
    this.failTestOnErrorResponse = true
    this._app.client.interceptors.response.use(
      (response) => {
        return response
      },
      (error) => {
        if (this.failTestOnErrorResponse) {
          fail(error)
        }
        return Promise.reject(error)
      }
    )
  }

  dontFailOnErrorResponses() {
    this.failTestOnErrorResponse = false
  }

  failOnErrorResponses() {
    this.failTestOnErrorResponse = true
  }

  /**
   * Cleans up after a test run performed by TestApp. Make sure you call this
   * in your test suits afterEach method!
   */
  async afterEach() {
    this.mock.reset()
    this.failOnErrorResponses()
    this.store.replaceState(_.cloneDeep(this._initialCleanStoreState))
    this._vueContext.teardownVueContext()
    await flushPromises()
  }

  /**
   * Creates a fully rendered version of the provided Component and calls
   * asyncData on the component at the correct time with the provided params.
   */
  async mount(Component, { asyncDataParams = {}, ...kwargs }) {
    // Sometimes baserow directly appends to the documents body, ensure that we
    // are mounting into the document so we can correctly inspect the modals that
    // are placed there.
    const rootDiv = document.createElement('div')
    document.body.appendChild(rootDiv)

    if (Object.prototype.hasOwnProperty.call(Component, 'asyncData')) {
      const data = await Component.asyncData({
        store: this.store,
        params: asyncDataParams,
        error: fail,
        app: this._app,
      })
      Component.data = function () {
        return data
      }
    }
    const wrapper = this._vueContext.vueTestUtils.mount(Component, {
      localVue: this._vueContext.vue,
      mocks: this._app,
      attachTo: rootDiv,
      ...kwargs,
    })

    // The vm property doesn't alway exist. See https://vue-test-utils.vuejs.org/api/wrapper/#properties
    if (wrapper.vm) {
      await this.callFetchOnChildren(wrapper.vm)
    }
    return wrapper
  }

  async callFetchOnChildren(c) {
    if (c.$options.fetch) {
      const fetch = c.$options.fetch
      if (typeof fetch !== 'function') {
        throw new TypeError('fetch should be a function')
      }
      await c.$options.fetch.bind(c)()
    }
    for (const child of c.$children) {
      await this.callFetchOnChildren(child)
    }
  }
}
/**
 * Various helper functions which interact with baserow components. Lean towards
 * putting and sharing any test code which relies on specific details of how baserow
 * components are structured and styled in here This way there is a single place
 * to fix when changes are made to the components instead of 30 different test cases.
 */
export const UIHelpers = {
  async performSearch(tableComponent, searchTerm) {
    await tableComponent.get('i.header__search-icon').trigger('click')
    const searchBox = tableComponent.get(
      'input[placeholder*="viewSearchContext.searchInRows"]'
    )
    await searchBox.setValue(searchTerm)
    await searchBox.trigger('submit')
    await flushPromises()
  },
  async startEditForCellContaining(tableComponent, htmlInsideCellToSearchFor) {
    const targetCell = tableComponent
      .findAll('.grid-view__cell')
      .filter((w) => w.html().includes(htmlInsideCellToSearchFor))
      .at(0)

    await targetCell.trigger('click')

    const activeCell = tableComponent.get('.grid-view__cell.active')
    // Double click to start editing cell
    await activeCell.trigger('click')
    await activeCell.trigger('click')

    return activeCell.find('input')
  },
}