const path = require('path');
const packageJson = require('./package.json');
const importOnce = require('node-sass-import-once');
const rimraf = require('rimraf');
const mkdirp = require('mkdirp');
const spectron = require('spectron');
const asar = require('asar');
const fs = require('fs');
const assert = require('assert');
const sass = require('node-sass');

/* eslint-disable more/no-then, no-console  */

module.exports = grunt => {
  const bower = grunt.file.readJSON('bower.json');
  const components = [];
  // eslint-disable-next-line guard-for-in, no-restricted-syntax
  for (const i in bower.concat.app) {
    components.push(bower.concat.app[i]);
  }

  const libtextsecurecomponents = [];
  // eslint-disable-next-line guard-for-in, no-restricted-syntax
  for (const i in bower.concat.libtextsecure) {
    libtextsecurecomponents.push(bower.concat.libtextsecure[i]);
  }

  const utilWorkerComponents = [
    'node_modules/bytebuffer/dist/bytebuffer.js',
    'js/curve/curve25519_compiled.js',
    'js/curve/curve25519_wrapper.js',
    'node_modules/libsodium/dist/modules/libsodium.js',
    'node_modules/libsodium-wrappers/dist/modules/libsodium-wrappers.js',

    'libtextsecure/libsignal-protocol.js',
    'js/util_worker_tasks.js',
  ];

  grunt.loadNpmTasks('grunt-sass');

  grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),
    concat: {
      components: {
        src: components,
        dest: 'js/components.js',
      },
      util_worker: {
        src: utilWorkerComponents,
        dest: 'js/util_worker.js',
      },
      libtextsecurecomponents: {
        src: libtextsecurecomponents,
        dest: 'libtextsecure/components.js',
      },
      test: {
        src: ['node_modules/mocha/mocha.js', 'node_modules/chai/chai.js', 'test/_test.js'],
        dest: 'test/test.js',
      },
      libtextsecure: {
        options: {
          banner: ';(function() {\n',
          footer: '})();\n',
        },
        src: [
          'libtextsecure/errors.js',
          'libtextsecure/libsignal-protocol.js',
          'libtextsecure/crypto.js',
          'libtextsecure/storage.js',
          'libtextsecure/storage/user.js',
          'libtextsecure/storage/groups.js',
          'libtextsecure/protobufs.js',
          'libtextsecure/helpers.js',
          'libtextsecure/stringview.js',
          'libtextsecure/event_target.js',
          'libtextsecure/http-resources.js',
          'libtextsecure/message_receiver.js',
        ],
        dest: 'js/libtextsecure.js',
      },
      libtextsecuretest: {
        src: [
          'node_modules/jquery/dist/jquery.js',
          'node_modules/mocha/mocha.js',
          'node_modules/chai/chai.js',
          'libtextsecure/test/_test.js',
        ],
        dest: 'libtextsecure/test/test.js',
      },
    },
    sass: {
      options: {
        implementation: sass,
        sourceMap: true,
        importer: importOnce,
      },
      dev: {
        files: {
          'stylesheets/manifest.css': 'stylesheets/manifest.scss',
        },
      },
    },
    watch: {
      libtextsecure: {
        files: ['./libtextsecure/*.js', './libtextsecure/storage/*.js'],
        tasks: ['concat:libtextsecure'],
      },
      utilworker: {
        files: utilWorkerComponents,
        tasks: ['concat:util_worker'],
      },
      protobuf: {
        files: ['./protos/SignalService.proto'],
        tasks: ['exec:build-protobuf'],
      },
      sass: {
        files: ['./stylesheets/*.scss'],
        tasks: ['sass'],
      },
      transpile: {
        files: ['./ts/**/*.ts', './ts/**/*.tsx', './ts/**/**/*.tsx', './test/ts/**.ts'],
        tasks: ['exec:transpile'],
      },
    },
    exec: {
      transpile: {
        cmd: 'yarn transpile',
      },
      'build-protobuf': {
        cmd: 'yarn build-protobuf',
      },
    },
    'test-release': {
      osx: {
        archive: `mac/${packageJson.productName}.app/Contents/Resources/app.asar`,
        appUpdateYML: `mac/${packageJson.productName}.app/Contents/Resources/app-update.yml`,
        exe: `mac/${packageJson.productName}.app/Contents/MacOS/${packageJson.productName}`,
      },
      mas: {
        archive: 'mas/Signal.app/Contents/Resources/app.asar',
        appUpdateYML: 'mac/Signal.app/Contents/Resources/app-update.yml',
        exe: `mas/${packageJson.productName}.app/Contents/MacOS/${packageJson.productName}`,
      },
      linux: {
        archive: 'linux-unpacked/resources/app.asar',
        exe: `linux-unpacked/${packageJson.name}`,
      },
      win: {
        archive: 'win-unpacked/resources/app.asar',
        appUpdateYML: 'win-unpacked/resources/app-update.yml',
        exe: `win-unpacked/${packageJson.productName}.exe`,
      },
    },
    gitinfo: {}, // to be populated by grunt gitinfo
  });

  Object.keys(grunt.config.get('pkg').devDependencies).forEach(key => {
    if (/^grunt(?!(-cli)?$)/.test(key)) {
      // ignore grunt and grunt-cli
      grunt.loadNpmTasks(key);
    }
  });

  function updateLocalConfig(update) {
    const environment = process.env.SIGNAL_ENV || 'development';
    const configPath = `config/local-${environment}.json`;
    let localConfig;
    try {
      localConfig = grunt.file.readJSON(configPath);
    } catch (e) {
      //
    }
    localConfig = {
      ...localConfig,
      ...update,
    };
    grunt.file.write(configPath, `${JSON.stringify(localConfig)}\n`);
  }

  grunt.registerTask('getExpireTime', () => {
    grunt.task.requires('gitinfo');
    const gitinfo = grunt.config.get('gitinfo');
    const committed = gitinfo.local.branch.current.lastCommitTime;
    const time = Date.parse(committed) + 1000 * 60 * 60 * 24 * 90;
    updateLocalConfig({ buildExpiration: time });
  });

  grunt.registerTask('getCommitHash', () => {
    grunt.task.requires('gitinfo');
    const gitinfo = grunt.config.get('gitinfo');
    const hash = gitinfo.local.branch.current.SHA;
    updateLocalConfig({ commitHash: hash });
  });

  grunt.registerTask('clean-release', () => {
    rimraf.sync('release');
    mkdirp.sync('release');
  });

  function runTests(environment, cb) {
    let failure;
    const { Application } = spectron;
    const electronBinary = process.platform === 'win32' ? 'electron.cmd' : 'electron';
    const app = new Application({
      path: path.join(__dirname, 'node_modules', '.bin', electronBinary),
      args: [path.join(__dirname, 'main.js')],
      env: {
        NODE_ENV: environment,
      },
      requireName: 'unused',
      chromeDriverArgs: [
        `remote-debugging-port=${Math.floor(Math.random() * (9999 - 9000) + 9000)}`,
      ],
    });

    function getMochaResults() {
      // eslint-disable-next-line no-undef
      return window.mochaResults;
    }

    app
      .start()
      .then(() =>
        app.client.waitUntil(
          () => app.client.execute(getMochaResults).then(data => Boolean(data.value)),
          25000,
          'Expected to find window.mochaResults set!'
        )
      )
      .then(() => app.client.execute(getMochaResults))
      .then(data => {
        const results = data.value;
        if (results.failures > 0) {
          console.error(results.reports);
          failure = () => grunt.fail.fatal(`Found ${results.failures} failing unit tests.`);
          return app.client.log('browser');
        }
        grunt.log.ok(`${results.passes} tests passed.`);
        return null;
      })
      .then(logs => {
        if (logs) {
          console.error();
          console.error('Because tests failed, printing browser logs:');
          console.error(logs);
        }
      })
      .catch(error => {
        failure = () => grunt.fail.fatal(`Something went wrong: ${error.message} ${error.stack}`);
      })
      .then(() => {
        // We need to use the failure variable and this early stop to clean up before
        // shutting down. Grunt's fail methods are the only way to set the return value,
        // but they shut the process down immediately!
        if (failure) {
          console.log();
          console.log('Main process logs:');
          return app.client.getMainProcessLogs().then(logs => {
            logs.forEach(log => {
              console.log(log);
            });
            try {
              return app.stop();
            } catch (err) {
              return Promise.resolve();
            }
          });
        }
        try {
          return app.stop();
        } catch (err) {
          return Promise.resolve();
        }
      })
      .then(() => {
        if (failure) {
          failure();
        }
        cb();
      })
      .catch(error => {
        console.error('Second-level error:', error.message, error.stack);
        if (failure) {
          failure();
        }
        cb();
      });
  }

  grunt.registerTask('unit-tests', 'Run unit tests w/Electron', function thisNeeded() {
    const environment = grunt.option('env') || 'test';
    const done = this.async();

    runTests(environment, done);
  });

  grunt.registerMultiTask('test-release', 'Test packaged releases', function thisNeeded() {
    const dir = grunt.option('dir') || 'release';
    const environment = grunt.option('env') || 'production';
    const config = this.data;
    const archive = [dir, config.archive].join('/');
    const files = [
      'config/default.json',
      `config/${environment}.json`,
      `config/local-${environment}.json`,
    ];

    console.log(this.target, archive);
    const releaseFiles = files.concat(config.files || []);
    releaseFiles.forEach(fileName => {
      console.log(fileName);
      try {
        asar.statFile(archive, fileName);
        return true;
      } catch (e) {
        console.log(e);
        throw new Error(`Missing file ${fileName}`);
      }
    });

    if (config.appUpdateYML) {
      const appUpdateYML = [dir, config.appUpdateYML].join('/');
      if (fs.existsSync(appUpdateYML)) {
        console.log('auto update ok');
      } else {
        throw new Error(`Missing auto update config ${appUpdateYML}`);
      }
    }

    const done = this.async();
    // A simple test to verify a visible window is opened with a title
    const { Application } = spectron;

    const app = new Application({
      path: [dir, config.exe].join('/'),
      requireName: 'unused',
      chromeDriverArgs: [
        `remote-debugging-port=${Math.floor(Math.random() * (9999 - 9000) + 9000)}`,
      ],
    });

    app
      .start()
      .then(() => app.client.getWindowCount())
      .then(count => {
        assert.equal(count, 1);
        console.log('window opened');
      })
      .then(() =>
        // Get the window's title
        app.client.getTitle()
      )
      .then(title => {
        // TODO: restore once fixed on win
        if (this.target !== 'win') {
          // Verify the window's title
          assert.equal(title, packageJson.productName);
          console.log('title ok');
        }
      })
      .then(() => {
        assert(app.chromeDriver.logLines.indexOf(`NODE_ENV ${environment}`) > -1);
        console.log('environment ok');
      })
      .then(
        () =>
          // Successfully completed test
          app.stop(),
        error =>
          // Test failed!
          app.stop().then(() => {
            grunt.fail.fatal(`Test failed: ${error.message} ${error.stack}`);
          })
      )
      .then(done);
  });

  grunt.registerTask('dev', ['default', 'watch']);
  grunt.registerTask('test', ['unit-tests']);
  grunt.registerTask('date', ['gitinfo', 'getExpireTime']);
  grunt.registerTask('default', [
    'exec:build-protobuf',
    'exec:transpile',
    'concat',
    'sass',
    'date',
    'getCommitHash',
  ]);
};
