const Application = require('spectron').Application
const { copyFileSync } = require('fs')
const fs = require('fs')
const parseTorrent = require('parse-torrent')
const path = require('path')
const PNG = require('pngjs').PNG
const rimraf = require('rimraf')

const config = require('./config')

module.exports = {
  createApp,
  endTest,
  screenshotCreateOrCompare,
  compareDownloadFolder,
  compareFiles,
  compareTorrentFile,
  compareTorrentFiles,
  waitForLoad,
  wait,
  resetTestDataDir,
  deleteTestDataDir,
  copy
}

// Runs WebTorrent Desktop.
// Returns a promise that resolves to a Spectron Application once the app has loaded.
// Takes a Tape test. Makes some basic assertions to verify that the app loaded correctly.
function createApp (t) {
  const userDataDir = process.platform === 'win32'
    ? path.join('C:\\Windows\\Temp', 'WebTorrentTest')
    : path.join('/tmp', 'WebTorrentTest')

  return new Application({
    path: path.join(__dirname, '..', 'node_modules', '.bin',
      'electron' + (process.platform === 'win32' ? '.cmd' : '')),
    args: ['-r', path.join(__dirname, 'mocks.js'), path.join(__dirname, '..')],
    chromeDriverArgs: [`--user-data-dir=${userDataDir}`],
    env: { NODE_ENV: 'test' },
    waitTimeout: 10e3
  })
}

// Starts the app, waits for it to load, returns a promise
function waitForLoad (app, t, opts) {
  if (!opts) opts = {}
  return app.start().then(function () {
    return app.client.waitUntilWindowLoaded()
  }).then(function () {
    // Offline mode
    if (!opts.online) app.webContents.executeJavaScript('testOfflineMode()')
  }).then(function () {
    // Switch to the main window. Index 0 is apparently the hidden webtorrent window...
    return app.client.windowByIndex(1)
  }).then(function () {
    return app.client.waitUntilWindowLoaded()
  }).then(function () {
    return app.webContents.getTitle()
  }).then(function (title) {
    // Note the window title is WebTorrent, this is the HTML <title>
    t.equal(title, 'Main Window', 'html title')
  })
}

// Returns a promise that resolves after 'ms' milliseconds. Default: 1 second
function wait (ms) {
  if (ms === undefined) ms = 1000 // Default: wait long enough for the UI to update
  return new Promise(function (resolve, reject) {
    setTimeout(resolve, ms)
  })
}

// Quit the app, end the test, either in success (!err) or failure (err)
function endTest (app, t, err) {
  return app.stop().then(function () {
    t.end(err)
  })
}

// Takes a screenshot of the app
// If we already have a reference under test/screenshots, assert that they're the same
// Otherwise, create the reference screenshot: test/screenshots/<platform>/<name>.png
function screenshotCreateOrCompare (app, t, name) {
  const ssDir = path.join(__dirname, 'screenshots', process.platform)

  // check that path exists otherwise create it
  if (!fs.existsSync(ssDir)) {
    fs.mkdirSync(ssDir)
  }

  const ssPath = path.join(ssDir, name + '.png')
  let ssBuf

  try {
    ssBuf = fs.readFileSync(ssPath)
  } catch (err) {
    ssBuf = Buffer.alloc(0)
  }

  return app.browserWindow.focus()
    .then(() => wait())
    .then(() => app.browserWindow.capturePage())
    .then(function (buffer) {
      if (ssBuf.length === 0) {
        console.log('Saving screenshot ' + ssPath)
        fs.writeFileSync(ssPath, buffer)
      } else {
        const match = compareIgnoringTransparency(buffer, ssBuf)
        t.ok(match, 'screenshot comparison ' + name)
        if (!match) {
          const ssFailedPath = path.join(ssDir, name + '-failed.png')
          console.log('Saving screenshot, failed comparison: ' + ssFailedPath)
          fs.writeFileSync(ssFailedPath, buffer)
        }
      }
    })
}

