'use strict';

var fs                = require('fs');
var Blueprint         = require('../../../lib/models/blueprint');
var Task              = require('../../../lib/models/task');
var MockProject       = require('../../helpers/mock-project');
var MockUI            = require('../../helpers/mock-ui');
var assert            = require('assert');
var path              = require('path');
var walkSync          = require('walk-sync');
var Promise           = require('../../../lib/ext/promise');
var rimraf            = Promise.denodeify(require('rimraf'));
var EOL               = require('os').EOL;
var root              = process.cwd();
var tmp               = require('tmp-sync');
var tmproot           = path.join(root, 'tmp');
var SilentError       = require('../../../lib/errors/silent');

var defaultBlueprints = path.resolve(__dirname, '..', '..', '..', 'blueprints');
var fixtureBlueprints = path.resolve(__dirname, '..', '..', 'fixtures', 'blueprints');
var basicBlueprint    = path.join(fixtureBlueprints, 'basic');
var basicNewBlueprint = path.join(fixtureBlueprints, 'basic_2');

var defaultIgnoredFiles = Blueprint.ignoredFiles;

var basicBlueprintFiles = [
  '.ember-cli',
  '.gitignore',
  'foo.txt',
  'test.txt'
];

assert.match = function(actual, matcher) {
  assert(matcher.test(actual), 'expected: ' +
                                actual +
                                ' to match ' +
                                matcher);
};

