'use strict';

var fs = require('fs');
var path = require('path');
var assert = require('assert');
var cp = require('child_process');
var mkdirp = require('mkdirp');
var rimraf = require('rimraf');

// Sets directory to output coverage data to
// Incremented every time getRunner() is called.
var covCount = 1;
var isIstanbul = process.env.running_under_istanbul;

/*
 * I/O utilities for temporary directory.
 */
function j(paths) {
  return path.join.apply(path, paths);
}

function t(paths) {
  paths = Array.isArray(paths) ? paths : [paths];
  var args = [__dirname, 'temp'].concat(paths);
  return j(args);
}

function r(paths) {
  return fs.readFileSync(t(paths), 'utf8');
}

function rs(paths) {
  return fs.createReadStream(t(paths));
}

function w(paths, content) {
  return fs.writeFileSync(t(paths), content);
}

function a(paths, content) {
  return fs.appendFileSync(t(paths), content);
}

function u(paths) {
  return fs.unlinkSync(t(paths));
}

/**
 * Gets an array containing the routine to run the pug CLI. If this file is
 * being processed with istanbul then this function will return a routine
 * asking istanbul to store coverage data to a unique directory
 * (cov-pt<covCount>/).
 */
function getRunner() {
  var pugExe = j([__dirname, '..', 'index.js']);

  if (!isIstanbul) return [process.argv[0], [pugExe]];
  else {
    return [ 'istanbul',
             [ 'cover',
               '--print',  'none',
               '--report', 'none',
               '--root',   process.cwd(),
               '--dir',    process.cwd() + '/cov-pt' + (covCount++),
               pugExe,
               '--' ] ];
  }
}

/*
 * Run Pug CLI.
 *
 * @param  args     Array of arguments
 * @param [stdin]   Stream of standard input
 * @param  callback Function to call when the process finishes
 */
function run(args, stdin, callback) {
  if (arguments.length === 2) {
    callback = stdin;
    stdin    = null;
  }
  var runner = getRunner();
  var proc = cp.execFile(runner[0], runner[1].concat(args), {
    cwd: t([])
  }, callback);
  if (stdin) stdin.pipe(proc.stdin);
}

/**
 * Set timing limits for a test case
 */
function timing(testCase) {
  if (isIstanbul) {
    testCase.timeout(20000);
    testCase.slow(3000);
  } else {
    testCase.timeout(12500);
    testCase.slow(2000);
  }
}

/*
 * Make temporary directories
 */
rimraf.sync(t([]));
mkdirp.sync(t(['depwatch']));
mkdirp.sync(t(['inputs', 'level-1-1']));
mkdirp.sync(t(['inputs', 'level-1-2']));
mkdirp.sync(t(['outputs', 'level-1-1']));
mkdirp.sync(t(['outputs', 'level-1-2']));

/*
 * CLI utilities
 */
describe('miscellanea', function () {
  timing(this);
  it('--version', function (done) {
    run(['-V'], function (err, stdout) {
      if (err) done(err);
      assert.equal(stdout.trim(), 'pug version: ' + require('pug/package.json').version + '\npug-cli version: ' + require('../package.json').version);
      run(['--version'], function (err, stdout) {
        if (err) done(err);
        assert.equal(stdout.trim(), 'pug version: ' + require('pug/package.json').version + '\npug-cli version: ' + require('../package.json').version);
        done()
      });
    });
  });
  it('--help', function (done) {
    // only check that it doesn't crash
    run(['-h'], function (err, stdout) {
      if (err) done(err);
      run(['--help'], function (err, stdout) {
        if (err) done(err);
        done()
      });
    });
  });
});

