import path from "path"
import {
  getRecordApi,
  getCollectionApi,
  getIndexApi,
  getActionsApi,
} from "../src"
import memory from "./memory"
import { setupDatastore } from "../src/appInitialise"
import {
  configFolder,
  fieldDefinitions,
  templateDefinitions,
  joinKey,
  isSomething,
  crypto as nodeCrypto,
} from "../src/common"
import { getNewIndexTemplate } from "../src/templateApi/createNodes"
import { indexTypes } from "../src/templateApi/indexes"
import getTemplateApi from "../src/templateApi"
import getAuthApi from "../src/authApi"
import { createEventAggregator } from "../src/appInitialise/eventAggregator"
import { filter, find } from "lodash/fp"
import { createBehaviourSources } from "../src/actionsApi/buildBehaviourSource"
import { createAction, createTrigger } from "../src/templateApi/createActions"
import { initialiseActions } from "../src/actionsApi/initialise"
import { setCleanupFunc } from "../src/transactions/setCleanupFunc"
import { permission } from "../src/authApi/permissions"
import { generateFullPermissions } from "../src/authApi/generateFullPermissions"
import { initialiseData } from "../src/appInitialise/initialiseData"

export const testFileArea = testNameArea =>
  path.join("test", "fs_test_area", testNameArea)
export const testConfigFolder = testAreaName =>
  path.join(testFileArea(testAreaName), configFolder)
export const testFieldDefinitionsPath = testAreaName =>
  path.join(testFileArea(testAreaName), fieldDefinitions)
export const testTemplatesPath = testAreaName =>
  path.join(testFileArea(testAreaName), templateDefinitions)

export const getMemoryStore = () => setupDatastore(memory({}))
export const getMemoryTemplateApi = store => {
  const app = {
    datastore: store || getMemoryStore(),
    publish: () => {},
    getEpochTime: async () => new Date().getTime(),
    user: { name: "", permissions: [permission.writeTemplates.get()] },
  }
  app.removePermission = removePermission(app)
  app.withOnlyThisPermission = withOnlyThisPermission(app)
  app.withNoPermissions = withNoPermissions(app)
  const templateApi = getTemplateApi(app)
  templateApi._eventAggregator = createEventAggregator()
  templateApi._storeHandle = app.datastore
  return { templateApi, app }
}

// TODO: subscribe actions
export const appFromTempalteApi = async (
  templateApi,
  disableCleanupTransactions = false
) => {
  const appDef = await templateApi.getApplicationDefinition()
  const app = {
    hierarchy: appDef.hierarchy,
    datastore: templateApi._storeHandle,
    publish: templateApi._eventAggregator.publish,
    _eventAggregator: templateApi._eventAggregator,
    getEpochTime: async () => new Date().getTime(),
    crypto: nodeCrypto,
    user: { name: "bob", permissions: [] },
    actions: {},
  }
  app.removePermission = removePermission(app)
  app.withOnlyThisPermission = withOnlyThisPermission(app)
  app.withNoPermissions = withNoPermissions(app)

  const fullPermissions = generateFullPermissions(app)
  app.user.permissions = fullPermissions

  if (disableCleanupTransactions) setCleanupFunc(app, async () => {})
  else setCleanupFunc(app)

  return app
}

const removePermission = app => perm => {
  app.user.permissions = filter(
    p =>
      p.type !== perm.type ||
      (isSomething(perm.nodeKey) && perm.nodeKey !== p.nodeKey)
  )(app.user.permissions)
}

const withOnlyThisPermission = app => perm => (app.user.permissions = [perm])

const withNoPermissions = app => () => (app.user.permissions = [])

export const getRecordApiFromTemplateApi = async (
  templateApi,
  disableCleanupTransactions = false
) => {
  const app = await appFromTempalteApi(templateApi, disableCleanupTransactions)
  const recordapi = getRecordApi(app)
  recordapi._storeHandle = app.datastore
  return recordapi
}

export const getCollectionApiFromTemplateApi = async (
  templateApi,
  disableCleanupTransactions = false
) =>
  getCollectionApi(
    await appFromTempalteApi(templateApi, disableCleanupTransactions)
  )

