/*
 * prompt-test.js: Tests for prompt.
 *
 * (C) 2010, Nodejitsu Inc.
 *
 */

var assert = require('assert'),
    vows = require('vows'),
    prompt = require('../lib/prompt'),
    helpers = require('./helpers'),
    macros = require('./macros'),
    schema = helpers.schema;

// A helper to pass fragments of our schema into prompt as full schemas.
function grab () {
  var names = [].slice.call(arguments),
      complete = { schema: {} };

  names.forEach(function (name) {
    complete.path = [name],
    complete.schema = schema.properties[name];
  });
  return complete;
};

//
// Reset the prompt for mock testing
//
prompt.started = false;
prompt.start({
  stdin: helpers.stdin,
  stdout: helpers.stdout
});

vows.describe('prompt').addBatch({
  "When using prompt": {
    "the getInput() method": {
      "with a simple string prompt": {
        topic: function () {
          var that = this;
          helpers.stdout.once('data', function (msg) {
            that.msg = msg;
          })

          prompt.getInput('test input', this.callback);
          helpers.stdin.writeNextTick('test value\n');
        },
        "should prompt to stdout and respond with data": function (err, input) {
          assert.isNull(err);
          assert.equal(input, 'test value');
          assert.isTrue(this.msg.indexOf('test input') !== -1);
        }
      },
    }
  }
}).addBatch({
  "When using prompt": {
    "the getInput() method": {
      "with any field that is not supposed to be empty": {
        "and we don't provide any input": {
          topic: function () {
            var that = this;
            helpers.stdout.once('data', function (msg) {
              that.msg = msg;
            });

            helpers.stderr.once('data', function (msg) {
              that.errmsg = msg;
            });

            prompt.getInput(grab('notblank'), function () {});
            prompt.once('invalid', this.callback.bind(null, null))
            helpers.stdin.writeNextTick('\n');
          },
          "should prompt with an error": function (_, prop, input) {
            assert.isObject(prop);
            assert.equal(input, '');
            assert.isTrue(this.errmsg.indexOf('Invalid input') !== -1);
            assert.isTrue(this.msg.indexOf('notblank') !== -1);
          }
        }
      }
    }
  }
}).addBatch({
  "When using prompt": {
    "the getInput() method": {
      "with a hidden field that is not supposed to be empty": {
        "and we provide valid input": {
          topic: function () {
            var that = this;
            helpers.stdout.once('data', function (msg) {
              that.msg = msg;
            });

            prompt.getInput('password', this.callback);
            helpers.stdin.writeNextTick('trustno1\n');
          },

          "should prompt to stdout and respond with data": function (err, input) {
            assert.isNull(err);
            assert.equal(input, 'trustno1');
            assert.isTrue(this.msg.indexOf('password') !== -1);
          }
        },
      }
    }
  }
}).addBatch({
  "When using prompt": {
    "the getInput() method": {
      "with a hidden field that is not supposed to be empty": {
        "and we don't provide an input": {
          topic: function () {
            var that = this;
            helpers.stdout.once('data', function (msg) {
              that.msg = msg;
            });

            helpers.stderr.once('data', function (msg) {
              that.errmsg = msg;
            });

            prompt.getInput(grab('password'), function () {} );
            prompt.once('invalid', this.callback.bind(null, null))
            helpers.stdin.writeNextTick('\n');
          },
          "should prompt with an error": function (ign, prop, input) {
            assert.isObject(prop);
            assert.equal(input, '');
            assert.isTrue(this.errmsg.indexOf('Invalid input') !== -1);
            assert.isTrue(this.msg.indexOf('password') !== -1);
          }
        }
      }
    }
  }
}).addBatch({
  "When using prompt": {
    "the getInput() method": {
      "with an integer field": {
        "and we provide valid input": {
          topic: function () {
            var that = this;
            helpers.stdout.once('data', function (msg) {
              that.msg = msg;
            });

            prompt.getInput(grab('integer'), this.callback);
            helpers.stdin.writeNextTick('42\n');
          },
          "should prompt to stdout and respond with data": function (err, input) {
            assert.isNull(err);
            assert.equal(input, '42');
            assert.isTrue(this.msg.indexOf('integer') !== -1);
          }
        },
      }
    }
  }
}).addBatch({
  "When using prompt": {
    "the getInput() method": {
      "with an integer field": {
        "and we don't provide an integer": {
          topic: function () {
            var that = this;
            helpers.stdout.once('data', function (msg) {
              that.msg = msg;
            });

            helpers.stderr.once('data', function (msg) {
              that.errmsg = msg;
            })

            prompt.getInput(grab('integer'), this.callback);

            prompt.once('invalid', function () {
              prompt.once('prompt', function () {
                process.nextTick(function () {
                  helpers.stdin.writeNextTick('42\n');
                })
              })
            });

            helpers.stdin.writeNextTick('4.2\n');
          },
          "should prompt with an error before completing the operation": function (err, input) {
            assert.isNull(err);
            assert.equal(input, '42');
            assert.isTrue(this.errmsg.indexOf('Invalid input') !== -1);
            assert.isTrue(this.msg.indexOf('integer') !== -1);
          }
        }
      }
    }
  }
}).addBatch({
  "When using prompt": {
    "the getInput() method": {
      "with a boolean field": {
        "and we provide valid input": {
          topic: function () {
            var that = this;
            helpers.stdout.once('data', function (msg) {
              that.msg = msg;
            });

            prompt.getInput(grab('boolean'), this.callback);
            helpers.stdin.writeNextTick('true\n');
          },
          "should prompt to stdout and respond with data": function (err, input) {
            assert.isNull(err);
            assert.equal(input, true);
            assert.isTrue(this.msg.indexOf('boolean') !== -1);
          }
        },
      }
    }
  }
}).addBatch({
  "When using prompt": {
    "the getInput() method": {
      "with a boolean field": {
        "and we don't provide an bool": {
          topic: function () {
            var that = this;
            helpers.stdout.once('data', function (msg) {
              that.msg = msg;
            });

            helpers.stderr.once('data', function (msg) {
              that.errmsg = msg;
            })

            prompt.getInput(grab('boolean'), this.callback);

            prompt.once('invalid', function () {
              prompt.once('prompt', function () {
                process.nextTick(function () {
                  helpers.stdin.writeNextTick('F\n');
                })
              })
            });

            helpers.stdin.writeNextTick('4.2\n');
          },
          "should prompt with an error before completing the operation": function (err, input) {
            assert.isNull(err);
            assert.equal(input, false);
            assert.isTrue(this.errmsg.indexOf('Invalid input') !== -1);
            assert.isTrue(this.msg.indexOf('boolean') !== -1);
          }
        }
      }
    }
  }
}).addBatch({
  "When using prompt": {
    "the getInput() method": {
      "with a complex property prompt": {
        "and a valid input": {
          topic: function () {
            var that = this;
            helpers.stdout.once('data', function (msg) {
              that.msg = msg;
            });

            prompt.getInput(grab('username'), this.callback);
            helpers.stdin.writeNextTick('some-user\n');
          },
          "should prompt to stdout and respond with data": function (err, input) {
            assert.isNull(err);
            assert.equal(input, 'some-user');
            assert.isTrue(this.msg.indexOf('username') !== -1);
          }
        }
      }
    }
  }
}).addBatch({
  "When using prompt": {
    "the getInput() method": {
      "with a complex property prompt": {
        "and an invalid input": {
          topic: function () {
            var that = this;
            helpers.stdout.once('data', function (msg) {
              that.msg = msg;
            });

            helpers.stderr.once('data', function (msg) {
              that.errmsg = msg;
            })

            prompt.getInput(grab('username'), this.callback);

            prompt.once('invalid', function () {
              prompt.once('prompt', function () {
                process.nextTick(function () {
                  helpers.stdin.writeNextTick('some-user\n');
                })
              })
            });

            helpers.stdin.writeNextTick('some -user\n');
          },
          "should prompt with an error before completing the operation": function (err, input) {
            assert.isNull(err);
            assert.equal(input, 'some-user');
            assert.isTrue(this.errmsg.indexOf('Username can only be letters, numbers, and dashes') !== -1);
            assert.isTrue(this.msg.indexOf('username') !== -1);
          }
        }
      }
    }
  }
}).addBatch({
  "When using prompt": {
    "the getInput() method": {
      "with a complex property prompt": {
        "with an invalid validator (array)": {
          topic: function () {
            var that = this,
                called;

            prompt.getInput(grab('badValidator'), function (err) {
              if (!called) {
                called = true;
                that.callback(err);
              }
            });
            helpers.stdin.writeNextTick('some-user\n');
          },
          "should respond with an error": function (err, ign) {
            assert.isTrue(!!err);
          }
        }
      }
    }
  }
}).addBatch({
  "When using prompt": {
    "the get() method": {
      "with a simple string prompt": {
        "that is not a property in prompt.properties": {
          topic: function () {
            var that = this;
            helpers.stdout.once('data', function (msg) {
              that.msg = msg;
            })

            prompt.get('test input', this.callback);
            helpers.stdin.writeNextTick('test value\n');
          },
          "should prompt to stdout and respond with the value": function (err, result) {
            assert.isNull(err);
            assert.include(result, 'test input');
            assert.equal(result['test input'], 'test value');
            assert.isTrue(this.msg.indexOf('test input') !== -1);
          }
        },
      }
    }
  }
}).addBatch({
  "When using prompt": {
    "the get() method": {
      "with a simple string prompt": {
        "that is a property name in prompt.properties": {
          "with a default value": {
            topic: function () {
              var that = this;

              helpers.stdout.once('data', function (msg) {
                that.msg = msg;
              });

              prompt.properties.riffwabbles = schema.properties.riffwabbles;
              prompt.get('riffwabbles', this.callback);
              helpers.stdin.writeNextTick('\n');
            },
            "should prompt to stdout and respond with the default value": function (err, result) {
              assert.isNull(err);
              assert.isTrue(this.msg.indexOf('riffwabbles') !== -1);
              assert.isTrue(this.msg.indexOf('(foobizzles)') !== -1);
              assert.include(result, 'riffwabbles');
              assert.equal(result['riffwabbles'], schema.properties['riffwabbles'].default);
            }
          },
        }
      }
    }
  }
}).addBatch({
  "When using prompt": {
    "the get() method": {
      "with a simple string prompt": {
        "that is a property name in prompt.properties": {
          "that expects a numeric value": {
            "and gets valid input": {
              topic: function () {
                var that = this;

                helpers.stdout.once('data', function (msg) {
                  that.msg = msg;
                });

                prompt.properties.number = schema.properties.number;
                prompt.get('number', this.callback);
                helpers.stdin.writeNextTick('15\n');
              },
              "should prompt to stdout and respond with a numeric value": function (err, result) {
                assert.isNull(err);
                assert.include(result, 'number');
                assert.equal(result['number'], 15);
              }
            }
          }
        }
      }
    }
  }
}).addBatch({
  "When using prompt": {
    "the get() method": {
      "with a simple string prompt": {
        "that is a property name in prompt.properties": {
          "with a sync function validator (.validator)": {
            topic: function () {
              var that = this;

              helpers.stdout.once('data', function (msg) {
                that.msg = msg;
              });

              prompt.get(helpers.schema.properties.fnvalidator, this.callback);
              helpers.stdin.writeNextTick('fn123\n');
            },
            "should accept a value that is checked": function (err, result) {
              assert.isNull(err);
              assert.equal(result['fnvalidator'],'fn123');
            }
          }
        }
      }
    }
  }
}).addBatch({
  "When using prompt": {
    "the get() method": {
      "with a simple string prompt": {
        "that is a property name in prompt.properties": {
          "with a sync function validator (.conform)": {
            topic: function () {
              var that = this;

              helpers.stdout.once('data', function (msg) {
                that.msg = msg;
              });

              prompt.get(grab('fnconform'), this.callback);
              helpers.stdin.writeNextTick('fn123\n');
            },
            "should accept a value that is checked": function (err, result) {
              assert.isNull(err);
              assert.equal(result['fnconform'],'fn123');
            }
          }
          //
          // Remark Does not work with revalidator
          //
          // "with a callback validator": {
          //   topic: function () {
          //     var that = this;
          //
          //     helpers.stdout.once('data', function (msg) {
          //       that.msg = msg;
          //     });
          //
          //     prompt.get(grab('cbvalidator'), this.callback);
          //     helpers.stdin.writeNextTick('cb123\n');
          //   },
          //   "should not accept a value that is correct": function (err, result) {
          //     assert.isNull(err);
          //     assert.equal(result['cbvalidator'],'cb123');
          //   }
          // }
        }
      },
    }
  }
}).addBatch({
  "When using prompt": {
    "the get() method": {
      "with a simple string prompt": {
        "that is a property name in prompt.properties": {
          "with a sync function before (.before)": {
            topic: function() {
              var that = this;

              helpers.stdout.once('data', function(msg) {
                that.msg = msg;
              });

              prompt.get({
                properties: {
                  fnbefore: {
                    before: function(v) {
                      return 'v' + v;
                    }
                  }
                }
              }, this.callback);
              helpers.stdin.writeNextTick('fn456\n');
            },
            "should modify user's input": function(e, result) {
              assert.equal(result.fnbefore, 'vfn456');
            }
          }
        }
      }
    }
  }
}).addBatch({
  "When using prompt": {
    "the get() method": {
      "skip prompt with prompt.overide": {
        topic: function () {
          prompt.override = { coconihet: 'whatever' }
          prompt.get('coconihet', this.callback);
        },
        "skips prompt and uses overide": function (err, results) {
          assert.equal(results.coconihet, 'whatever')
        }
      }
    }
  }
}).addBatch({
  "When using prompt": {
    "the addProperties() method": {
      topic: function () {
        prompt.addProperties({}, ['foo', 'bar'], this.callback);
        helpers.stdin.writeSequence(['foo\n', 'bar\n']);
      },
      "should add the properties to the object": function (err, obj) {
        assert.isNull(err);
        assert.isObject(obj);
        assert.equal(obj.foo, 'foo');
        assert.equal(obj.bar, 'bar');
      }
    }
  }
}).addBatch({
  "When using prompt": {
    "the get() method": {
      "with old schema": {
        topic: function () {
          var that = this;

          helpers.stdout.once('data', function (msg) {
            that.msg = msg;
          });

          prompt.properties.username = schema.properties.oldschema;
          prompt.get('username', this.callback);

          helpers.stdin.writeSequence(['\n', 'hell$\n', 'hello\n']);
        },
        "should prompt to stdout and respond with the default value": function (err, result) {
          assert.isNull(err);
          assert.isTrue(this.msg.indexOf('username') !== -1);
          assert.include(result, 'username');
          assert.equal(result.username, 'hello');
        }
      }
    }
  }
}).addBatch({
  "When using prompt": {
    "the history() method": {
      "when used inside of a complex property": {
        "with correct value(s)": {
          topic: function () {
            prompt.get([grab('animal'), grab('sound')], this.callback);
            helpers.stdin.writeSequence(['dog\n', 'woof\n']);
          },
          "should respond with the values entered": function (err, result) {
            assert.isTrue(!err);
            assert.equal(result.animal, 'dog');
            assert.equal(result.sound, 'woof');
            assert.equal(prompt.history('nothing'), null);
            assert.deepEqual(prompt.history('animal'), { property: 'animal', value: 'dog' });
          }
        },
      }
    }
  }
}).addBatch({
  "When using prompt": {
    "the history() method": {
      "when used inside of a complex property": {
        "with an incorrect value": {
          topic: function () {
            prompt.get([grab('animal'), grab('sound')], function () {});
            prompt.once('invalid', this.callback.bind(null, null));
            helpers.stdin.writeSequence(['dog\n', 'meow\n']);
          },
          "should prompt for the error": function (ign, property, line) {
            assert.equal(property.path.join(''), 'sound');
            assert.equal(line, 'meow');
          }
        }
      }
    }
  }
}).addBatch({
  "when using prompt": {
    "the get() method": {
      topic: function () {
        prompt.override = { xyz: 468, abc: 123 }
        prompt.get(['xyz', 'abc'], this.callback);
      },
      "should respond with overrides": function (err, results) {
        assert.isNull(err);
        assert.deepEqual(results, { xyz: 468, abc: 123 });
      }
    }
  }
}).addBatch({
  "When using prompt": {
    "with fancy properties": {
      "the get() method": {
        topic: function () {
          prompt.override = { UVW: 5423, DEF: 64235 }
          prompt.get({
            properties: {
              'UVW': {
                description: 'a custom message',
                default: 6
              },
              'DEF': {
                description: 'a custom message',
                default: 6
              }
            }
          }, this.callback);
        },
        "should respond with overrides": function (err, results) {
          assert.isNull(err);
          assert.deepEqual(results, { UVW: 5423, DEF: 64235 });
        }
      }
    }
  }
}).addBatch({
  "When using prompt": {
    "with a type and a description property": {
        "the get() method": {
          topic: function () {
            var that = this;
            helpers.stdout.once('data', function (msg) {
              that.msg = msg;
            });

            prompt.get({
              name: 'test',
              type: 'number',
              description: 'Please input a number'
            }, this.callback);
            helpers.stdin.writeNextTick('42\n');
          },
          "should prompt to stdout and respond with the value": function (err, result) {
            assert.isNull(err);
            assert.include(result, 'test');
            assert.equal(result['test'], '42');
            assert.isTrue(this.msg.indexOf('Please input a number') !== -1);
          }
        },
      }
    }
  })