describe('Blueprint', function() {
  beforeEach(function() {
    Blueprint.ignoredFiles = defaultIgnoredFiles;
  });

  describe('.mapFile', function() {
    it('replaces all occurences of __name__ with module name',function(){
      var path = Blueprint.prototype.mapFile('__name__/__name__-controller.js',{dasherizedModuleName: 'my-blueprint'});
      assert.equal(path,'my-blueprint/my-blueprint-controller.js');

      path = Blueprint.prototype.mapFile('__name__/controller.js',{dasherizedModuleName: 'my-blueprint'});
      assert.equal(path,'my-blueprint/controller.js');

      path = Blueprint.prototype.mapFile('__name__/__name__.js',{dasherizedModuleName: 'my-blueprint'});
      assert.equal(path,'my-blueprint/my-blueprint.js');
    });
  });
  describe('.lookup', function() {
    it('uses an explicit path if one is given', function() {
      var expectedClass = require(basicBlueprint);
      var blueprint = Blueprint.lookup(basicBlueprint);

      assert.equal(blueprint.name, 'basic');
      assert.equal(blueprint.path, basicBlueprint);
      assert(blueprint instanceof expectedClass);
    });

    it('finds blueprints within given lookup paths', function() {
      var expectedClass = require(basicBlueprint);
      var blueprint = Blueprint.lookup('basic', {
        paths: [fixtureBlueprints]
      });

      assert.equal(blueprint.name, 'basic');
      assert.equal(blueprint.path, basicBlueprint);
      assert(blueprint instanceof expectedClass);
    });

    it('finds blueprints in the ember-cli package', function() {
      var expectedPath = path.resolve(defaultBlueprints, 'app');
      var expectedClass = Blueprint;

      var blueprint = Blueprint.lookup('app');

      assert.equal(blueprint.name, 'app');
      assert.equal(blueprint.path, expectedPath);
      assert(blueprint instanceof expectedClass);
    });

    it('can instantiate a blueprint that exports an object instead of a constructor', function() {
      var blueprint = Blueprint.lookup('exporting-object', {
        paths: [fixtureBlueprints]
      });

      assert.equal(blueprint.woot, 'someValueHere');
      assert(blueprint instanceof Blueprint);
    });

    it('throws an error if no blueprint is found', function() {
      assert.throws(function() {
        Blueprint.lookup('foo');
      }, 'Unknown blueprint: foo');
    });

    it('returns undefined if no blueprint is found and ignoredMissing is passed', function() {
      var blueprint = Blueprint.lookup('foo', {
        ignoreMissing: true
      });

      assert.equal(blueprint, undefined);
    });
  });

  describe('.list', function() {
    it('returns a list of blueprints grouped by lookup path', function() {
      var list = Blueprint.list({ paths: [fixtureBlueprints] });
      var actual = list[0];
      var expected = {
        source: 'fixtures',
        blueprints: [{
          name: 'basic',
          description: 'A basic blueprint',
          overridden: false
        }, {
          name: 'basic_2',
          description: 'Another basic blueprint',
          overridden: false
        }, {
          name: 'exporting-object',
          description: 'A blueprint that exports an object',
          overridden: false
        }, {
          name: 'with-templating',
          description: 'A blueprint with templating',
          overridden: false
        }]
      };

      assert.deepEqual(actual[0], expected[0]);
    });
  });

  it('exists', function() {
    var blueprint = new Blueprint(basicBlueprint);
    assert(blueprint);
  });

  it('derives name from path', function() {
    var blueprint = new Blueprint(basicBlueprint);
    assert.equal(blueprint.name, 'basic');
  });

  describe('basic blueprint installation', function() {
    var blueprint;
    var ui;
    var project;
    var options;
    var tmpdir;

    beforeEach(function() {
      tmpdir    = tmp.in(tmproot);
      blueprint = new Blueprint(basicBlueprint);
      ui        = new MockUI();
      project   = new MockProject();
      options   = {
        ui: ui,
        project: project,
        target: tmpdir
      };
    });

    afterEach(function() {
      return rimraf(tmproot);
    });

    it('installs basic files', function() {
      assert(blueprint);

      return blueprint.install(options)
        .then(function() {
          var actualFiles = walkSync(tmpdir).sort();
          var output = ui.output.trim().split(EOL);

          assert.match(output.shift(), /^installing/);
          assert.match(output.shift(), /create.* .ember-cli/);
          assert.match(output.shift(), /create.* .gitignore/);
          assert.match(output.shift(), /create.* foo.txt/);
          assert.match(output.shift(), /create.* test.txt/);
          assert.equal(output.length, 0);

          assert.deepEqual(actualFiles, basicBlueprintFiles);
        });
    });

    it('re-installing identical files', function() {
      return blueprint.install(options)
        .then(function() {
          var output = ui.output.trim().split(EOL);
          ui.output = '';

          assert.match(output.shift(), /^installing/);
          assert.match(output.shift(), /create.* \.ember-cli/);
          assert.match(output.shift(), /create.* \.gitignore/);
          assert.match(output.shift(), /create.* foo.txt/);
          assert.match(output.shift(), /create.* test.txt/);
          assert.equal(output.length, 0);

          return blueprint.install(options);
        })
        .then(function() {
          var actualFiles = walkSync(tmpdir).sort();
          var output = ui.output.trim().split(EOL);

          assert.match(output.shift(), /^installing/);
          assert.match(output.shift(), /identical.* \.ember-cli/);
          assert.match(output.shift(), /identical.* \.gitignore/);
          assert.match(output.shift(), /identical.* foo.txt/);
          assert.match(output.shift(), /identical.* test.txt/);
          assert.equal(output.length, 0);

          assert.deepEqual(actualFiles, basicBlueprintFiles);
        });
    });

    it('re-installing conflicting files', function() {
      return blueprint.install(options)
        .then(function() {
          var output = ui.output.trim().split(EOL);
          ui.output = '';

          assert.match(output.shift(), /^installing/);
          assert.match(output.shift(), /create.* \.ember-cli/);
          assert.match(output.shift(), /create.* \.gitignore/);
          assert.match(output.shift(), /create.* foo.txt/);
          assert.match(output.shift(), /create.* test.txt/);
          assert.equal(output.length, 0);

          var blueprintNew = new Blueprint(basicNewBlueprint);

          setTimeout(function(){
            ui.inputStream.write('n' + EOL);
          }, 25);

          setTimeout(function(){
            ui.inputStream.write('y' + EOL);
          }, 50);

          return blueprintNew.install(options);
        })
        .then(function() {
          var actualFiles = walkSync(tmpdir).sort();
          // Prompts contain \n EOL
          // Split output on \n since it will have the same affect as spliting on OS specific EOL
          var output = ui.output.trim().split('\n');
          assert.match(output.shift(), /^installing/);
          assert.match(output.shift(), /Overwrite.*foo.*\?/); // Prompt
          assert.match(output.shift(), /Overwrite.*foo.*No, skip/);
          assert.match(output.shift(), /Overwrite.*test.*\?/); // Prompt
          assert.match(output.shift(), /Overwrite.*test.*Yes, overwrite/);
          assert.match(output.shift(), /identical.* \.ember-cli/);
          assert.match(output.shift(), /identical.* \.gitignore/);
          assert.match(output.shift(), /skip.* foo.txt/);
          assert.match(output.shift(), /overwrite.* test.txt/);
          assert.equal(output.length, 0);

          assert.deepEqual(actualFiles, basicBlueprintFiles);
        });
    });

    describe('called on an existing project', function() {
      beforeEach(function() {
        Blueprint.ignoredUpdateFiles.push('foo.txt');
      });

      it('ignores files in ignoredUpdateFiles', function() {
        return blueprint.install(options)
          .then(function() {
            var output = ui.output.trim().split(EOL);
            ui.output = '';

            assert.match(output.shift(), /^installing/);
            assert.match(output.shift(), /create.* \.ember-cli/);
            assert.match(output.shift(), /create.* \.gitignore/);
            assert.match(output.shift(), /create.* foo.txt/);
            assert.match(output.shift(), /create.* test.txt/);
            assert.equal(output.length, 0);

            var blueprintNew = new Blueprint(basicNewBlueprint);

            setTimeout(function(){
              ui.inputStream.write('n' + EOL);
            }, 25);

            setTimeout(function(){
              ui.inputStream.write('n'+ EOL);
            }, 50);

            options.project.isEmberCLIProject = function() { return true; };

            return blueprintNew.install(options);
          })
          .then(function() {
            var actualFiles = walkSync(tmpdir).sort();
            // Prompts contain \n EOL
            // Split output on \n since it will have the same affect as spliting on OS specific EOL
            var output = ui.output.trim().split('\n');
            assert.match(output.shift(), /^installing/);
            assert.match(output.shift(), /Overwrite.*test.*\?/); // Prompt
            assert.match(output.shift(), /Overwrite.*test.*No, skip/);
            assert.match(output.shift(), /identical.* \.ember-cli/);
            assert.match(output.shift(), /identical.* \.gitignore/);
            assert.match(output.shift(), /skip.* test.txt/);
            assert.equal(output.length, 0);

            assert.deepEqual(actualFiles, basicBlueprintFiles);
          });
      });
    });

    it('throws error when there is a trailing forward slash in entityName', function(){
      options.entity = { name: 'foo/' };
      assert.throws(function(){
        blueprint.install(options);
      }, /You specified "foo\/", but you can't use a trailing slash as an entity name with generators. Please re-run the command with "foo"./);

      options.entity = { name: 'foo\\' };
      assert.throws(function(){
        blueprint.install(options);
      }, /You specified "foo\\", but you can't use a trailing slash as an entity name with generators. Please re-run the command with "foo"./);

      options.entity = { name: 'foo' };
      assert.doesNotThrow(function(){
        blueprint.install(options);
      });
    });

    it('throws error when an entityName is not provided', function(){
      options.entity = { };
      assert.throws(function(){
        blueprint.install(options);
      }, SilentError, /'The `ember generate` command requires an entity name to be specified./);
    });


    it('calls normalizeEntityName hook during install', function(done){
      blueprint.normalizeEntityName = function(){ done(); };
      options.entity = { name: 'foo' };
      blueprint.install(options);
    });

    it('normalizeEntityName hook can modify the entity name', function(){
      blueprint.normalizeEntityName = function(){ return 'foo'; };
      options.entity = { name: 'bar' };

      return blueprint.install(options)
          .then(function() {
            var actualFiles = walkSync(tmpdir).sort();

            assert.deepEqual(actualFiles, basicBlueprintFiles);
          });
    });

    it('calls normalizeEntityName before locals hook is called', function(done) {
      blueprint.normalizeEntityName = function(){ return 'foo'; };
      blueprint.locals = function(options) {
        assert.equal(options.entity.name, 'foo');
        done();
      };
      options.entity = { name: 'bar' };
      blueprint.install(options);
    });
  });

  describe('addPackageToProject', function() {
    var blueprint;
    var ui;
    var tmpdir;

    beforeEach(function() {
      tmpdir    = tmp.in(tmproot);
      blueprint = new Blueprint(basicBlueprint);
      ui        = new MockUI();
    });

    afterEach(function() {
      return rimraf(tmproot);
    });

    it('calls _exec with the proper command when no version is supplied', function() {
      blueprint._exec = function(command) {
        assert.equal(command, 'npm install --save-dev foo-bar');
      };

      blueprint.addPackageToProject('foo-bar');
    });

    it('calls _exec with the proper command when a version is supplied', function() {
      blueprint._exec = function(command) {
        assert.equal(command, 'npm install --save-dev foo-bar@^123.1.12');
      };

      blueprint.addPackageToProject('foo-bar', '^123.1.12');
    });

    it('writes information to the ui log', function() {
      blueprint._exec = function() { };
      blueprint.ui = ui;

      blueprint.addPackageToProject('foo-bar', '^123.1.12');

      var output = ui.output.trim();

      assert.match(output, /install package.*foo-bar/);
    });

    it('does not error if ui is not present', function() {
      blueprint._exec = function() { };
      delete blueprint.ui;

      blueprint.addPackageToProject('foo-bar', '^123.1.12');

      var output = ui.output.trim();

      assert(!output.match(/install package.*foo-bar/));
    });
  });

  describe('addBowerPackageToProject', function() {
    var blueprint;
    var ui;
    var tmpdir;
    var BowerInstallTask;
    var taskNameLookedUp;

    beforeEach(function() {
      tmpdir    = tmp.in(tmproot);
      blueprint = new Blueprint(basicBlueprint);
      ui        = new MockUI();

      blueprint.taskFor = function(name) {
        taskNameLookedUp = name;

        return new BowerInstallTask();
      };
    });

    afterEach(function() {
      return rimraf(tmproot);
    });

    it('looks up the `bower-install` task', function() {
      BowerInstallTask = Task.extend({
        run: function() {}
      });
      blueprint.addBowerPackageToProject('foo-bar');

      assert.equal(taskNameLookedUp, 'bower-install');
    });

    it('calls the task with the package name', function() {
      var packages;

      BowerInstallTask = Task.extend({
        run: function(options) {
          packages = options.packages;
        }
      });

      blueprint.addBowerPackageToProject('foo-bar');

      assert.deepEqual(packages, ['foo-bar']);
    });

    it('uses the provided target (version, range, sha, etc)', function() {
      var packages;

      BowerInstallTask = Task.extend({
        run: function(options) {
          packages = options.packages;
        }
      });

      blueprint.addBowerPackageToProject('foo-bar', '~1.0.0');

      assert.deepEqual(packages, ['foo-bar#~1.0.0']);
    });

    it('uses uses verbose mode with the task', function() {
      var verbose;

      BowerInstallTask = Task.extend({
        run: function(options) {
          verbose = options.verbose;
        }
      });

      blueprint.addBowerPackageToProject('foo-bar', '~1.0.0');

      assert(verbose);
    });
  });

  describe('insertIntoFile', function() {
    var blueprint;
    var ui;
    var tmpdir;
    var project;
    var filename;

    beforeEach(function() {
      tmpdir    = tmp.in(tmproot);
      blueprint = new Blueprint(basicBlueprint);
      ui        = new MockUI();
      project   = new MockProject();

      // normally provided by `install`, but mocked here for testing
      project.root = tmpdir;
      blueprint.project = project;

      filename = 'foo-bar-baz.txt';
    });

    afterEach(function() {
      return rimraf(tmproot);
    });

    it('will create the file if not already existing', function() {
      var toInsert = 'blahzorz blammo';

      return blueprint.insertIntoFile(filename, toInsert)
        .then(function(result) {
          var contents = fs.readFileSync(path.join(project.root, filename), { encoding: 'utf8' });

          assert(contents.indexOf(toInsert) > -1, 'contents were inserted');
          assert.equal(result.originalContents, '', 'returned object should contain original contents');
          assert(result.inserted, 'inserted should indicate that the file was modified');
          assert.equal(contents, result.contents, 'returned object should contain contents');
        });
    });

    it('will insert into the file if it already exists', function() {
      var toInsert = 'blahzorz blammo';
      var originalContent = 'some original content\n';
      var filePath = path.join(project.root, filename);

      fs.writeFileSync(filePath, originalContent, { encoding: 'utf8' });

      return blueprint.insertIntoFile(filename, toInsert)
        .then(function(result) {
          var contents = fs.readFileSync(path.join(project.root, filename), { encoding: 'utf8' });

          assert.equal(contents, originalContent + toInsert, 'inserted contents should be appended to original');
          assert.equal(result.originalContents, originalContent, 'returned object should contain original contents');
          assert(result.inserted, 'inserted should indicate that the file was modified');
        });
    });

    it('will not insert into the file if it already contains the content', function() {
      var toInsert = 'blahzorz blammo';
      var filePath = path.join(project.root, filename);

      fs.writeFileSync(filePath, toInsert, { encoding: 'utf8' });

      return blueprint.insertIntoFile(filename, toInsert)
        .then(function(result) {
          var contents = fs.readFileSync(path.join(project.root, filename), { encoding: 'utf8' });

          assert.equal(contents, toInsert, 'contents should be unchanged');
          assert(!result.inserted, 'inserted should indicate that the file was not modified');
        });
    });

    it('will insert into the file if it already contains the content if force option is passed', function() {
      var toInsert = 'blahzorz blammo';
      var filePath = path.join(project.root, filename);

      fs.writeFileSync(filePath, toInsert, { encoding: 'utf8' });

      return blueprint.insertIntoFile(filename, toInsert, { force: true })
        .then(function(result) {
          var contents = fs.readFileSync(path.join(project.root, filename), { encoding: 'utf8' });

          assert.equal(contents, toInsert + toInsert, 'contents should be unchanged');
          assert(result.inserted, 'inserted should indicate that the file was not modified');
        });
    });
  });

  describe('lookupBlueprint', function() {
    var blueprint;
    var ui;
    var tmpdir;
    var project;
    var filename;

    beforeEach(function() {
      tmpdir    = tmp.in(tmproot);
      blueprint = new Blueprint(basicBlueprint);
      ui        = new MockUI();
      project   = new MockProject();

      // normally provided by `install`, but mocked here for testing
      project.root = tmpdir;
      blueprint.project = project;
      project.blueprintLookupPaths = function() {
        return [fixtureBlueprints];
      };

      filename = 'foo-bar-baz.txt';
    });

    afterEach(function() {
      return rimraf(tmproot);
    });

    it('can lookup other Blueprints from the project blueprintLookupPaths', function() {
      var result = blueprint.lookupBlueprint('basic_2');

      assert.equal(result.description, 'Another basic blueprint');
    });

    it('can find internal blueprints', function() {
      var result = blueprint.lookupBlueprint('controller');

      assert.equal(result.description, 'Generates a controller of the given type.');
    });
  });
});