// Compares two PNGs, ignoring any transparent regions in bufExpected.
// Returns true if they match.
function compareIgnoringTransparency (bufActual, bufExpected) {
  // Common case: exact byte-for-byte match
  if (Buffer.compare(bufActual, bufExpected) === 0) return true

  // Otherwise, compare pixel by pixel
  let sumSquareDiff = 0
  let numDiff = 0
  const pngA = PNG.sync.read(bufActual)
  const pngE = PNG.sync.read(bufExpected)
  if (pngA.width !== pngE.width || pngA.height !== pngE.height) return false
  const w = pngA.width
  const h = pngE.height
  const da = pngA.data
  const de = pngE.data
  for (let y = 0; y < h; y++) {
    for (let x = 0; x < w; x++) {
      const i = ((y * w) + x) * 4
      if (de[i + 3] === 0) continue // Skip transparent pixels
      const ca = (da[i] << 16) | (da[i + 1] << 8) | da[i + 2]
      const ce = (de[i] << 16) | (de[i + 1] << 8) | de[i + 2]
      if (ca === ce) continue

      // Add pixel diff to running sum
      // This is necessary on Windows, where rendering apparently isn't quite deterministic
      // and a few pixels in the screenshot will sometimes be off by 1. (Visually identical.)
      numDiff++
      sumSquareDiff += (da[i] - de[i]) * (da[i] - de[i])
      sumSquareDiff += (da[i + 1] - de[i + 1]) * (da[i + 1] - de[i + 1])
      sumSquareDiff += (da[i + 2] - de[i + 2]) * (da[i + 2] - de[i + 2])
    }
  }
  const rms = Math.sqrt(sumSquareDiff / (numDiff + 1))
  const l2Distance = Math.round(Math.sqrt(sumSquareDiff))
  console.log('screenshot diff l2 distance: ' + l2Distance + ', rms: ' + rms)
  return l2Distance < 5000 && rms < 100
}

// Resets the test directory, containing config.json, torrents, downloads, etc
function resetTestDataDir () {
  rimraf.sync(config.TEST_DIR)
  // Create TEST_DIR as well as /Downloads and /Desktop
  fs.mkdirSync(config.TEST_DIR_DOWNLOAD, { recursive: true })
  fs.mkdirSync(config.TEST_DIR_DESKTOP, { recursive: true })
}

function deleteTestDataDir () {
  rimraf.sync(config.TEST_DIR)
}

// Checks a given folder under Downloads.
// Makes sure that the filenames match exactly.
// If `filenames` is null, asserts that the folder doesn't exist.
function compareDownloadFolder (t, dirname, filenames) {
  const dirpath = path.join(config.TEST_DIR_DOWNLOAD, dirname)
  try {
    const actualFilenames = fs.readdirSync(dirpath)
    if (filenames === null) {
      return t.fail('expected download folder to be absent, but it\'s here: ' + dirpath)
    }
    const expectedSorted = filenames.slice().sort()
    const actualSorted = actualFilenames.slice().sort()
    console.log(actualSorted)
    t.deepEqual(actualSorted, expectedSorted, 'download folder contents: ' + dirname)
  } catch (err) {
    if (err.code === 'ENOENT') {
      t.equal(filenames, null, 'download folder missing: ' + dirname)
    } else {
      console.error(err)
      t.fail('unexpected error getting download folder: ' + dirname)
    }
  }
}

// Makes sure two files have identical contents
function compareFiles (t, pathActual, pathExpected) {
  const bufActual = fs.readFileSync(pathActual)
  const bufExpected = fs.readFileSync(pathExpected)
  const match = Buffer.compare(bufActual, bufExpected) === 0
  t.ok(match, 'correct contents: ' + pathActual)
}

// Makes sure two torrents have the same infohash and flags
function compareTorrentFiles (t, pathActual, pathExpected) {
  const bufActual = fs.readFileSync(pathActual)
  const bufExpected = fs.readFileSync(pathExpected)
  const fieldsActual = extractImportantFields(parseTorrent(bufActual))
  const fieldsExpected = extractImportantFields(parseTorrent(bufExpected))
  t.deepEqual(fieldsActual, fieldsExpected, 'torrent contents: ' + pathActual)
}

// Makes sure two torrents have the same infohash and flags
function compareTorrentFile (t, pathActual, fieldsExpected) {
  const bufActual = fs.readFileSync(pathActual)
  const fieldsActual = extractImportantFields(parseTorrent(bufActual))
  if (Array.isArray(fieldsExpected.announce)) fieldsExpected.announce.sort()
  t.deepEqual(fieldsActual, fieldsExpected, 'torrent contents: ' + pathActual)
}

function extractImportantFields (parsedTorrent) {
  let { infoHash, name, announce, urlList, comment } = parsedTorrent
  const priv = parsedTorrent.private // private is a reserved word in JS
  announce = announce.slice().sort()
  return { infoHash, name, announce, urlList, comment, private: priv }
}

function copy (pathFrom, pathTo) {
  try {
    copyFileSync(pathFrom, pathTo)
  } catch (err) {
    // Windows lets us create files and folders under C:\Windows\Temp,
    // but when you try to `copySync` into one of those folders, you get EPERM
    // Ignore for now...
    if (process.platform !== 'win32' || err.code !== 'EPERM') throw err
    console.log('ignoring windows copy EPERM error', err)
  }
}