.addBatch(
  macros.shouldConfirm({
    messages: ['with a string message'],
    prop: 'test',
    response: 'Y'
  })
).addBatch(
  macros.shouldConfirm({
    messages: ['with an empty string and default yes'],
    prop: 'test',
    response: ''
  }, {
    default: 'yes'
  })
).addBatch(
  macros.shouldNotConfirm({
    messages: ['with a string message'],
    prop: 'test',
    response: 'N'
  })
).addBatch(
  macros.shouldConfirm({
    messages: ['with a string message'],
    prop: 'test',
    response: 'YES'
  })
).addBatch(
  macros.shouldNotConfirm({
    messages: ['with a string message'],
    prop: 'test',
    response: 'NO'
  })
).addBatch(
  macros.shouldConfirm({
    messages: ['with a string message'],
    prop: 'test',
    response: 'T'
  })
).addBatch(
  macros.shouldNotConfirm({
    messages: ['with a string message'],
    prop: 'test',
    response: 'F'
  })
).addBatch(
  macros.shouldConfirm({
    messages: ['with a string message'],
    prop: 'test',
    response: 'TRUE'
  })
).addBatch(
  macros.shouldNotConfirm({
    messages: ['with a string message'],
    prop: 'test',
    response: 'FALSE'
  })
).addBatch(
  macros.shouldConfirm({
    messages: ['with an object', 'and description set'],
    prop: { description: 'a custom message' },
    response: 'Y'
  })
).addBatch(
  macros.shouldConfirm({
    messages: ['with an object', 'and they forgot the description'],
    prop: { description: 'a custom message' },
    response: 'Y'
  })
).addBatch(
  macros.shouldConfirm({
    messages: ['with an object', 'and custom validators'],
    prop: {
      description: 'node or jitsu?',
      pattern: /^(node|jitsu)/i,
      yes: /^node/i
    },
    response: 'node'
  })
).addBatch(
  macros.shouldNotConfirm({
    messages: ['with an object', 'and custom validators'],
    prop: {
      description: 'node or jitsu?',
      pattern: /^(node|jitsu)/i,
      yes: /^node/i
    },
    response: 'jitsu'
  })
).addBatch(
  macros.shouldConfirm({
    messages: ['with multiple strings'],
    prop: ["test", "test2", "test3"],
    response: ['Y\n', 'y\n', 'YES\n']
  })
).addBatch(
  macros.shouldNotConfirm({
    messages: ['with multiple strings'],
    prop: ["test", "test2", "test3"],
    response: ['Y\n', 'N\n', 'YES\n']
  })
).addBatch(
  macros.shouldNotConfirm({
    messages: ['with multiple strings'],
    prop: ["test", "test2", "test3"],
    response: ['n\n', 'NO\n', 'N\n']
  })
).addBatch(
  macros.shouldConfirm({
    messages: ['with multiple objects'],
    prop: [
      { message: "test" },
      { message: "test2" }
    ],
    response: ['y\n', 'y\n']
  })
).addBatch(
  macros.shouldNotConfirm({
    messages: ['with multiple objects'],
    prop: [
      { message: "test" },
      { message: "test2" }
    ],
    response: ['n\n', 'n\n']
  })
).addBatch(
  macros.shouldNotConfirm({
    messages: ['with multiple objects'],
    prop: [
      { message: "test" },
      { message: "test2" }
    ],
    response: ['n\n', 'y\n']
  })
).export(module);
