// Coverage Reporter
// Part of this code is based on [1], which is licensed under the New BSD License.
// For more information see the See the accompanying LICENSE-istanbul file for terms.
//
// [1]: https://github.com/gotwarlost/istanbul/blob/master/lib/command/check-coverage.js
// =====================
//
// Generates the report

// Dependencies
// ------------

var path = require('path')
const { promisify } = require('util')
var istanbulLibCoverage = require('istanbul-lib-coverage')
var istanbulLibReport = require('istanbul-lib-report')
var minimatch = require('minimatch')

var globalSourceMapStore = require('./source-map-store')
var globalCoverageMap = require('./coverage-map')
var reports = require('./report-creator')
const hasOwnProperty = Object.prototype.hasOwnProperty

// TODO(vojta): inject only what required (config.basePath, config.coverageReporter)
var CoverageReporter = function (rootConfig, helper, logger, emitter) {
  var log = logger.create('coverage')

  // Instance variables
  // ------------------

  this.adapters = []

  // Options
  // -------

  var config = rootConfig.coverageReporter || {}
  var basePath = rootConfig.basePath
  var reporters = config.reporters
  var sourceMapStore = globalSourceMapStore.get(basePath)
  var includeAllSources = config.includeAllSources === true

  if (config.watermarks) {
    config.watermarks = helper.merge({}, istanbulLibReport.getDefaultWatermarks(), config.watermarks)
  }

  if (!helper.isDefined(reporters)) {
    reporters = [config]
  }

  var coverageMaps

  function normalize (key) {
    // Exclude keys will always be relative, but covObj keys can be absolute or relative
    var excludeKey = path.isAbsolute(key) ? path.relative(basePath, key) : key
    // Also normalize for files that start with `./`, etc.
    excludeKey = path.normalize(excludeKey)

    return excludeKey
  }

  function getTrackedFiles (coverageMap, patterns) {
    var files = []

    coverageMap.files().forEach(function (key) {
      // Do any patterns match the resolved key
      var found = patterns.some(function (pattern) {
        return minimatch(normalize(key), pattern, { dot: true })
      })

      // if no patterns match, keep the key
      if (!found) {
        files.push(key)
      }
    })

    return files
  }

  function overrideThresholds (key, overrides) {
    var thresholds = {}

    // First match wins
    Object.keys(overrides).some(function (pattern) {
      if (minimatch(normalize(key), pattern, { dot: true })) {
        thresholds = overrides[pattern]
        return true
      }
    })

    return thresholds
  }

  function checkCoverage (browser, coverageMap) {
    var defaultThresholds = {
      global: {
        statements: 0,
        branches: 0,
        lines: 0,
        functions: 0,
        excludes: []
      },
      each: {
        statements: 0,
        branches: 0,
        lines: 0,
        functions: 0,
        excludes: [],
        overrides: {}
      }
    }

    var thresholds = helper.merge({}, defaultThresholds, config.check)

    var globalTrackedFiles = getTrackedFiles(coverageMap, thresholds.global.excludes)
    var eachTrackedFiles = getTrackedFiles(coverageMap, thresholds.each.excludes)
    var globalResults = istanbulLibCoverage.createCoverageSummary()
    var eachResults = {}
    globalTrackedFiles.forEach(function (f) {
      var fileCoverage = coverageMap.fileCoverageFor(f)
      var summary = fileCoverage.toSummary()
      globalResults.merge(summary)
    })
    eachTrackedFiles.forEach(function (f) {
      var fileCoverage = coverageMap.fileCoverageFor(f)
      var summary = fileCoverage.toSummary()
      eachResults[f] = summary
    })

    var coverageFailed = false
    const { emitWarning = false } = thresholds

    function check (name, thresholds, actuals) {
      var keys = [
        'statements',
        'branches',
        'lines',
        'functions'
      ]

      keys.forEach(function (key) {
        var actual = actuals[key].pct
        var actualUncovered = actuals[key].total - actuals[key].covered
        var threshold = thresholds[key]

        if (threshold < 0) {
          if (threshold * -1 < actualUncovered) {
            coverageFailed = true
            log.error(browser.name + ': Uncovered count for ' + key + ' (' + actualUncovered +
              ') exceeds ' + name + ' threshold (' + -1 * threshold + ')')
          }
        } else if (actual < threshold) {
          const message = `${browser.name}: Coverage for ${key} (${actual}%) does not meet ${name} threshold (${threshold}%)`
          if (emitWarning) {
            log.warn(message)
          } else {
            coverageFailed = true
            log.error(message)
          }
        }
      })
    }

    check('global', thresholds.global, globalResults.toJSON())

    eachTrackedFiles.forEach(function (key) {
      var keyThreshold = helper.merge(thresholds.each, overrideThresholds(key, thresholds.each.overrides))
      check('per-file' + ' (' + key + ') ', keyThreshold, eachResults[key].toJSON())
    })

    return coverageFailed
  }

  // Generate the output path from the `coverageReporter.dir` and
  // `coverageReporter.subdir` options.
  function generateOutputPath (basePath, browserName, dir = 'coverage', subdir) {
    if (subdir && typeof subdir === 'function') {
      subdir = subdir(browserName)
    }
    if (browserName) {
      browserName = browserName.replace(':', '')
    }

    let outPutPath = path.join(dir, subdir || browserName)
    outPutPath = path.resolve(basePath, outPutPath)

    return helper.normalizeWinPath(outPutPath)
  }

  this.onRunStart = function (browsers) {
    coverageMaps = Object.create(null)

    // TODO(vojta): remove once we don't care about Karma 0.10
    if (browsers) {
      browsers.forEach(this.onBrowserStart.bind(this))
    }
  }

  this.onBrowserStart = function (browser) {
    var startingMap = {}
    if (includeAllSources) {
      startingMap = globalCoverageMap.get()
    }

    coverageMaps[browser.id] = istanbulLibCoverage.createCoverageMap(startingMap)
  }

  this.onBrowserComplete = function (browser, result) {
    var coverageMap = coverageMaps[browser.id]

    if (!coverageMap) return
    if (!result || !result.coverage) return

    coverageMap.merge(result.coverage)
  }

  this.onSpecComplete = function (browser, result) {
    var coverageMap = coverageMaps[browser.id]

    if (!coverageMap) return
    if (!result.coverage) return

    coverageMap.merge(result.coverage)
  }

  let checkedCoverage = {}
  let promiseComplete = null

  this.executeReport = async function (reporterConfig, browser) {
    const results = { exitCode: 0 }
    const coverageMap = coverageMaps[browser.id]
    if (!coverageMap) {
      return
    }

    const mainDir = reporterConfig.dir || config.dir
    const subDir = reporterConfig.subdir || config.subdir
    const outputPath = generateOutputPath(basePath, browser.name, mainDir, subDir)
    const remappedCoverageMap = await sourceMapStore.transformCoverage(coverageMap)

    const options = helper.merge(config, reporterConfig, {
      dir: outputPath,
      subdir: '',
      browser: browser,
      emitter: emitter,
      coverageMap: remappedCoverageMap
    })

    // If config.check is defined, check coverage levels for each browser
    if (hasOwnProperty.call(config, 'check') && !checkedCoverage[browser.id]) {
      checkedCoverage[browser.id] = true
      var coverageFailed = checkCoverage(browser, remappedCoverageMap)
      if (coverageFailed && results) {
        results.exitCode = 1
      }
    }

    const context = istanbulLibReport.createContext(options)
    const report = reports.create(reporterConfig.type || 'html', options)

    // // If reporting to console or in-memory skip directory creation
    const toDisk = !reporterConfig.type || !reporterConfig.type.match(/^(text|text-summary|in-memory)$/)

    if (!toDisk && reporterConfig.file === undefined) {
      report.execute(context)
      return results
    }

    const mkdirIfNotExists = promisify(helper.mkdirIfNotExists)
    await mkdirIfNotExists(outputPath)

    log.debug('Writing coverage to %s', outputPath)
    report.execute(context)

    return results
  }

  this.onRunComplete = function (browsers) {
    checkedCoverage = {}
    let results = { exitCode: 0 }

    const promiseCollection = reporters.map(reporterConfig =>
      Promise.all(browsers.map(async (browser) => {
        const res = await this.executeReport(reporterConfig, browser)
        if (res && res.exitCode === 1) {
          results = res
        }
      })))
    promiseComplete = Promise.all(promiseCollection).then(() => results)
    return promiseComplete
  }

  this.onExit = async function (done) {
    try {
      const results = await promiseComplete
      if (results && results.exitCode === 1) {
        done(results.exitCode)
        return
      }
      if (typeof config._onExit === 'function') {
        config._onExit(done)
      } else {
        done()
      }
    } catch (e) {
      log.error('Unexpected error while generating coverage report.\n', e)
      done(1)
    }
  }
}

CoverageReporter.$inject = ['config', 'helper', 'logger', 'emitter']

// PUBLISH
module.exports = CoverageReporter
