var common = require('../common');
var MultipartParserStub = GENTLY.stub('./multipart_parser', 'MultipartParser'),
    QuerystringParserStub = GENTLY.stub('./querystring_parser', 'QuerystringParser'),
    EventEmitterStub = GENTLY.stub('events', 'EventEmitter'),
    StreamStub = GENTLY.stub('stream', 'Stream'),
    FileStub = GENTLY.stub('./file');

var formidable = require(common.lib + '/index'),
    IncomingForm = formidable.IncomingForm,
    events = require('events'),
    fs = require('fs'),
    path = require('path'),
    Buffer = require('buffer').Buffer,
    fixtures = require(TEST_FIXTURES + '/multipart'),
    form,
    gently;

function test(test) {
  gently = new Gently();
  gently.expect(EventEmitterStub, 'call');
  form = new IncomingForm();
  test();
  gently.verify(test.name);
}

test(function constructor() {
  assert.strictEqual(form.error, null);
  assert.strictEqual(form.ended, false);
  assert.strictEqual(form.type, null);
  assert.strictEqual(form.headers, null);
  assert.strictEqual(form.keepExtensions, false);
  // Can't assume dir === '/tmp' for portability
  // assert.strictEqual(form.uploadDir, '/tmp');
  // Make sure it is a directory instead
  assert.doesNotThrow(function () {
    assert(fs.statSync(form.uploadDir).isDirectory());
  });
  assert.strictEqual(form.encoding, 'utf-8');
  assert.strictEqual(form.bytesReceived, null);
  assert.strictEqual(form.bytesExpected, null);
  assert.strictEqual(form.maxFieldsSize, 2 * 1024 * 1024);
  assert.strictEqual(form._parser, null);
  assert.strictEqual(form._flushing, 0);
  assert.strictEqual(form._fieldsSize, 0);
  assert.ok(form instanceof EventEmitterStub);
  assert.equal(form.constructor.name, 'IncomingForm');

  (function testSimpleConstructor() {
    gently.expect(EventEmitterStub, 'call');
    var form = IncomingForm();
    assert.ok(form instanceof IncomingForm);
  })();

  (function testSimpleConstructorShortcut() {
    gently.expect(EventEmitterStub, 'call');
    var form = formidable();
    assert.ok(form instanceof IncomingForm);
  })();
});