export const getIndexApiFromTemplateApi = async (
  templateApi,
  disableCleanupTransactions = false
) =>
  getIndexApi(await appFromTempalteApi(templateApi, disableCleanupTransactions))

export const getAuthApiFromTemplateApi = async (
  templateApi,
  disableCleanupTransactions = false
) =>
  getAuthApi(await appFromTempalteApi(templateApi, disableCleanupTransactions))

export const findIndex = (parentNode, name) =>
  find(i => i.name === name)(parentNode.indexes)

export const findCollectionDefaultIndex = recordCollectionNode =>
  findIndex(recordCollectionNode.parent(), recordCollectionNode.name + "_index")

export const hierarchyFactory = (...additionalFeatures) => templateApi => {
  const root = templateApi.getNewRootLevel()

  const settingsRecord = templateApi.getNewSingleRecordTemplate(root)
  settingsRecord.name = "settings"

  const customerRecord = templateApi.getNewRecordTemplate(root, "customer")
  customerRecord.collectionName = "customers"
  findCollectionDefaultIndex(customerRecord).map =
    "return {surname:record.surname, isalive:record.isalive, partner:record.partner};"

  const partnerRecord = templateApi.getNewRecordTemplate(root, "partner")
  partnerRecord.collectionName = "partners"

  const partnerInvoiceRecord = templateApi.getNewRecordTemplate(
    partnerRecord,
    "invoice"
  )
  partnerInvoiceRecord.collectionName = "invoices"
  findCollectionDefaultIndex(partnerInvoiceRecord).name =
    "partnerInvoices_index"

  const invoiceRecord = templateApi.getNewRecordTemplate(
    customerRecord,
    "invoice"
  )
  invoiceRecord.collectionName = "invoices"
  findCollectionDefaultIndex(invoiceRecord).map =
    "return {createdDate: record.createdDate, totalIncVat: record.totalIncVat};"

  const chargeRecord = templateApi.getNewRecordTemplate(invoiceRecord, "charge")
  chargeRecord.collectionName = "charges"

  const hierarchy = {
    root,
    customerRecord,
    invoiceRecord,
    partnerRecord,
    partnerInvoiceRecord,
    chargeRecord,
    settingsRecord,
  }

  for (let feature of additionalFeatures) {
    feature(hierarchy, templateApi)
  }
  return hierarchy
}

export const basicAppHierarchyCreator = templateApis =>
  hierarchyFactory()(templateApis)