describe('HTML output', function () {
  timing(this);
  it('works', function (done) {
    w('input.pug', '.foo bar');
    w('input.html', '<p>output not written</p>');

    run(['--no-debug', 'input.pug'], function (err) {
      if (err) return done(err);
      var html = r('input.html');
      assert(html === '<div class="foo">bar</div>');
      done();
    });
  });
  it('--extension', function (done) {
    w('input.pug', '.foo bar');
    w('input.special-html', '<p>output not written</p>');

    run(['--no-debug', '-E', 'special-html', 'input.pug'], function (err) {
      if (err) return done(err);
      var html = r('input.special-html');
      assert(html === '<div class="foo">bar</div>');
      done();
    });
  });
  it('--basedir', function (done) {
    w('input.pug', 'extends /dependency1.pug');
    w('input.html', '<p>output not written</p>');
    run(['--no-debug', '-b', j([__dirname, 'dependencies']), 'input.pug'], function (err, stdout) {
      if (err) return done(err);
      var html = r('input.html');
      assert.equal(html, '<html><body></body></html>');
      done();
    });
  });
  context('--obj', function () {
    it('JavaScript syntax works', function (done) {
      w('input.pug', '.foo= loc');
      w('input.html', '<p>output not written</p>');
      run(['--no-debug', '--obj', "{'loc':'str'}", 'input.pug'], function (err) {
        if (err) return done(err);
        var html = r('input.html');
        assert(html === '<div class="foo">str</div>');
        done();
      });
    });
    it('JavaScript syntax does not accept UTF newlines', function (done) {
      w('input.pug', '.foo= loc');
      w('input.html', '<p>output not written</p>');
      run(['--no-debug', '--obj', "{'loc':'st\u2028r'}", 'input.pug'], function (err) {
        if (!err) return done(new Error('expecting error'));
        done();
      });
    });
    it('JSON syntax accept UTF newlines', function (done) {
      w('input.pug', '.foo= loc');
      w('input.html', '<p>output not written</p>');
      run(['--no-debug', '--obj', '{"loc":"st\u2028r"}', 'input.pug'], function (err) {
        if (err) return done(err);
        var html = r('input.html');
        assert.equal(html, '<div class="foo">st\u2028r</div>');
        done();
      });
    });
    it('JSON file', function (done) {
      w('obj.json', '{"loc":"str"}');
      w('input.pug', '.foo= loc');
      w('input.html', '<p>output not written</p>');
      run(['--no-debug', '--obj', 'obj.json', 'input.pug'], function (err) {
        if (err) return done(err);
        var html = r('input.html');
        assert(html === '<div class="foo">str</div>');
        done();
      });
    });
    it('JavaScript module', function (done) {
      w('obj.js', 'module.exports = {loc: "str"};');
      w('input.pug', '.foo= loc');
      w('input.html', '<p>output not written</p>');
      run(['--no-debug', '--obj', 'obj.js', 'input.pug'], function (err) {
        if (err) return done(err);
        var html = r('input.html');
        assert(html === '<div class="foo">str</div>');
        done();
      });
    });
  });
  it('stdio', function (done) {
    w('input.pug', '.foo bar');
    run(['--no-debug'], rs('input.pug'), function (err, stdout, stderr) {
      if (err) return done(err);
      assert(stdout === '<div class="foo">bar</div>');
      done();
    });
  });
  context('--out', function () {
    it('works', function (done) {
      w('input.pug', '.foo bar');
      w('input.html', '<p>output not written</p>');
      run(['--no-debug', '--out', 'outputs', 'input.pug'], function (err) {
        if (err) return done(err);
        var html = r(['outputs', 'input.html']);
        assert(html === '<div class="foo">bar</div>');
        done();
      });
    });
    it('works when input is a directory', function (done) {
      w(['inputs', 'input.pug'], '.foo bar 1');
      w(['inputs', 'level-1-1', 'input.pug'], '.foo bar 1-1');
      w(['inputs', 'level-1-2', 'input.pug'], '.foo bar 1-2');
      w(['outputs', 'input.html'], 'BIG FAT HEN 1');
      w(['outputs', 'level-1-1', 'input.html'], 'BIG FAT HEN 1-1');
      w(['outputs', 'level-1-2', 'input.html'], 'BIG FAT HEN 1-2');

      run(['--no-debug', '--hierarchy', '--out', 'outputs', 'inputs'], function (err) {
        if (err) return done(err);
        var html = r(['outputs', 'input.html']);
        assert(html === '<div class="foo">bar 1</div>');
        var html = r(['outputs', 'level-1-1', 'input.html']);
        assert(html === '<div class="foo">bar 1-1</div>');
        var html = r(['outputs', 'level-1-2', 'input.html']);
        assert(html === '<div class="foo">bar 1-2</div>');
        done();
      });
    });
  });
  it('--silent', function (done) {
    w('input.pug', '.foo bar');
    w('input.html', '<p>output not written</p>');
    run(['--no-debug', '-s', 'input.pug'], function (err, stdout) {
      if (err) return done(err);
      var html = r('input.html');
      assert.equal(html, '<div class="foo">bar</div>');
      assert.equal(stdout, '');

      w('input.html', '<p>output not written</p>');
      run(['--no-debug', '--silent', 'input.pug'], function (err, stdout) {
        if (err) return done(err);
        var html = r('input.html');
        assert.equal(html, '<div class="foo">bar</div>');
        assert.equal(stdout, '');
        done();
      });
    });
  });
});

