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

let car = null

// all the projects to trigger / run / change environment variables for
const _PROVIDERS = {
  appVeyor: {
    main: 'cypress-io/cypress',
    win32: [
      'cypress-io/cypress-test-tiny',
      'cypress-io/cypress-test-example-repos',
    ],
  },

  circle: {
    main: 'cypress-io/cypress',
    linux: [
      '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',
    ],
    darwin: [
      'cypress-io/cypress-test-tiny',
      'cypress-io/cypress-test-example-repos',
    ],
  },
}

const remapProjects = function (projectsByProvider) {
  const list = []

  _.mapValues(projectsByProvider, (provider, name) => {
    const remapPlatform = (platform, repos) => {
      return repos.forEach((repo) => {
        return list.push({
          repo,
          provider: name,
          platform,
        })
      })
    }

    if (provider.win32) {
      remapPlatform('win32', provider.win32)
    }

    if (provider.linux) {
      remapPlatform('linux', provider.linux)
    }

    if (provider.darwin) {
      return remapPlatform('darwin', provider.darwin)
    }
  })

  return list
}

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

const getCiConfig = function () {
  const key = path.join('scripts', 'support', 'ci.json')
  const 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')
  }

  return config
}

const awaitEachProjectAndProvider = function (projects, fn, filter = R.identity) {
  const creds = getCiConfig()

  // configure a new Bumpercar
  const 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,
    }
  }

  const providerNames = Object.keys(providers)

  console.log('configured providers', providerNames)
  la(check.not.empty(providerNames), 'empty list of providers')

  car = bumpercar.create({ providers })

  const filteredProjects = R.filter(filter, projects)

  if (check.empty(filteredProjects)) {
    console.log('⚠️ zero filtered projects left after filtering')
  }

  console.log('filtered projects:')
  console.table(filteredProjects)

  return Promise.mapSeries(filteredProjects, (project) => {
    return 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
const getFilterByProvider = function (providerName, platformName) {
  let platformFilter; let providerFilter

  if (providerName) {
    console.log('only allow projects for provider', providerName)
    providerFilter = R.propEq('provider', providerName)
  } else {
    providerFilter = R.identity
  }

  if (platformName) {
    console.log('only allow projects for platform', platformName)
    platformFilter = R.propEq('platform', platformName)
  } else {
    platformFilter = R.identity
  }

  // combined filter is when both filters pass
  const projectFilter = R.allPass([providerFilter, platformFilter])

  return projectFilter
}

module.exports = {
  _PROVIDERS,

  remapProjects,

  getFilterByProvider,

  // in each project, set a couple of environment variables
  version (nameOrUrl, binaryVersionOrUrl, platform, providerName) {
    console.log('All possible projects:')
    console.table(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)

    const result = {
      versionName: nameOrUrl,
      binary: binaryVersionOrUrl,
    }

    const projectFilter = getFilterByProvider(providerName)

    const updateProject = function (project, provider) {
      console.log('setting environment variables in', project)

      return car.updateProjectEnv(project, provider, {
        CYPRESS_NPM_PACKAGE_NAME: nameOrUrl,
        CYPRESS_INSTALL_BINARY: binaryVersionOrUrl,
      })
    }

    return 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, platform) {
    const projectFilter = getFilterByProvider(providerName, platform)

    const makeCommit = function (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)

      // print if we have a few github variables present
      console.log('do we have GH_APP_ID?', Boolean(process.env.GH_APP_ID))
      console.log('do we have GH_INSTALLATION_ID?', Boolean(process.env.GH_INSTALLATION_ID))
      console.log('do we have GH_PRIVATE_KEY?', Boolean(process.env.GH_PRIVATE_KEY))

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

      let { status, message } = getStatusAndMessage(repo)

      if (!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) {
          const slug = process.env.APPVEYOR_PROJECT_SLUG
          const build = process.env.APPVEYOR_BUILD_ID

          message += '\n'
          message += `AppVeyor CI ${slug} ${build}`
        }
      }

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

      const createGithubCommitStatusCheck = function ({ sha }) {
        if (!status) {
          return
        }

        // status is {owner, repo, sha} and maybe a few other properties
        const isStatus = check.schema({
          owner: check.unemptyString,
          repo: check.unemptyString,
          sha: check.commitId,
          context: check.unemptyString,
          platform: check.unemptyString,
          arch: check.unemptyString,
        })

        if (!isStatus(status)) {
          console.error('Invalid status object %o', status)
        }

        const targetUrl = `https://github.com/${owner}/${repo}/commit/${sha}`
        const 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,
        )

        return setCommitStatus(commitStatusOptions)
      }

      if (!version) {
        return makeEmptyGithubCommit(defaultOptions).then(createGithubCommitStatusCheck)
      }

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

      return makeEmptyGithubCommit(specificBranchOptions)
      .catch(() => {
        // maybe there is no branch for next version
        // try default branch
        return makeEmptyGithubCommit(defaultOptions)
      }).then(createGithubCommitStatusCheck)
    }

    return awaitEachProjectAndProvider(PROJECTS, makeCommit, projectFilter)
  },
}
