require("console.table")
_         = require("lodash")
fs        = require("fs-extra")
Promise   = require("bluebird")
bumpercar = require("@cypress/bumpercar")
path      = require("path")
la        = require('lazy-ass')
check     = require('check-more-types')
R         = require("ramda")
os        = require("os")
{configFromEnvOrJsonFile, filenameToShellVariable} = require('@cypress/env-or-json-file')
makeEmptyGithubCommit = require("make-empty-github-commit")
parse = require("parse-github-repo-url")
{setCommitStatus} = require("@cypress/set-commit-status")

fs = Promise.promisifyAll(fs)

car = null

# all the projects to trigger / run / change environment variables for
_PROVIDERS = {
  appVeyor: {
    main: "cypress-io/cypress"
    others: [
      "cypress-io/cypress-test-tiny"
      "cypress-io/cypress-test-example-repos"
    ]
  }

  circle: {
    main: "cypress-io/cypress"
    others: [
      "cypress-io/cypress-test-tiny"
      "cypress-io/cypress-test-module-api"
      "cypress-io/cypress-test-node-versions"
      "cypress-io/cypress-test-nested-projects"
      "cypress-io/cypress-test-ci-environments"
      "cypress-io/cypress-test-example-repos"
    ]
  }
}

remapProjects = (projectsByProvider) ->
  list = []

  _.mapValues projectsByProvider, (provider, name) ->
    provider.others.forEach (repo) ->
      list.push({
        repo
        provider: name
      })

  list

remapMain = (projectsByProvider) ->
  list = []

  _.mapValues projectsByProvider, (provider, name) ->
    list.push({
      repo: provider.main
      provider: name
    })

  list

# make flat list of objects
# {repo, provider}
PROJECTS = remapProjects(_PROVIDERS)

getCiConfig = ->
  key = path.join("scripts", "support", "ci.json")
  config = configFromEnvOrJsonFile(key)

  if !config
    console.error('⛔️  Cannot find CI credentials')
    console.error('Using @cypress/env-or-json-file module')
    console.error('and filename', key)
    console.error('which is environment variable', filenameToShellVariable(key))
    throw new Error('CI config not found')
  config

awaitEachProjectAndProvider = (projects, fn, filter = R.identity) ->
  creds = getCiConfig()

  ## configure a new Bumpercar
  providers = {}
  if check.unemptyString(creds.githubToken)
    providers.travis = {
      githubToken: creds.githubToken
    }
  if check.unemptyString(creds.circleToken)
    providers.circle = {
      circleToken: creds.circleToken
    }
  if check.unemptyString(creds.appVeyorToken)
    providers.appVeyor = {
      appVeyorToken: creds.appVeyorToken
    }
  if check.unemptyString(creds.buildkiteToken)
    providers.buildkite = {
      buildkiteToken: creds.buildkiteToken
    }
  providerNames = Object.keys(providers)
  console.log("configured providers", providerNames)
  la(check.not.empty(providerNames), "empty list of providers")

  car = bumpercar.create({providers})

  filteredProjects = R.filter(filter, projects)
  if check.empty(filteredProjects)
    console.log("⚠️ zero filtered projects left after filtering")
  console.table("filtered projects", filteredProjects)
  Promise.mapSeries filteredProjects, (project) ->
    fn(project.repo, project.provider, creds)

# do not trigger all projects if there is specific provider
# for example appVeyor should be used for Windows testing
getFilterByProvider = (providerName) ->
  if providerName
    console.log("only allow projects for provider", providerName)
    projectFilter = R.propEq("provider", providerName)
  else
    projectFilter = R.identity
  projectFilter