export const withFields = (hierarchy, templateApi) => {
  const {
    customerRecord,
    invoiceRecord,
    partnerInvoiceRecord,
    chargeRecord,
    partnerRecord,
    settingsRecord,
    root,
  } = hierarchy

  getNewFieldAndAdd(templateApi, settingsRecord)("appName", "string", "")

  const newCustomerField = getNewFieldAndAdd(templateApi, customerRecord)

  const partnersReferenceIndex = templateApi.getNewIndexTemplate(root)
  partnersReferenceIndex.name = "partnersReference"
  partnersReferenceIndex.map =
    "return {name:record.businessName, phone:record.phone};"
  partnersReferenceIndex.allowedRecordNodeIds = [partnerRecord.nodeId]

  const partnerCustomersReverseIndex = templateApi.getNewIndexTemplate(
    partnerRecord,
    indexTypes.reference
  )
  partnerCustomersReverseIndex.name = "partnerCustomers"
  partnerCustomersReverseIndex.map = "return {...record};"
  partnerCustomersReverseIndex.filter = "record.isalive === true"
  partnerCustomersReverseIndex.allowedRecordNodeIds = [customerRecord.nodeId]
  hierarchy.partnerCustomersReverseIndex = partnerCustomersReverseIndex

  newCustomerField("surname", "string")
  newCustomerField("isalive", "bool", "true")
  newCustomerField("createddate", "datetime")
  newCustomerField("age", "number")
  newCustomerField("profilepic", "file")
  newCustomerField("partner", "reference", undefined, {
    indexNodeKey: "/partnersReference",
    displayValue: "name",
    reverseIndexNodeKeys: [
      joinKey(partnerRecord.nodeKey(), "partnerCustomers"),
    ],
  })

  const referredToCustomersReverseIndex = templateApi.getNewIndexTemplate(
    customerRecord,
    indexTypes.reference
  )
  referredToCustomersReverseIndex.name = "referredToCustomers"
  referredToCustomersReverseIndex.map = "return {...record};"
  referredToCustomersReverseIndex.getShardName =
    "return !record.surname ? 'null' : record.surname.substring(0,1);"
  referredToCustomersReverseIndex.allowedRecordNodeIds = [customerRecord.nodeId]
  hierarchy.referredToCustomersReverseIndex = referredToCustomersReverseIndex

  const customerReferredByField = newCustomerField(
    "referredBy",
    "reference",
    undefined,
    {
      indexNodeKey: "/customer_index",
      displayValue: "surname",
      reverseIndexNodeKeys: [
        joinKey(customerRecord.nodeKey(), "referredToCustomers"),
      ],
    }
  )
  hierarchy.customerReferredByField = customerReferredByField

  const newInvoiceField = getNewFieldAndAdd(templateApi, invoiceRecord)

  const invoiceTotalIncVatField = newInvoiceField("totalIncVat", "number")
  invoiceTotalIncVatField.typeOptions.decimalPlaces = 2
  newInvoiceField("createdDate", "datetime")
  newInvoiceField("paidAmount", "number")
  newInvoiceField("invoiceType", "string")
  newInvoiceField("isWrittenOff", "bool")

  const newPartnerField = getNewFieldAndAdd(templateApi, partnerRecord)
  newPartnerField("businessName", "string")
  newPartnerField("phone", "string")

  const newPartnerInvoiceField = getNewFieldAndAdd(
    templateApi,
    partnerInvoiceRecord
  )
  const partnerInvoiceTotalIncVatVield = newPartnerInvoiceField(
    "totalIncVat",
    "number"
  )
  partnerInvoiceTotalIncVatVield.typeOptions.decimalPlaces = 2
  newPartnerInvoiceField("createdDate", "datetime")
  newPartnerInvoiceField("paidAmount", "number")

  const newChargeField = getNewFieldAndAdd(templateApi, chargeRecord)
  newChargeField("amount", "number")

  newChargeField("partnerInvoice", "reference", undefined, {
    reverseIndexNodeKeys: [
      joinKey(partnerInvoiceRecord.nodeKey(), "partnerCharges"),
    ],
    displayValue: "createdDate",
    indexNodeKey: joinKey(partnerRecord.nodeKey(), "partnerInvoices_index"),
  })

  const partnerChargesReverseIndex = templateApi.getNewIndexTemplate(
    partnerInvoiceRecord,
    indexTypes.reference
  )
  partnerChargesReverseIndex.name = "partnerCharges"
  partnerChargesReverseIndex.map = "return {...record};"
  partnerChargesReverseIndex.allowedRecordNodeIds = [chargeRecord]
  hierarchy.partnerChargesReverseIndex = partnerChargesReverseIndex

  const customersReferenceIndex = templateApi.getNewIndexTemplate(
    hierarchy.root
  )
  customersReferenceIndex.name = "customersReference"
  customersReferenceIndex.map = "return {name:record.surname}"
  customersReferenceIndex.filter = "record.isalive === true"
  customersReferenceIndex.allowedRecordNodeIds = [customerRecord.nodeId]

  newInvoiceField("customer", "reference", undefined, {
    indexNodeKey: "/customersReference",
    reverseIndexNodeKeys: [findCollectionDefaultIndex(invoiceRecord).nodeKey()],
    displayValue: "name",
  })
}

