import jsdom, { JSDOM } from "jsdom"
import { loadBudibase } from "../src/index"

export const load = async (page, screens, url, host = "test.com") => {
  screens = screens || []
  url = url || "/"

  const fullUrl = `http://${host}${url}`
  const cookieJar = new jsdom.CookieJar()
  const cookie = `${btoa("{}")}.${btoa('{"appId":"TEST_APP_ID"}')}.signature`
  cookieJar.setCookie(
    `budibase:token=${cookie};domain=${host};path=/`,
    fullUrl,
    {
      looseMode: false,
    },
    () => {}
  )

  const dom = new JSDOM("<!DOCTYPE html><html><body></body><html>", {
    url: fullUrl,
    cookieJar,
  })

  autoAssignIds(page.props)
  for (let s of screens) {
    autoAssignIds(s.props)
  }
  setAppDef(dom.window, page, screens)
  addWindowGlobals(dom.window, page, screens, {
    hierarchy: {},
    actions: [],
    triggers: [],
  })
  setComponentCodeMeta(page, screens)
  const app = await loadBudibase({
    componentLibraries: allLibs(dom.window),
    window: dom.window,
    localStorage: createLocalStorage(),
  })
  return { dom, app }
}

const addWindowGlobals = (window, page, screens) => {
  window["##BUDIBASE_FRONTEND_DEFINITION##"] = {
    page,
    screens,
  }
}

export const makePage = props => ({ props })
export const makeScreen = (route, props) => ({ props, route })

export const timeout = ms => new Promise(resolve => setTimeout(resolve, ms))

export const walkComponentTree = (node, action) => {
  action(node)

  // works for nodes or props
  const children = node.children || node._children

  if (children) {
    for (let child of children) {
      walkComponentTree(child, action)
    }
  }
}

// this happens for real by the builder...
// ..this only assigns _ids when missing
const autoAssignIds = (props, count = 0) => {
  if (!props._id) {
    props._id = `auto_id_${count}`
  }
  if (props._children) {
    for (let child of props._children) {
      count += 1
      autoAssignIds(child, count)
    }
  }
}

// any component with an id that include "based_on_store" is
// assumed to have code that depends on store value
const setComponentCodeMeta = (page, screens) => {
  const setComponentCodeMeta_single = props => {
    walkComponentTree(props, c => {
      if (c._id.indexOf("based_on_store") >= 0) {
        c._codeMeta = { dependsOnStore: true }
      }
    })
  }
  setComponentCodeMeta_single(page.props)
  for (let s of screens || []) {
    setComponentCodeMeta_single(s.props)
  }
}

const setAppDef = (window, page, screens) => {
  window["##BUDIBASE_FRONTEND_DEFINITION##"] = {
    componentLibraries: [],
    page,
    screens,
  }
}

const allLibs = window => ({
  testlib: maketestlib(window),
})

const createLocalStorage = () => {
  const data = {}
  return {
    getItem: key => data[key],
    setItem: (key, value) => (data[key] = value),
  }
}

const maketestlib = window => ({
  div: function(opts) {
    const node = window.document.createElement("DIV")
    const defaultChild = window.document.createElement("DIV")
    defaultChild.className = "default-child"
    node.appendChild(defaultChild)

    let currentProps = { ...opts.props }
    let childNodes = []

    const set = props => {
      currentProps = Object.assign(currentProps, props)
      node.className = currentProps.className || ""
      if (currentProps._children && currentProps._children.length > 0) {
        if (currentProps.append) {
          for (let c of childNodes) {
            node.removeChild(c)
          }
          const components = currentProps._bb.attachChildren(node, {
            hydrate: false,
          })
          childNodes = components.map(c => c.component._element)
        } else {
          currentProps._bb.attachChildren(node)
        }
      }
    }

    this.$destroy = () => opts.target.removeChild(node)

    this.$set = set
    this._element = node
    set(opts.props)
    opts.target.appendChild(node)
  },

  h1: function(opts) {
    const node = window.document.createElement("H1")

    let currentProps = { ...opts.props }

    const set = props => {
      currentProps = Object.assign(currentProps, props)
      if (currentProps.text) {
        node.innerText = currentProps.text
      }
    }

    this.$destroy = () => opts.target.removeChild(node)

    this.$set = set
    this._element = node
    set(opts.props)
    opts.target.appendChild(node)
  },

  button: function(opts) {
    const node = window.document.createElement("BUTTON")

    let currentProps = { ...opts.props }

    const set = props => {
      currentProps = Object.assign(currentProps, props)
      if (currentProps.onClick) {
        node.addEventListener("click", () => {
          currentProps._bb.call("onClick")
        })
      }
    }

    this.$destroy = () => opts.target.removeChild(node)

    this.$set = set
    this._element = node
    set(opts.props)
    opts.target.appendChild(node)
  },

  list: function(opts) {
    const node = window.document.createElement("DIV")

    let currentProps = { ...opts.props }

    const set = props => {
      currentProps = Object.assign(currentProps, props)
      if (currentProps._children && currentProps._children.length > 0) {
        currentProps._bb.attachChildren(node, {
          context: currentProps.data || {},
        })
      }
    }

    this.$destroy = () => opts.target.removeChild(node)

    this.$set = set
    this._element = node
    set(opts.props)
    opts.target.appendChild(node)
  },

  input: function(opts) {
    const node = window.document.createElement("INPUT")
    let currentProps = { ...opts.props }

    const set = props => {
      currentProps = Object.assign(currentProps, props)
      opts.props._bb.setBinding("value", props.value)
    }

    node.addEventListener("change", e => {
      opts.props._bb.setBinding("value", e.target.value)
    })

    this.$destroy = () => opts.target.removeChild(node)

    this.$set = set
    this._element = node
    set(opts.props)
    opts.target.appendChild(node)
  },
})