test(function parse() {
  var REQ = {headers: {}}
    , emit = {};

  gently.expect(form, 'writeHeaders', function(headers) {
    assert.strictEqual(headers, REQ.headers);
  });

  var EVENTS = ['error', 'aborted', 'data', 'end'];
  gently.expect(REQ, 'on', EVENTS.length, function(event, fn) {
    assert.equal(event, EVENTS.shift());
    emit[event] = fn;
    return this;
  });

  form.parse(REQ);

  (function testPause() {
    gently.expect(REQ, 'pause');
    assert.strictEqual(form.pause(), true);
  })();

  (function testPauseCriticalException() {
    form.ended = false;

    var ERR = new Error('dasdsa');
    gently.expect(REQ, 'pause', function() {
      throw ERR;
    });

    gently.expect(form, '_error', function(err) {
      assert.strictEqual(err, ERR);
    });

    assert.strictEqual(form.pause(), false);
  })();

  (function testPauseHarmlessException() {
    form.ended = true;

    var ERR = new Error('dasdsa');
    gently.expect(REQ, 'pause', function() {
      throw ERR;
    });

    assert.strictEqual(form.pause(), false);
  })();

  (function testResume() {
    gently.expect(REQ, 'resume');
    assert.strictEqual(form.resume(), true);
  })();

  (function testResumeCriticalException() {
    form.ended = false;

    var ERR = new Error('dasdsa');
    gently.expect(REQ, 'resume', function() {
      throw ERR;
    });

    gently.expect(form, '_error', function(err) {
      assert.strictEqual(err, ERR);
    });

    assert.strictEqual(form.resume(), false);
  })();

  (function testResumeHarmlessException() {
    form.ended = true;

    var ERR = new Error('dasdsa');
    gently.expect(REQ, 'resume', function() {
      throw ERR;
    });

    assert.strictEqual(form.resume(), false);
  })();

  (function testEmitError() {
    var ERR = new Error('something bad happened');
    gently.expect(form, '_error',function(err) {
      assert.strictEqual(err, ERR);
    });
    emit.error(ERR);
  })();

  (function testEmitAborted() {
    gently.expect(form, 'emit',function(event) {
      assert.equal(event, 'aborted');
    });
    gently.expect(form, '_error');

    emit.aborted();
  })();


  (function testEmitData() {
    var BUFFER = [1, 2, 3];
    gently.expect(form, 'write', function(buffer) {
      assert.strictEqual(buffer, BUFFER);
    });
    emit.data(BUFFER);
  })();

  (function testEmitEnd() {
    form._parser = {};

    (function testWithError() {
      var ERR = new Error('haha');
      gently.expect(form._parser, 'end', function() {
        return ERR;
      });

      gently.expect(form, '_error', function(err) {
        assert.strictEqual(err, ERR);
      });

      emit.end();
    })();

    (function testWithoutError() {
      gently.expect(form._parser, 'end');
      emit.end();
    })();

    (function testAfterError() {
      form.error = true;
      emit.end();
    })();
  })();

  (function testWithCallback() {
    gently.expect(EventEmitterStub, 'call');
    var form = new IncomingForm(),
        REQ = {headers: {}},
        parseCalled = 0;

    gently.expect(form, 'on', 4, function(event, fn) {
      if (event == 'field') {
        fn('field1', 'foo');
        fn('field1', 'bar');
        fn('field2', 'nice');
      }

      if (event == 'file') {
        fn('file1', '1');
        fn('file1', '2');
        fn('file2', '3');
      }

      if (event == 'end') {
        fn();
      }
      return this;
    });

    gently.expect(form, 'writeHeaders');

    gently.expect(REQ, 'on', 4, function() {
      return this;
    });

    var parseCbOk = function (err, fields, files) {
      assert.deepEqual(fields, {field1: 'bar', field2: 'nice'});
      assert.deepEqual(files, {file1: '2', file2: '3'});
    };
    form.parse(REQ, parseCbOk);

    var ERR = new Error('test');
    gently.expect(form, 'on', 3, function(event, fn) {
      if (event == 'field') {
        fn('foo', 'bar');
      }

      if (event == 'error') {
        fn(ERR);
        gently.expect(form, 'on');
        gently.expect(form, 'writeHeaders');
        gently.expect(REQ, 'on', 4, function() {
          return this;
        });
      }
      return this;
    });

    form.parse(REQ, function parseCbErr(err, fields, files) {
      assert.strictEqual(err, ERR);
      assert.deepEqual(fields, {foo: 'bar'});
    });
  })();

  (function testWriteOrder() {
    gently.expect(EventEmitterStub, 'call');
    var form    = new IncomingForm();
    var REQ     = new events.EventEmitter();
    var BUF     = {};
    var DATACB  = null;

    REQ.on('newListener', function(event, fn) {
      if ('data' === event) fn(BUF);
    });

    gently.expect(form, 'writeHeaders');
    gently.expect(form, 'write', function(buf) {
      assert.strictEqual(buf, BUF);
    });

    form.parse(REQ);
  })();
});

test(function pause() {
  assert.strictEqual(form.pause(), false);
});

test(function resume() {
  assert.strictEqual(form.resume(), false);
});


test(function writeHeaders() {
  var HEADERS = {};
  gently.expect(form, '_parseContentLength');
  gently.expect(form, '_parseContentType');

  form.writeHeaders(HEADERS);
  assert.strictEqual(form.headers, HEADERS);
});

test(function write() {
  var parser = {},
      BUFFER = [1, 2, 3];

  form._parser = parser;
  form.bytesExpected = 523423;

  (function testBasic() {
    gently.expect(form, 'emit', function(event, bytesReceived, bytesExpected) {
      assert.equal(event, 'progress');
      assert.equal(bytesReceived, BUFFER.length);
      assert.equal(bytesExpected, form.bytesExpected);
    });

    gently.expect(parser, 'write', function(buffer) {
      assert.strictEqual(buffer, BUFFER);
      return buffer.length;
    });

    assert.equal(form.write(BUFFER), BUFFER.length);
    assert.equal(form.bytesReceived, BUFFER.length);
  })();

  (function testParserError() {
    gently.expect(form, 'emit');

    gently.expect(parser, 'write', function(buffer) {
      assert.strictEqual(buffer, BUFFER);
      return buffer.length - 1;
    });

    gently.expect(form, '_error', function(err) {
      assert.ok(err.message.match(/parser error/i));
    });

    assert.equal(form.write(BUFFER), BUFFER.length - 1);
    assert.equal(form.bytesReceived, BUFFER.length + BUFFER.length);
  })();

  (function testUninitialized() {
    delete form._parser;

    gently.expect(form, '_error', function(err) {
      assert.ok(err.message.match(/unintialized parser/i));
    });
    form.write(BUFFER);
  })();
});