export const withIndexes = (hierarchy, templateApi) => {
  const {
    root,
    customerRecord,
    partnerInvoiceRecord,
    invoiceRecord,
    partnerRecord,
    chargeRecord,
  } = hierarchy
  const deceasedCustomersIndex = getNewIndexTemplate(root)
  deceasedCustomersIndex.name = "deceased"
  deceasedCustomersIndex.map =
    "return {surname: record.surname, age:record.age};"
  deceasedCustomersIndex.filter = "record.isalive === false"
  findCollectionDefaultIndex(customerRecord).map = "return record;"
  deceasedCustomersIndex.allowedRecordNodeIds = [customerRecord.nodeId]

  findCollectionDefaultIndex(invoiceRecord).allowedRecordNodeIds = [
    invoiceRecord.nodeId,
  ]
  findCollectionDefaultIndex(customerRecord).allowedRecordNodeIds = [
    customerRecord.nodeId,
  ]
  findCollectionDefaultIndex(partnerRecord).allowedRecordNodeIds = [
    partnerRecord.nodeId,
  ]
  findIndex(partnerRecord, "partnerInvoices_index").allowedRecordNodeIds = [
    partnerInvoiceRecord.nodeId,
  ]
  findCollectionDefaultIndex(chargeRecord).allowedRecordNodeIds = [
    chargeRecord.nodeId,
  ]

  const customerInvoicesIndex = getNewIndexTemplate(root)
  customerInvoicesIndex.name = "customer_invoices"
  customerInvoicesIndex.map = "return record;"
  customerInvoicesIndex.filter = "record.type === 'invoice'"
  customerInvoicesIndex.allowedRecordNodeIds = [invoiceRecord.nodeId]

  const outstandingInvoicesIndex = getNewIndexTemplate(root)
  outstandingInvoicesIndex.name = "Outstanding Invoices"
  outstandingInvoicesIndex.filter =
    "record.type === 'invoice' && record.paidAmount < record.totalIncVat"
  outstandingInvoicesIndex.map = "return {...record};"
  outstandingInvoicesIndex.allowedRecordNodeIds = [
    invoiceRecord.nodeId,
    partnerInvoiceRecord.nodeId,
  ]

  const allInvoicesAggregateGroup = templateApi.getNewAggregateGroupTemplate(
    outstandingInvoicesIndex
  )
  allInvoicesAggregateGroup.name = "all_invoices"

  const allInvoicesByType = templateApi.getNewAggregateGroupTemplate(
    outstandingInvoicesIndex
  )
  allInvoicesByType.groupBy = "return record.invoiceType"
  allInvoicesByType.name = "all_invoices_by_type"

  const allInvoicesTotalAmountAggregate = templateApi.getNewAggregateTemplate(
    allInvoicesByType
  )
  allInvoicesTotalAmountAggregate.name = "totalIncVat"
  allInvoicesTotalAmountAggregate.aggregatedValue = "return record.totalIncVat"

  const allInvoicesPaidAmountAggregate = templateApi.getNewAggregateTemplate(
    allInvoicesByType
  )
  allInvoicesPaidAmountAggregate.name = "paidAmount"
  allInvoicesPaidAmountAggregate.aggregatedValue = "return record.paidAmount"

  const writtenOffInvoicesByType = templateApi.getNewAggregateGroupTemplate(
    outstandingInvoicesIndex
  )
  writtenOffInvoicesByType.groupBy = "return record.invoiceType"
  writtenOffInvoicesByType.name = "written_off"
  writtenOffInvoicesByType.condition = "record.isWrittenOff === true"

  const writtenOffInvoicesTotalAmountAggregate = templateApi.getNewAggregateTemplate(
    writtenOffInvoicesByType
  )
  writtenOffInvoicesTotalAmountAggregate.name = "totalIncVat"
  writtenOffInvoicesTotalAmountAggregate.aggregatedValue =
    "return record.totalIncVat"

  const customersBySurnameIndex = templateApi.getNewIndexTemplate(root)
  customersBySurnameIndex.name = "customersBySurname"
  customersBySurnameIndex.map = "return {...record};"
  customersBySurnameIndex.filter = ""
  customersBySurnameIndex.allowedRecordNodeIds = [customerRecord.nodeId]
  customersBySurnameIndex.getShardName =
    "return !record.surname ? 'null' : record.surname.substring(0,1);"

  const customersDefaultIndex = findCollectionDefaultIndex(customerRecord)
  const customersNoGroupaggregateGroup = templateApi.getNewAggregateGroupTemplate(
    customersDefaultIndex
  )
  customersNoGroupaggregateGroup.name = "Customers Summary"
  const allCustomersAgeFunctions = templateApi.getNewAggregateTemplate(
    customersNoGroupaggregateGroup
  )
  allCustomersAgeFunctions.aggregatedValue = "return record.age"
  allCustomersAgeFunctions.name = "all customers - age breakdown"

  const invoicesByOutstandingIndex = templateApi.getNewIndexTemplate(
    customerRecord
  )
  invoicesByOutstandingIndex.name = "invoicesByOutstanding"
  invoicesByOutstandingIndex.map = "return {...record};"
  invoicesByOutstandingIndex.filter = ""
  invoicesByOutstandingIndex.getShardName =
    "return (record.totalIncVat > record.paidAmount ? 'outstanding' : 'paid');"
  invoicesByOutstandingIndex.allowedRecordNodeIds = [
    partnerInvoiceRecord.nodeId,
    invoiceRecord.nodeId,
  ]
  const allInvoicesByType_Sharded = templateApi.getNewAggregateGroupTemplate(
    invoicesByOutstandingIndex
  )
  allInvoicesByType_Sharded.groupBy = "return record.invoiceType"
  allInvoicesByType_Sharded.name = "all_invoices_by_type"

  const allInvoicesTotalAmountAggregate_Sharded = templateApi.getNewAggregateTemplate(
    allInvoicesByType_Sharded
  )
  allInvoicesTotalAmountAggregate_Sharded.name = "totalIncVat"
  allInvoicesTotalAmountAggregate_Sharded.aggregatedValue =
    "return record.totalIncVat"

  hierarchy.allInvoicesByType = allInvoicesByType
  hierarchy.allInvoicesTotalAmountAggregate = allInvoicesTotalAmountAggregate
  hierarchy.allInvoicesPaidAmountAggregate = allInvoicesPaidAmountAggregate
  hierarchy.customersDefaultIndex = customersDefaultIndex
  hierarchy.allCustomersAgeFunctions = allCustomersAgeFunctions
  hierarchy.customersNoGroupaggregateGroup = customersNoGroupaggregateGroup
  hierarchy.invoicesByOutstandingIndex = invoicesByOutstandingIndex
  hierarchy.customersBySurnameIndex = customersBySurnameIndex
  hierarchy.outstandingInvoicesIndex = outstandingInvoicesIndex
  hierarchy.deceasedCustomersIndex = deceasedCustomersIndex
  hierarchy.customerInvoicesIndex = customerInvoicesIndex
}