module.exports = {
  nextVersion: (version) ->
    MAIN_PROJECTS = remapMain(_PROVIDERS)
    console.log("Setting next version to build", version)
    console.table("In these projects", MAIN_PROJECTS)

    la(check.unemptyString(version),
      "missing next version to set", version)

    setNextDevVersion = (project, provider) ->
      console.log("setting env var NEXT_DEV_VERSION to %s on %s in project %s",
        version, provider, project)
      car.updateProjectEnv(project, provider, {
        NEXT_DEV_VERSION: version,
      })

    awaitEachProjectAndProvider(MAIN_PROJECTS, setNextDevVersion)

  # in each project, set a couple of environment variables
  version: (nameOrUrl, binaryVersionOrUrl, platform, providerName) ->
    console.table("All possible projects", PROJECTS)

    la(check.unemptyString(nameOrUrl),
      "missing cypress name or url to set", nameOrUrl)

    if check.semver(nameOrUrl)
      console.log("for version", nameOrUrl)
      nameOrUrl = "cypress@#{nameOrUrl}"
      console.log("full NPM install name is", nameOrUrl)

    la(check.unemptyString(binaryVersionOrUrl),
      "missing binary version or url", binaryVersionOrUrl)

    result = {
      versionName: nameOrUrl,
      binary: binaryVersionOrUrl
    }

    projectFilter = getFilterByProvider(providerName)

    updateProject = (project, provider) ->
      console.log("setting environment variables in", project)
      car.updateProjectEnv(project, provider, {
        CYPRESS_NPM_PACKAGE_NAME: nameOrUrl,
        CYPRESS_INSTALL_BINARY: binaryVersionOrUrl
      })
    awaitEachProjectAndProvider(PROJECTS, updateProject, projectFilter)
    .then R.always(result)

  # triggers test projects on multiple CIs
  # the test projects will exercise the new version of
  # the Cypress test runner we just built
  runTestProjects: (getStatusAndMessage, providerName, version) ->

    projectFilter = getFilterByProvider(providerName)

    makeCommit = (project, provider, creds) ->
      ## make empty commit to trigger CIs
      ## project is owner/repo string like cypress-io/cypress-test-tiny
      console.log("making commit to project", project)

      parsedRepo = parse(project)
      owner = parsedRepo[0]
      repo = parsedRepo[1]

      { status, message } = getStatusAndMessage(repo)

      if not message
        message =
          """
          Testing new Cypress version #{version}

          """
        if process.env.CIRCLE_BUILD_URL
          message += "\n"
          message += "Circle CI build url #{process.env.CIRCLE_BUILD_URL}"

        if process.env.APPVEYOR
          slug = process.env.APPVEYOR_PROJECT_SLUG
          build = process.env.APPVEYOR_BUILD_ID
          message += "\n"
          message += "AppVeyor CI #{slug} #{build}"

      defaultOptions = {
        owner,
        repo,
        message,
        token: creds.githubToken,
      }

      createGithubCommitStatusCheck = ({ sha }) ->
        return if not status

        # status is {owner, repo, sha} and maybe a few other properties
        isStatus = check.schema({
          owner: check.unemptyString,
          repo: check.unemptyString,
          sha: check.commitId,
          context: check.unemptyString,
          platform: check.unemptyString,
          arch: check.unemptyString
        })
        if not isStatus(status)
          console.error("Invalid status object %o", status)

        targetUrl = "https://github.com/#{owner}/#{repo}/commit/#{sha}"
        commitStatusOptions = {
          targetUrl,
          owner: status.owner,
          repo: status.repo,
          sha: status.sha,
          context: status.context,
          state: 'pending',
          description: "#{owner}/#{repo}",
        }

        console.log(
          'creating commit status check',
          commitStatusOptions.description,
          commitStatusOptions.context
        )

        setCommitStatus(commitStatusOptions)

      if not version
        return makeEmptyGithubCommit(defaultOptions).then(createGithubCommitStatusCheck)

      # first try to commit to branch for next upcoming version
      specificBranchOptions = {
        owner: owner,
        repo: repo,
        token: creds.githubToken,
        message,
        branch: version
      }

      makeEmptyGithubCommit(specificBranchOptions)
      .catch () ->
        # maybe there is no branch for next version
        # try default branch
        makeEmptyGithubCommit(defaultOptions)
      .then(createGithubCommitStatusCheck)

    awaitEachProjectAndProvider(PROJECTS, makeCommit, projectFilter)
}