describe('client JavaScript output', function () {
  timing(this);
  it('works', function (done) {
    w('input.pug', '.foo bar');
    w('input.js', 'throw new Error("output not written");');
    run(['--no-debug', '--client', 'input.pug'], function (err) {
      if (err) return done(err);
      var template = Function('', r('input.js') + ';return template;')();
      assert(template() === '<div class="foo">bar</div>');
      done();
    });
  });
  it('--name', function (done) {
    w('input.pug', '.foo bar');
    w('input.js', 'throw new Error("output not written");');
    run(['--no-debug', '--client', '--name', 'myTemplate', 'input.pug'], function (err) {
      if (err) return done(err);
      var template = Function('', r('input.js') + ';return myTemplate;')();
      assert(template() === '<div class="foo">bar</div>');
      done();
    });
  });
  it('--name --extension', function (done) {
    w('input.pug', '.foo bar');
    w('input.special-js', 'throw new Error("output not written");');
    run(['--no-debug', '--client', '-E', 'special-js', 'input.pug'], function (err) {
      if (err) return done(err);
      var template = Function('', r('input.special-js') + ';return template;')();
      assert(template() === '<div class="foo">bar</div>');
      done();
    });
  });
  it('stdio', function (done) {
    w('input.pug', '.foo bar');
    w('input.js', 'throw new Error("output not written");');
    run(['--no-debug', '--client'], rs('input.pug'), function (err, stdout) {
      if (err) return done(err);
      var template = Function('', stdout + ';return template;')();
      assert(template() === '<div class="foo">bar</div>');
      done();
    });
  });
  it('--name-after-file', function (done) {
    w('input-file.pug', '.foo bar');
    w('input-file.js', 'throw new Error("output not written");');
    run(['--no-debug', '--client', '--name-after-file', 'input-file.pug'], function (err, stdout, stderr) {
      if (err) return done(err);
      var template = Function('', r('input-file.js') + ';return inputFileTemplate;')();
      assert(template() === '<div class="foo">bar</div>');
      return done();
    });
  });
  it('--name-after-file _InPuTwIthWEiRdNaMME.pug', function (done) {
    w('_InPuTwIthWEiRdNaMME.pug', '.foo bar');
    w('_InPuTwIthWEiRdNaMME.js', 'throw new Error("output not written");');
    run(['--no-debug', '--client', '--name-after-file', '_InPuTwIthWEiRdNaMME.pug'], function (err, stdout, stderr) {
      if (err) return done(err);
      var template = Function('', r('_InPuTwIthWEiRdNaMME.js') + ';return InputwithweirdnammeTemplate;')();
      assert(template() === '<div class="foo">bar</div>');
      return done();
    });
  });
});