test(function parseContentType() {
  var HEADERS = {};

  form.headers = {'content-type': 'application/x-www-form-urlencoded'};
  gently.expect(form, '_initUrlencoded');
  form._parseContentType();

  // accept anything that has 'urlencoded' in it
  form.headers = {'content-type': 'broken-client/urlencoded-stupid'};
  gently.expect(form, '_initUrlencoded');
  form._parseContentType();

  var BOUNDARY = '---------------------------57814261102167618332366269';
  form.headers = {'content-type': 'multipart/form-data; boundary='+BOUNDARY};

  gently.expect(form, '_initMultipart', function(boundary) {
    assert.equal(boundary, BOUNDARY);
  });
  form._parseContentType();

  (function testQuotedBoundary() {
    form.headers = {'content-type': 'multipart/form-data; boundary="' + BOUNDARY + '"'};

    gently.expect(form, '_initMultipart', function(boundary) {
      assert.equal(boundary, BOUNDARY);
    });
    form._parseContentType();
  })();

  (function testNoBoundary() {
    form.headers = {'content-type': 'multipart/form-data'};

    gently.expect(form, '_error', function(err) {
      assert.ok(err.message.match(/no multipart boundary/i));
    });
    form._parseContentType();
  })();

  (function testNoContentType() {
    form.headers = {};

    gently.expect(form, '_error', function(err) {
      assert.ok(err.message.match(/no content-type/i));
    });
    form._parseContentType();
  })();

  (function testUnknownContentType() {
    form.headers = {'content-type': 'invalid'};

    gently.expect(form, '_error', function(err) {
      assert.ok(err.message.match(/unknown content-type/i));
    });
    form._parseContentType();
  })();
});

test(function parseContentLength() {
  var HEADERS = {};

  form.headers = {};
  gently.expect(form, 'emit', function(event, bytesReceived, bytesExpected) {
    assert.equal(event, 'progress');
    assert.equal(bytesReceived, 0);
    assert.equal(bytesExpected, 0);
  });
  form._parseContentLength();

  form.headers['content-length'] = '8';
  gently.expect(form, 'emit', function(event, bytesReceived, bytesExpected) {
    assert.equal(event, 'progress');
    assert.equal(bytesReceived, 0);
    assert.equal(bytesExpected, 8);
  });
  form._parseContentLength();
  assert.strictEqual(form.bytesReceived, 0);
  assert.strictEqual(form.bytesExpected, 8);

  // JS can be evil, lets make sure we are not
  form.headers['content-length'] = '08';
  gently.expect(form, 'emit', function(event, bytesReceived, bytesExpected) {
    assert.equal(event, 'progress');
    assert.equal(bytesReceived, 0);
    assert.equal(bytesExpected, 8);
  });
  form._parseContentLength();
  assert.strictEqual(form.bytesExpected, 8);
});