export const basicAppHierarchyCreator_WithFields = templateApi =>
  hierarchyFactory(withFields)(templateApi)

export const basicAppHierarchyCreator_WithFields_AndIndexes = templateApi =>
  hierarchyFactory(withFields, withIndexes)(templateApi)

export const setupApphierarchy = async (
  creator,
  disableCleanupTransactions = false
) => {
  const { templateApi } = getMemoryTemplateApi()
  const hierarchy = creator(templateApi)
  await initialiseData(templateApi._storeHandle, {
    hierarchy: hierarchy.root,
    actions: [],
    triggers: [],
  })
  await templateApi.saveApplicationHierarchy(hierarchy.root)
  const app = await appFromTempalteApi(templateApi, disableCleanupTransactions)
  const collectionApi = getCollectionApi(app)
  const indexApi = getIndexApi(app)
  const authApi = getAuthApi(app)
  const actionsApi = getActionsApi(app)
  const recordApi = await getRecordApi(app)
  recordApi._storeHandle = app.datastore
  actionsApi._app = app

  const apis = {
    recordApi,
    collectionApi,
    templateApi,
    indexApi,
    authApi,
    actionsApi,
    appHierarchy: hierarchy,
    subscribe: templateApi._eventAggregator.subscribe,
    app,
  }

  return apis
}

export const getNewFieldAndAdd = (templateApi, record) => (
  name,
  type,
  initial,
  typeOptions
) => {
  const field = templateApi.getNewField(type)
  field.name = name
  field.getInitialValue = !initial ? "default" : initial
  if (typeOptions) field.typeOptions = typeOptions
  templateApi.addField(record, field)
  return field
}