describe('--watch', function () {
  var watchProc;
  var stdout = '';

  function cleanup() {
    stdout = '';
    if (!watchProc) return;

    watchProc.stderr.removeAllListeners('data');
    watchProc.stdout.removeAllListeners('data');
    watchProc.removeAllListeners('error');
    watchProc.removeAllListeners('close');
  }

  after(function () {
    cleanup();
    watchProc.kill('SIGINT');
    watchProc = null;
  });

  beforeEach(cleanup);

  afterEach(function (done) {
    // pug --watch can only detect changes that are at least 1 second apart
    setTimeout(done, 1000);
  });

  it('pass 1: initial compilation', function (done) {
    timing(this);

    w('input-file.pug', '.foo bar');
    w('input-file.js', 'throw new Error("output not written (pass 1)");');
    var cmd = getRunner();
    cmd[1].push('--no-debug', '--client', '--name-after-file', '--watch', 'input-file.pug');
    watchProc = cp.spawn(cmd[0], cmd[1],  {
      cwd: t([])
    });

    watchProc.stdout.setEncoding('utf8');
    watchProc.stderr.setEncoding('utf8');
    watchProc.on('error', done);
    watchProc.stdout.on('data', function(buf) {
      stdout += buf;
      if (/rendered/.test(stdout)) {
        cleanup();

        var template = Function('', r('input-file.js') + ';return inputFileTemplate;')();
        assert(template() === '<div class="foo">bar</div>');

        return done();
      }
    });
  });
  it('pass 2: change the file', function (done) {
    w('input-file.js', 'throw new Error("output not written (pass 2)");');

    watchProc.on('error', done);
    watchProc.stdout.on('data', function(buf) {
      stdout += buf;
      if (/rendered/.test(stdout)) {
        cleanup();

        var template = Function('', r('input-file.js') + ';return inputFileTemplate;')();
        assert(template() === '<div class="foo">baz</div>');

        return done();
      }
    });

    w('input-file.pug', '.foo baz');
  });
  it('pass 3: remove the file then add it back', function (done) {
    w('input-file.js', 'throw new Error("output not written (pass 3)");');

    watchProc.on('error', done)
    watchProc.stdout.on('data', function(buf) {
      stdout += buf;
      if (/rendered/.test(stdout)) {
        cleanup();

        var template = Function('', r('input-file.js') + ';return inputFileTemplate;')();
        assert(template() === '<div class="foo">bat</div>');

        return done();
      }
    });

    u('input-file.pug');
    setTimeout(function () {
      w('input-file.pug', '.foo bat');
    }, 250);
  });
  it('pass 4: intentional errors in the pug file', function (done) {
    var stderr = '';
    var errored = false;

    watchProc.on('error', done);
    watchProc.on('close', function() {
      errored = true;
      return done(new Error('Pug should not terminate in watch mode'));
    });
    watchProc.stdout.on('data', function(buf) {
      stdout += buf;
      if (/rendered/.test(stdout)) {
        stdout = '';
        return done(new Error('Pug compiles an erroneous file w/o error'));
      }
    });
    watchProc.stderr.on('data', function(buf) {
      stderr += buf;
      if (!/Invalid indentation/.test(stderr)) return;
      stderr = '';
      var template = Function('', r('input-file.js') + ';return inputFileTemplate;')();
      assert(template() === '<div class="foo">bat</div>');

      watchProc.stderr.removeAllListeners('data');
      watchProc.stdout.removeAllListeners('data');
      watchProc.removeAllListeners('error');
      watchProc.removeAllListeners('exit');
      // The stderr event will always fire sooner than the close event.
      // Wait for it.
      setTimeout(function() {
        if (!errored) done();
      }, 100);
    });

    w('input-file.pug', [
      'div',
      '  div',
      '\tarticle'
    ].join('\n'));
  });
});