test(function _initMultipart() {
  var BOUNDARY = '123',
      PARSER;

  gently.expect(MultipartParserStub, 'new', function() {
    PARSER = this;
  });

  gently.expect(MultipartParserStub.prototype, 'initWithBoundary', function(boundary) {
    assert.equal(boundary, BOUNDARY);
  });

  form._initMultipart(BOUNDARY);
  assert.equal(form.type, 'multipart');
  assert.strictEqual(form._parser, PARSER);

  (function testRegularField() {
    var PART;
    gently.expect(StreamStub, 'new', function() {
      PART = this;
    });

    gently.expect(form, 'onPart', function(part) {
      assert.strictEqual(part, PART);
      assert.deepEqual
        ( part.headers
        , { 'content-disposition': 'form-data; name="field1"'
          , 'foo': 'bar'
          }
        );
      assert.equal(part.name, 'field1');

      var strings = ['hello', ' world'];
      gently.expect(part, 'emit', 2, function(event, b) {
          assert.equal(event, 'data');
          assert.equal(b.toString(), strings.shift());
      });

      gently.expect(part, 'emit', function(event, b) {
          assert.equal(event, 'end');
      });
    });

    PARSER.onPartBegin();
    PARSER.onHeaderField(new Buffer('content-disposition'), 0, 10);
    PARSER.onHeaderField(new Buffer('content-disposition'), 10, 19);
    PARSER.onHeaderValue(new Buffer('form-data; name="field1"'), 0, 14);
    PARSER.onHeaderValue(new Buffer('form-data; name="field1"'), 14, 24);
    PARSER.onHeaderEnd();
    PARSER.onHeaderField(new Buffer('foo'), 0, 3);
    PARSER.onHeaderValue(new Buffer('bar'), 0, 3);
    PARSER.onHeaderEnd();
    PARSER.onHeadersEnd();
    PARSER.onPartData(new Buffer('hello world'), 0, 5);
    PARSER.onPartData(new Buffer('hello world'), 5, 11);
    PARSER.onPartEnd();
  })();

  (function testFileField() {
    var PART;
    gently.expect(StreamStub, 'new', function() {
      PART = this;
    });

    gently.expect(form, 'onPart', function(part) {
      assert.deepEqual
        ( part.headers
        , { 'content-disposition': 'form-data; name="field2"; filename="C:\\Documents and Settings\\IE\\Must\\Die\\Sun"et.jpg"'
          , 'content-type': 'text/plain'
          }
        );
      assert.equal(part.name, 'field2');
      assert.equal(part.filename, 'Sun"et.jpg');
      assert.equal(part.mime, 'text/plain');

      gently.expect(part, 'emit', function(event, b) {
        assert.equal(event, 'data');
        assert.equal(b.toString(), '... contents of file1.txt ...');
      });

      gently.expect(part, 'emit', function(event, b) {
          assert.equal(event, 'end');
      });
    });

    PARSER.onPartBegin();
    PARSER.onHeaderField(new Buffer('content-disposition'), 0, 19);
    PARSER.onHeaderValue(new Buffer('form-data; name="field2"; filename="C:\\Documents and Settings\\IE\\Must\\Die\\Sun"et.jpg"'), 0, 85);
    PARSER.onHeaderEnd();
    PARSER.onHeaderField(new Buffer('Content-Type'), 0, 12);
    PARSER.onHeaderValue(new Buffer('text/plain'), 0, 10);
    PARSER.onHeaderEnd();
    PARSER.onHeadersEnd();
    PARSER.onPartData(new Buffer('... contents of file1.txt ...'), 0, 29);
    PARSER.onPartEnd();
  })();

  (function testEnd() {
    gently.expect(form, '_maybeEnd');
    PARSER.onEnd();
    assert.ok(form.ended);
  })();
});

test(function _fileName() {
  // TODO
  return;
});

test(function _initUrlencoded() {
  var PARSER;

  gently.expect(QuerystringParserStub, 'new', function() {
    PARSER = this;
  });

  form._initUrlencoded();
  assert.equal(form.type, 'urlencoded');
  assert.strictEqual(form._parser, PARSER);

  (function testOnField() {
    var KEY = 'KEY', VAL = 'VAL';
    gently.expect(form, 'emit', function(field, key, val) {
      assert.equal(field, 'field');
      assert.equal(key, KEY);
      assert.equal(val, VAL);
    });

    PARSER.onField(KEY, VAL);
  })();

  (function testOnEnd() {
    gently.expect(form, '_maybeEnd');

    PARSER.onEnd();
    assert.equal(form.ended, true);
  })();
});

test(function _error() {
  var ERR = new Error('bla');

  gently.expect(form, 'pause');
  gently.expect(form, 'emit', function(event, err) {
    assert.equal(event, 'error');
    assert.strictEqual(err, ERR);
  });

  form._error(ERR);
  assert.strictEqual(form.error, ERR);

  // make sure _error only does its thing once
  form._error(ERR);
});

test(function onPart() {
  var PART = {};
  gently.expect(form, 'handlePart', function(part) {
    assert.strictEqual(part, PART);
  });

  form.onPart(PART);
});