export const stubEventHandler = () => {
  const events = []
  return {
    handle: (name, context) => {
      events.push({ name, context })
    },
    events,
    getEvents: n => filter(e => e.name === n)(events),
  }
}

export const createValidActionsAndTriggers = () => {
  const logMessage = createAction()
  logMessage.name = "logMessage"
  logMessage.behaviourName = "log"
  logMessage.behaviourSource = "budibase-behaviours"

  const measureCallTime = createAction()
  measureCallTime.name = "measureCallTime"
  measureCallTime.behaviourName = "call_timer"
  measureCallTime.behaviourSource = "budibase-behaviours"

  const sendEmail = createAction()
  sendEmail.name = "sendEmail"
  sendEmail.behaviourName = "send_email"
  sendEmail.behaviourSource = "my-custom-lib"

  const logOnErrorTrigger = createTrigger()
  logOnErrorTrigger.actionName = "logMessage"
  logOnErrorTrigger.eventName = "recordApi:save:onError"
  logOnErrorTrigger.optionsCreator = "return context.error.message;"

  const timeCustomerSaveTrigger = createTrigger()
  timeCustomerSaveTrigger.actionName = "measureCallTime"
  timeCustomerSaveTrigger.eventName = "recordApi:save:onComplete"
  timeCustomerSaveTrigger.optionsCreator = "return 999;"
  timeCustomerSaveTrigger.condition = "context.record.type === 'customer'"

  const allActions = [logMessage, measureCallTime, sendEmail]
  const allTriggers = [logOnErrorTrigger, timeCustomerSaveTrigger]

  const behaviourSources = createBehaviourSources()
  const logs = []
  const call_timers = []
  const emails = []
  behaviourSources.register("budibase-behaviours", {
    log: message => logs.push(message),
    call_timer: opts => call_timers.push(opts),
  })
  behaviourSources.register("my-custom-lib", {
    send_email: em => emails.push(em),
  })

  return {
    logMessage,
    measureCallTime,
    sendEmail,
    logOnErrorTrigger,
    timeCustomerSaveTrigger,
    allActions,
    allTriggers,
    behaviourSources,
    logs,
    call_timers,
    emails,
  }
}

export const createAppDefinitionWithActionsAndTriggers = async () => {
  const {
    appHierarchy,
    templateApi,
    app,
    actionsApi,
  } = await setupApphierarchy(basicAppHierarchyCreator_WithFields)

  // adding validation rule so it can fail when we save it
  templateApi.addRecordValidationRule(appHierarchy.customerRecord)(
    templateApi.commonRecordValidationRules.fieldNotEmpty("surname")
  )

  await templateApi.saveApplicationHierarchy(appHierarchy.root)

  const actionsAndTriggers = createValidActionsAndTriggers()
  const { allActions, allTriggers, behaviourSources } = actionsAndTriggers
  await templateApi.saveActionsAndTriggers(allActions, allTriggers)
  app.actions = initialiseActions(
    templateApi._eventAggregator.subscribe,
    behaviourSources,
    allActions,
    allTriggers
  )
  app.user.permissions = generateFullPermissions(app)
  app.behaviourSources = behaviourSources
  const appDefinition = await templateApi.getApplicationDefinition()
  return {
    templateApi,
    appDefinition,
    ...actionsAndTriggers,
    ...appHierarchy,
    app,
    actionsApi,
  }
}

export const validUser = async (
  app,
  authApi,
  password,
  enabled = true,
  accessLevels = null
) => {
  const access = await authApi.getNewAccessLevel(app)
  access.name = "admin"
  permission.setPassword.add(access)

  const access2 = await authApi.getNewAccessLevel(app)
  access2.name = "admin2"
  permission.setPassword.add(access)

  await authApi.saveAccessLevels({ version: 0, levels: [access, access2] })

  const u = authApi.getNewUser(app)
  u.name = "bob"
  if (accessLevels === null) u.accessLevels = ["admin"]
  else u.accessLevels = accessLevels

  u.enabled = enabled

  await authApi.createUser(u, password)
  return u
}