describe('--watch with dependencies', function () {
  var watchProc;
  var stdout = '';

  before(function () {
    function copy(file) {
      w(['depwatch', file],
        fs.readFileSync(j([__dirname, 'dependencies', file])));
    }
    copy('include2.pug');
    copy('dependency2.pug');
    copy('dependency3.pug');
  });

  function cleanup() {
    stdout = '';

    if (!watchProc) return;

    watchProc.stderr.removeAllListeners('data');
    watchProc.stdout.removeAllListeners('data');
    watchProc.removeAllListeners('error');
    watchProc.removeAllListeners('close');
  }

  after(function () {
    cleanup();
    watchProc.kill('SIGINT');
    watchProc = null;
  });

  beforeEach(cleanup);

  afterEach(function (done) {
    // pug --watch can only detect changes that are at least 1 second apart
    setTimeout(done, 1000);
  });

  it('pass 1: initial compilation', function (done) {
    timing(this);

    w(['depwatch', 'include2.html'],    'output not written (pass 1)');
    w(['depwatch', 'dependency2.html'], 'output not written (pass 1)');
    var cmd = getRunner();
    cmd[1].push('--watch', 'include2.pug', 'dependency2.pug');
    watchProc = cp.spawn(cmd[0], cmd[1],  {
      cwd: t('depwatch')
    });

    watchProc.stdout.setEncoding('utf8');
    watchProc.stderr.setEncoding('utf8');
    watchProc.on('error', done);
    watchProc.stdout.on('data', function(buf) {
      stdout += buf;
      if ((stdout.match(/rendered/g) || []).length === 2) {
        cleanup();

        var output = r(['depwatch', 'include2.html']);
        assert.equal(output.trim(), '<strong>dependency3</strong>');
        output = r(['depwatch', 'dependency2.html']);
        assert.equal(output.trim(), '<strong>dependency3</strong>');

        return done();
      }
    });
  });
  it('pass 2: change a dependency', function (done) {
    timing(this);

    w(['depwatch', 'include2.html'],    'output not written (pass 2)');
    w(['depwatch', 'dependency2.html'], 'output not written (pass 2)');

    watchProc.on('error', done);
    watchProc.stdout.on('data', function(buf) {
      stdout += buf;
      if ((stdout.match(/rendered/g) || []).length === 2) {
        cleanup();

        var output = r(['depwatch', 'include2.html']);
        assert.equal(output.trim(), '<strong>dependency3</strong><p>Hey</p>');
        output = r(['depwatch', 'dependency2.html']);
        assert.equal(output.trim(), '<strong>dependency3</strong><p>Hey</p>');

        return done();
      }
    });

    a(['depwatch', 'dependency2.pug'], '\np Hey\n');
  });
  it('pass 3: change a deeper dependency', function (done) {
    timing(this);

    w(['depwatch', 'include2.html'],    'output not written (pass 3)');
    w(['depwatch', 'dependency2.html'], 'output not written (pass 3)');

    watchProc.on('error', done)
    watchProc.stdout.on('data', function(buf) {
      stdout += buf;
      if ((stdout.match(/rendered/g) || []).length === 2) {
        cleanup();

        var output = r(['depwatch', 'include2.html']);
        assert.equal(output.trim(), '<strong>dependency3</strong><p>Foo</p><p>Hey</p>');
        output = r(['depwatch', 'dependency2.html']);
        assert.equal(output.trim(), '<strong>dependency3</strong><p>Foo</p><p>Hey</p>');

        return done();
      }
    });

    a(['depwatch', 'dependency3.pug'], '\np Foo\n');
  });
  it('pass 4: change main file', function (done) {
    timing(this);

    w(['depwatch', 'include2.html'],    'output not written (pass 4)');
    w(['depwatch', 'dependency2.html'], 'output not written (pass 4)');

    watchProc.on('error', done);
    watchProc.stdout.on('data', function(buf) {
      stdout += buf;
      if ((stdout.match(/rendered/g) || []).length === 1) {
        cleanup();

        var output = r(['depwatch', 'include2.html']);
        assert.equal(output.trim(), '<strong>dependency3</strong><p>Foo</p><p>Hey</p><p>Baz</p>');
        output = r(['depwatch', 'dependency2.html']);
        assert.equal(output.trim(), 'output not written (pass 4)');

        return done();
      }
    });

    a(['depwatch', 'include2.pug'], '\np Baz\n');
  });
});