test(function handlePart() {
  (function testUtf8Field() {
    var PART = new events.EventEmitter();
    PART.name = 'my_field';

    gently.expect(form, 'emit', function(event, field, value) {
      assert.equal(event, 'field');
      assert.equal(field, 'my_field');
      assert.equal(value, 'hello world: €');
    });

    form.handlePart(PART);
    PART.emit('data', new Buffer('hello'));
    PART.emit('data', new Buffer(' world: '));
    PART.emit('data', new Buffer([0xE2]));
    PART.emit('data', new Buffer([0x82, 0xAC]));
    PART.emit('end');
  })();

  (function testBinaryField() {
    var PART = new events.EventEmitter();
    PART.name = 'my_field2';

    gently.expect(form, 'emit', function(event, field, value) {
      assert.equal(event, 'field');
      assert.equal(field, 'my_field2');
      assert.equal(value, 'hello world: '+new Buffer([0xE2, 0x82, 0xAC]).toString('binary'));
    });

    form.encoding = 'binary';
    form.handlePart(PART);
    PART.emit('data', new Buffer('hello'));
    PART.emit('data', new Buffer(' world: '));
    PART.emit('data', new Buffer([0xE2]));
    PART.emit('data', new Buffer([0x82, 0xAC]));
    PART.emit('end');
  })();

  (function testFieldSize() {
    form.maxFieldsSize = 8;
    var PART = new events.EventEmitter();
    PART.name = 'my_field';

    gently.expect(form, '_error', function(err) {
      assert.equal(err.message, 'maxFieldsSize exceeded, received 9 bytes of field data');
    });

    form.handlePart(PART);
    form._fieldsSize = 1;
    PART.emit('data', new Buffer(7));
    PART.emit('data', new Buffer(1));
  })();

  (function testFilePart() {
    var PART = new events.EventEmitter(),
        FILE = new events.EventEmitter(),
        PATH = '/foo/bar';

    PART.name = 'my_file';
    PART.filename = 'sweet.txt';
    PART.mime = 'sweet.txt';

    gently.expect(form, '_uploadPath', function(filename) {
      assert.equal(filename, PART.filename);
      return PATH;
    });

    gently.expect(FileStub, 'new', function(properties) {
      assert.equal(properties.path, PATH);
      assert.equal(properties.name, PART.filename);
      assert.equal(properties.type, PART.mime);
      FILE = this;

      gently.expect(form, 'emit', function (event, field, file) {
        assert.equal(event, 'fileBegin');
        assert.strictEqual(field, PART.name);
        assert.strictEqual(file, FILE);
      });

      gently.expect(FILE, 'open');
    });

    form.handlePart(PART);
    assert.equal(form._flushing, 1);

    var BUFFER;
    gently.expect(form, 'pause');
    gently.expect(FILE, 'write', function(buffer, cb) {
      assert.strictEqual(buffer, BUFFER);
      gently.expect(form, 'resume');
      // @todo handle cb(new Err)
      cb();
    });

    PART.emit('data', BUFFER = new Buffer('test'));

    gently.expect(FILE, 'end', function(cb) {
      gently.expect(form, 'emit', function(event, field, file) {
        assert.equal(event, 'file');
        assert.strictEqual(file, FILE);
      });

      gently.expect(form, '_maybeEnd');

      cb();
      assert.equal(form._flushing, 0);
    });

    PART.emit('end');
  })();
});

test(function _uploadPath() {
  (function testUniqueId() {
    var UUID_A, UUID_B;
    gently.expect(GENTLY.hijacked.path, 'join', function(uploadDir, uuid) {
      assert.equal(uploadDir, form.uploadDir);
      UUID_A = uuid;
    });
    form._uploadPath();

    gently.expect(GENTLY.hijacked.path, 'join', function(uploadDir, uuid) {
      UUID_B = uuid;
    });
    form._uploadPath();

    assert.notEqual(UUID_A, UUID_B);
  })();

  (function testFileExtension() {
    form.keepExtensions = true;
    var FILENAME = 'foo.jpg',
        EXT = '.bar';

    gently.expect(GENTLY.hijacked.path, 'extname', function(filename) {
      assert.equal(filename, FILENAME);
      gently.restore(path, 'extname');

      return EXT;
    });

    gently.expect(GENTLY.hijacked.path, 'join', function(uploadDir, name) {
      assert.equal(path.extname(name), EXT);
    });
    form._uploadPath(FILENAME);
  })();
});

test(function _maybeEnd() {
  gently.expect(form, 'emit', 0);
  form._maybeEnd();

  form.ended = true;
  form._flushing = 1;
  form._maybeEnd();

  gently.expect(form, 'emit', function(event) {
    assert.equal(event, 'end');
  });

  form.ended = true;
  form._flushing = 0;
  form._maybeEnd();
});
