text: var stream = require('stream');
  var test = require('tape');
  var _ = require('underscore');
  var fs = require('fs');
  var path = require('path');
  var assert = require('assert');
  var upload = require('mapbox-upload');
  var tm = require('../lib/tm');
  var source = require('../lib/source');
  var tilelive = require('tilelive');
  var testutil = require('./util');
  var mockOauth = require('../lib/mapbox-mock')(require('express')());
  var creds = {
      account: 'test',
      accesstoken: 'testaccesstoken'
  };
  var tmp = tm.join(require('os').tmpdir(),'mapbox-studio');
  var UPDATE = !!process.env.UPDATE;
  var server;
  
  var defaultsource = 'tmsource://' + tm.join(path.dirname(require.resolve('mapbox-studio-default-source')));
  var localsource = 'tmsource://' + path.join(__dirname,'fixtures-localsource');
  var tmppath = tm.join(tmp, 'Source ШЖФ - ' + +new Date);
  
  test('setup: config', function(t) {
      tm.config({
          log: false,
          db: path.join(tmppath, 'app.db'),
          tmp: path.join(tmppath, 'tmp'),
          fonts: path.join(tmppath, 'fonts'),
          cache: path.join(tmppath, 'cache')
      }, t.end);
  });
  
  test('setup: mockserver', function(t) {
      tm.db.set('oauth', creds);
      tm.db.set('MapboxAPIAuth', 'http://localhost:3001');
      tm.db.set('MapboxAPITile', 'http://localhost:3001');
      server = mockOauth.listen(3001, t.end);
  });
  
  test('source.normalize', function(t) {
      var n = source.normalize({
          id: 'tmsource://' + __dirname + '/fixtures-localsource',
          Layer: [{
              id: 'box',
              fields: {
                  Id: 'Valid helptext for a field',
                  missing: 'Invalid helptext no field named missing'
              },
              Datasource: {
                  type: 'shape',
                  file: __dirname + '/fixtures-localsource/10m-900913-bounding-box.shp',
                  bogus: 'true'
              }
          }]
      });
      t.deepEqual(n.Layer.length, 1);
      t.deepEqual(n.vector_layers.length, 1);
      t.deepEqual(n.vector_layers[0].fields, {'Id':'Valid helptext for a field'},
          'Populates field help');
      t.deepEqual(Object.keys(tm.sortkeys(n.Layer[0])), ['id','Datasource','description','fields','properties','srs'],
          'Populates deep defaults in Layer objects');
      t.deepEqual(Object.keys(tm.sortkeys(n.Layer[0].Datasource)), ['file','type'],
          'Strips invalid datasource properties based on type');
  
      // Throws for bad datasource type.
      t.throws(function() {
          source.normalize({ Layer: [{ Datasource: { type: 'xboxlive' } }] });
      }, /Invalid datasource type/);
  
      // Throws if datasource is missing required fields.
      t.throws(function() {
          source.normalize({ Layer: [{ Datasource: { type: 'shape' } }] });
      }, /Missing required field/);
  
  
      // Test a raster source with multiple layers
      n = source.normalize({
          id: 'tmsource://' + __dirname + '/fixtures-localraster',
          Layer: [{
              id: 'raster2',
              fields: {},
              Datasource: {
                  type: 'gdal',
                  file: __dirname + '/fixtures-localraster/raster2.tif'
              }
          }, {
              id: 'raster1',
              fields: {},
              Datasource: {
                  type: 'gdal',
                  file: __dirname + '/fixtures-localraster/raster1.tif'
              }
          }]
      });
      t.deepEqual(n.Layer.length, 2, 'raster source contains two layers');
      t.deepEqual(n.vector_layers, undefined, 'raster source contains no vector_layers');
  
      // Test normalizing metadata for a remote vector source.
      n = source.normalize({
          id: 'mapbox:///mapbox.remote-vector',
          format: 'pbf',
          vector_layers: [{
              id: 'water',
          }, {
              id: 'landuse'
          }]
      });
      t.deepEqual(n.format, 'pbf', 'remote vector source is pbf');
      t.deepEqual(n.vector_layers.length, 2, 'remote vector source preserves vector_layers key');
  
      // Test normalizing metadata for a remote raster source.
      n = source.normalize({
          id: 'mapbox:///mapbox.remote-raster',
          format: 'webp'
      });
      t.deepEqual(n.format, 'webp', 'remote raster source is webp');
      t.deepEqual(n.vector_layers, undefined, 'remote raster source contains no vector_layers');
  
  
      // @TODO check postgis auto srs extent generation ... without postgis.
  
      t.end();
  });
  
  test('remote: fails without oauth', function(t) {
      tm.db.set('oauth', null);
      source('mapbox:///test.oauth-fail-source', function(err, source) {
          t.ok(err);
          t.equal('EOAUTH', err.code);
          tm.db.set('oauth', creds);
          t.end();
      });
  });
  
  test('remote: loads', function(t) {
      source('mapbox:///mapbox.mapbox-streets-v2', function(err, source) {
          t.ifError(err);
          t.equal('Mapbox Streets V2', source.data.name);
          t.equal(0, source.data.minzoom);
          t.equal(14, source.data.maxzoom);
          t.ok(!!source.style);
          t.end();
      });
  });
  
  test('remote: loads raster', function(t) {
      source('mapbox:///mapbox.satellite', function(err, source) {
          t.ifError(err);
          t.equal('Mapbox Satellite', source.data.name);
          t.equal(0, source.data.minzoom);
          t.equal(19, source.data.maxzoom);
          t.equal(source.data.vector_layers, undefined);
          t.ok(!!source.style);
          t.end();
      });
  });
  
  test('remote: loads via tilelive', function(t) {
      tilelive.load('mapbox:///mapbox.mapbox-streets-v2', function(err, source) {
          t.ifError(err);
          t.equal('Mapbox Streets V2', source.data.name);
          t.equal(0, source.data.minzoom);
          t.equal(14, source.data.maxzoom);
          t.ok(!!source.style);
          t.end();
      });
  });
  
  test('remote: loads via http', function(t) {
      source('http://localhost:3001/v4/mapbox.mapbox-streets-v4.json?access_token=testaccesstoken', function (err, source) {
          t.ifError(err);
          t.equal('Mapbox Streets V4', source.data.name);
          t.equal(0, source.data.minzoom);
          t.equal(14, source.data.maxzoom);
          t.ok(!!source.style);
          t.end();
      });
  });
  
  test.skip('remote: loads via https', function(t) {
      source('https://localhost:3001/v4/mapbox.mapbox-streets-v4.json', function (err, source) {
          t.ifError(err);
          t.equal('Mapbox Streets V4', source.data.name);
          t.equal(0, source.data.minzoom);
          t.equal(14, source.data.maxzoom);
          t.ok(!!source.style);
          t.end();
      });
  });
  
  test('remote: error bad protocol', function(t) {
      source('invalid://www.google.com', function(err, source) {
          t.ok(err);
          t.equal('Invalid source protocol', err.message);
          t.end();
      });
  });
  
  test('remote: noop refresh', function(t) {
      source.refresh({id:'mapbox:///mapbox.mapbox-streets-v2'}, function(err, source) {
          t.ifError(err);
          t.end();
      });
  });
  
  test('remote: save error', function(t) {
      source.save({id:'mapbox:///mapbox.mapbox-streets-v2'}, function(err, source) {
          t.ok(err);
          t.ok(/^Error: Cannot save remote source/.test(err.toString()));
          t.end();
      });
  });
  
  test('local: invalid yaml (non-object)', function(t) {
      source('tmsource://' + __dirname + '/fixtures-invalid-nonobj', function(err, source) {
          t.ok(err);
          t.ok(/^Error: Invalid YAML/.test(err.toString()));
          t.end();
      });
  });
  
  test('local: invalid yaml', function(t) {
      source('tmsource://' + __dirname + '/fixtures-invalid-yaml', function(err, source) {
          t.ok(err);
          t.ok(/^JS-YAML/.test(err.toString()));
          t.end();
      });
  });
  
  test('local: loads', function(t) {
      source('tmsource://' + __dirname + '/fixtures-localsource', function(err, source) {
          t.ifError(err);
          t.equal('Test source', source.data.name);
          t.equal(0, source.data.minzoom);
          t.equal(6, source.data.maxzoom);
          t.ok(!!source.style);
          t.end();
      });
  });
  
  test('local: loads via tilelive', function(t) {
      tilelive.load('tmsource://' + __dirname + '/fixtures-localsource', function(err, source) {
          t.ifError(err);
          t.equal('Test source', source.data.name);
          t.equal(0, source.data.minzoom);
          t.equal(6, source.data.maxzoom);
          t.ok(!!source.style);
          t.end();
      });
  });
  
  test('local: refresh source in memory', function(t) {
      testutil.createTmpProject('source-save', localsource, function(err, tmpid, info) {
          assert.ifError(err);
          source.refresh(_({id:source.tmpid()}).defaults(info), function(err, source) {
              t.ifError(err);
              t.ok(source);
              t.end();
          });
      });
  });
  
  test('local: refresh source (invalid minzoom)', function(t) {
      testutil.createTmpProject('source-save', defaultsource, function(err, tmpid, info) {
          assert.ifError(err);
          source.refresh(_({id:source.tmpid(), minzoom:-1}).defaults(info), function(err, source) {
              assert.equal(err.toString(), 'Error: minzoom must be an integer between 0 and 22', 'source.refresh() errors on invalid source');
              t.end();
          });
      });
  });
  
  test('local: refresh source (invalid center)', function(t) {
      testutil.createTmpProject('source-save', defaultsource, function(err, tmpid, info) {
          assert.ifError(err);
          source.refresh(_({id:source.tmpid(), minzoom:3, center:[0,0,0]}).defaults(info), function(err, source) {
              assert.equal(err.toString(), 'Error: center zoom value must be greater than or equal to minzoom 3', 'source.refresh() errors on invalid source');
              t.end();
          });
      });
  });
  
  test('local: save temporary errors', function(t) {
      testutil.createTmpProject('source-save', localsource, function(err, tmpid, info) {
          assert.ifError(err);
          source.save(_({id:source.tmpid()}).defaults(info), function(err, source) {
              t.ok(err);
              t.ok(/^Error: Cannot save temporary source/.test(err.toString()));
              t.end();
          });
      });
  });
  
  test('local: saves source to disk', function(t) {
      testutil.createTmpProject('source-save', localsource, function(err, tmpid, data) {
      assert.ifError(err);
  
      source.save(data, function(err, source) {
          t.ifError(err);
          t.ok(source);
  
          // Windows filepaths can lead to dramatically different yaml fixtures
          // than unix paths. This is not just because of backslashes but also
          // the c: drivename which leads to use of double quotes in yaml.
          // Normalize all this nonsense before following through with basepath
          // replacement for fixture comparison + creation.
          var yaml = require('js-yaml');
          var dirname = tm.join(__dirname);
          var ymldirname = yaml.dump(dirname).trim().replace(/"/g,'');
  
          var projectdir = tm.parse(tmpid).dirname;
          var datayml = fs.readFileSync(projectdir + '/data.yml', 'utf8').replace(new RegExp(ymldirname,'g'),'BASEPATH');
          var dataxml = fs.readFileSync(projectdir + '/data.xml', 'utf8').replace(new RegExp(dirname,'g'),'BASEPATH');
  
          if (UPDATE) {
              fs.writeFileSync(__dirname + '/expected/source-save-data.yml', datayml);
              fs.writeFileSync(__dirname + '/expected/source-save-data.xml', dataxml);
          }
  
          t.deepEqual(yaml.load(datayml), yaml.load(fs.readFileSync(__dirname + '/expected/source-save-data.yml', 'utf8')));
          t.equal(dataxml, fs.readFileSync(__dirname + '/expected/source-save-data.xml', 'utf8'));
  
          // This setTimeout is here because thumbnail generation on save
          // is an optimistic operation (e.g. callback does not wait for it
          // to complete).
          setTimeout(function() {
              t.ok(fs.existsSync(projectdir + '/.thumb.png'), 'saves thumb');
              t.end();
          }, 1000);
      });
  
      });
  });
  
  test('local: save as tmp => perm', function(t) {
      var tmpid = 'tmpsource://' + tm.join(__dirname, '/fixtures-localsource');
      source.info(tmpid, function(err, data) {
          t.ifError(err);
          var permid = path.join(tmp, 'source-saveas-' + Math.random().toString(36).split('.').pop());
          source.save(_({_tmp:tmpid, id:permid}).defaults(data), function(err, source) {
              t.ifError(err);
              t.ok(source);
              t.equal(source.data._tmp, false);
              var tmpdir = tm.parse(permid).dirname;
              t.ok(fs.existsSync(tm.join(tmpdir,'data.yml')));
              t.ok(fs.existsSync(tm.join(tmpdir,'data.xml')));
              t.end();
          });
      });
  });
  
  test('local: saves source with space', function(t) {
      // proxy assertion via createTmpProject stat check of project saves.
      testutil.createTmpProject('source-save space', localsource, function(err, tmpid, data) {
          assert.ifError(err);
          t.end();
      });
  });
  
  test('source.info: fails on bad path', function(t) {
      source.info('tmsource:///path/does/not/exist', function(err, info) {
          t.ok(err);
          t.equal('ENOENT', err.code);
          t.end();
      });
  });
  
  test('source.info: reads source YML', function(t) {
      var tmpid = 'tmsource://' + tm.join(__dirname, 'fixtures-localsource');
      source.info(tmpid, function(err, info) {
          t.ifError(err);
          t.equal(info.id, tmpid, 'source.info adds id key');
          t.equal(info._tmp, false, 'style info adds _tmp=false');
  
          var basepath = tm.parse(tmpid).dirname;
          info.id = info.id.replace(basepath, '[BASEPATH]');
  
          var filepath = __dirname + '/expected/source-info.json';
          if (UPDATE) {
              fs.writeFileSync(filepath, JSON.stringify(info, null, 2).replace(__dirname, '[basepath]'));
          }
          t.deepEqual(info, require(filepath));
          t.end();
      });
  });
  
  test('source.info: reads source YML (tmp)', function(t) {
      var tmpid = 'tmpsource://' + tm.join(__dirname, '/fixtures-localsource');
      source.info(tmpid, function(err, info) {
          t.ifError(err);
          t.equal(info.id, tmpid, 'source.info adds id key');
  
          var basepath = tm.parse(tmpid).dirname;
          info.id = info.id.replace(basepath, '[BASEPATH]');
          info._tmp = info._tmp.replace(basepath, '[BASEPATH]');
  
          var filepath = __dirname + '/expected/source-info-tmp.json';
          if (UPDATE) {
              fs.writeFileSync(filepath, JSON.stringify(info, null, 2).replace(__dirname, '[basepath]'));
          }
          t.deepEqual(info, require(filepath));
          t.end();
      });
  });
  
  test('source export: setup', function(t) {
      testutil.createTmpProject('source-export', localsource, function(err, tmpid) {
          assert.ifError(err);
          t.end();
      });
  });
  
  test('source.mbtilesExport: exports mbtiles file', function(t) {
      testutil.createTmpProject('source-export', localsource, function(err, id) {
      assert.ifError(err);
  
      source.toHash(id, function(err, hash) {
          t.ifError(err);
          t.equal(false, fs.existsSync(hash), 'export does not exist yet');
          var task = source.mbtilesExport(id);
          t.strictEqual(task.id, id, 'sets task.id');
          t.ok(task.progress instanceof stream.Duplex, 'sets task.progress');
          task.progress.once('finished', function() {
              t.equal(task.progress.progress().percentage, 100, 'progress.percentage');
              t.equal(task.progress.progress().transferred, 5462, 'progress.transferred');
              t.equal(task.progress.progress().eta, 0, 'progress.eta');
              t.equal(true, fs.existsSync(hash), 'export moved into place');
              t.end();
          });
      });
  
      });
  });
  
  test('source.mbtilesExport: verify export', function(t) {
      testutil.createTmpProject('source-export', localsource, function(err, id) {
      assert.ifError(err);
  
      var MBTiles = require('mbtiles');
      source.toHash(id, function(err, hash) {
          t.ifError(err);
          new MBTiles({ pathname: hash }, function(err, src) {
              t.ifError(err);
              src._db.get('select count(1) as count, sum(length(tile_data)) as size from tiles;', function(err, row) {
                  t.ifError(err);
                  t.equal(row.count, 5461);
                  t.equal(row.size, 378705);
                  check([
                      [0,0,0],
                      [1,0,0],
                      [1,1,0],
                      [2,0,1],
                      [2,2,1]
                  ]);
              });
              function check(queue) {
                  if (!queue.length) return src.getInfo(function(err, info) {
                      t.ifError(err);
  
                      // Omit id, basename, filesize from fixture check.
                      delete info.id;
                      delete info.basename;
                      delete info.filesize;
  
                      if (UPDATE) {
                          fs.writeFileSync(__dirname + '/expected/source-export-info.json', JSON.stringify(info, null, 2));
                      }
                      t.deepEqual(info, JSON.parse(fs.readFileSync(__dirname + '/expected/source-export-info.json')));
                      t.end();
                  });
                  var zxy = queue.shift();
                  src.getTile(zxy[0],zxy[1],zxy[2], function(err, buffer) {
                      t.ifError(err);
                      t.ok(!!buffer);
                      check(queue);
                  });
              }
          });
      });
  
      });
  });
  
  test('source.mbtilesUpload: uploads map', function(t) {
      testutil.createTmpProject('source-export', localsource, function(err, id) {
      assert.ifError(err);
  
      source.upload(id, false, function(err, task) {
          t.ifError(err);
          t.strictEqual(task.id, id, 'sets task.id');
          t.ok(task.progress instanceof stream.Duplex, 'sets task.progress');
          // returns a task object with active progress
          task.progress.on('error', function(err){
              t.ifError(err);
          });
          task.progress.on('finished', function(p){
              t.equal(task.progress.progress().percentage, 100, 'progress.percentage');
              t.equal(task.progress.progress().eta, 0, 'progress.eta');
          });
  
          task.progress.on('finished', function(){
              t.end()
          });
      });
  
      });
  });
  
  test('source.mbtilesUpload: does not allow redundant upload', function(t) {
      testutil.createTmpProject('source-export', localsource, function(err, id) {
      assert.ifError(err);
  
      source.upload(id, false, function(err, task) {
          t.ifError(err);
          t.equal(task.progress, null, 'progress obj not created');
  
          source.info(id, function(err, info){
              t.ifError(err);
              t.assert(/test\..{8}/.test(info._prefs.mapid), 'mapid correctly generated');
              // reset mapid to null
              info._prefs.mapid = null;
              source.save(info, function(){
                  t.end();
              })
          });
      });
  
      });
  });
  
  test('cleanup', function(t) {
      server.close(function() { t.end(); });
  });
  