text: /*global defineSuite*/
  defineSuite([
          'Scene/QuadtreePrimitive',
          'Core/Cartesian3',
          'Core/Cartographic',
          'Core/defineProperties',
          'Core/EventHelper',
          'Core/GeographicTilingScheme',
          'Core/Visibility',
          'Scene/QuadtreeTile',
          'Scene/QuadtreeTileLoadState',
          'Specs/createScene',
          'Specs/pollToPromise'
      ], function(
          QuadtreePrimitive,
          Cartesian3,
          Cartographic,
          defineProperties,
          EventHelper,
          GeographicTilingScheme,
          Visibility,
          QuadtreeTile,
          QuadtreeTileLoadState,
          createScene,
          pollToPromise) {
      'use strict';
  
      var scene;
  
      beforeAll(function() {
          scene = createScene();
          scene.render();
      });
  
      afterAll(function() {
          scene.destroyForSpecs();
      });
  
      it('must be constructed with a tileProvider', function() {
          expect(function() {
              return new QuadtreePrimitive();
          }).toThrowDeveloperError();
  
          expect(function() {
              return new QuadtreePrimitive({});
          }).toThrowDeveloperError();
      });
  
      function createSpyTileProvider() {
          var result = jasmine.createSpyObj('tileProvider', [
              'getQuadtree', 'setQuadtree', 'getReady', 'getTilingScheme', 'getErrorEvent',
              'initialize', 'beginUpdate', 'endUpdate', 'getLevelMaximumGeometricError', 'loadTile',
              'computeTileVisibility', 'showTileThisFrame', 'computeDistanceToTile', 'isDestroyed', 'destroy']);
  
          defineProperties(result, {
              quadtree : {
                  get : result.getQuadtree,
                  set : result.setQuadtree
              },
              ready : {
                  get : result.getReady
              },
              tilingScheme : {
                  get : result.getTilingScheme
              },
              errorEvent : {
                  get : result.getErrorEvent
              }
          });
  
          var tilingScheme = new GeographicTilingScheme();
          result.getTilingScheme.and.returnValue(tilingScheme);
  
          return result;
      }
  
      it('calls initialize, beginUpdate, loadTile, and endUpdate', function() {
          var tileProvider = createSpyTileProvider();
          tileProvider.getReady.and.returnValue(true);
  
          var quadtree = new QuadtreePrimitive({
              tileProvider : tileProvider
          });
  
          // determine what tiles to load
          quadtree.beginFrame(scene.frameState);
          quadtree.update(scene.frameState);
          quadtree.endFrame(scene.frameState);
  
          // load tiles
          quadtree.beginFrame(scene.frameState);
          quadtree.update(scene.frameState);
          quadtree.endFrame(scene.frameState);
  
          expect(tileProvider.initialize).toHaveBeenCalled();
          expect(tileProvider.beginUpdate).toHaveBeenCalled();
          expect(tileProvider.loadTile).toHaveBeenCalled();
          expect(tileProvider.endUpdate).toHaveBeenCalled();
      });
  
      it('shows the root tiles when they are ready and visible', function() {
          var tileProvider = createSpyTileProvider();
          tileProvider.getReady.and.returnValue(true);
          tileProvider.computeTileVisibility.and.returnValue(Visibility.FULL);
          tileProvider.loadTile.and.callFake(function(frameState, tile) {
              tile.renderable = true;
          });
  
          var quadtree = new QuadtreePrimitive({
              tileProvider : tileProvider
          });
  
          // determine what tiles to load
          quadtree.beginFrame(scene.frameState);
          quadtree.update(scene.frameState);
          quadtree.endFrame(scene.frameState);
  
          // load tiles
          quadtree.beginFrame(scene.frameState);
          quadtree.update(scene.frameState);
          quadtree.endFrame(scene.frameState);
  
          expect(tileProvider.showTileThisFrame).toHaveBeenCalled();
      });
  
      it('stops loading a tile that moves to the DONE state', function() {
          var tileProvider = createSpyTileProvider();
          tileProvider.getReady.and.returnValue(true);
          tileProvider.computeTileVisibility.and.returnValue(Visibility.FULL);
  
          var calls = 0;
          tileProvider.loadTile.and.callFake(function(frameState, tile) {
              ++calls;
              tile.state = QuadtreeTileLoadState.DONE;
          });
  
          var quadtree = new QuadtreePrimitive({
              tileProvider : tileProvider
          });
  
          // determine what tiles to load
          quadtree.beginFrame(scene.frameState);
          quadtree.update(scene.frameState);
          quadtree.endFrame(scene.frameState);
  
          // load tiles
          quadtree.beginFrame(scene.frameState);
          quadtree.update(scene.frameState);
          quadtree.endFrame(scene.frameState);
          expect(calls).toBe(2);
  
          quadtree.beginFrame(scene.frameState);
          quadtree.update(scene.frameState);
          quadtree.endFrame(scene.frameState);
          expect(calls).toBe(2);
      });
  
      it('tileLoadProgressEvent is raised when tile loaded and when new children discovered', function() {
          var eventHelper = new EventHelper();
  
          var tileProvider = createSpyTileProvider();
          tileProvider.getReady.and.returnValue(true);
          tileProvider.computeTileVisibility.and.returnValue(Visibility.FULL);
  
          var quadtree = new QuadtreePrimitive({
              tileProvider : tileProvider
          });
  
          var progressEventSpy = jasmine.createSpy('progressEventSpy');
          eventHelper.add(quadtree.tileLoadProgressEvent, progressEventSpy);
  
          // Initial update to get the zero-level tiles set up.
          quadtree.beginFrame(scene.frameState);
          quadtree.update(scene.frameState);
          quadtree.endFrame(scene.frameState);
  
          // load zero-level tiles
          quadtree.beginFrame(scene.frameState);
          quadtree.update(scene.frameState);
          quadtree.endFrame(scene.frameState);
  
          // There will now be two zero-level tiles in the load queue.
          expect(progressEventSpy.calls.mostRecent().args[0]).toEqual(2);
  
          // Change one to loaded and update again
          quadtree._levelZeroTiles[0].state = QuadtreeTileLoadState.DONE;
          quadtree._levelZeroTiles[1].state = QuadtreeTileLoadState.LOADING;
          quadtree.beginFrame(scene.frameState);
          quadtree.update(scene.frameState);
          quadtree.endFrame(scene.frameState);
  
          // Now there should only be one left in the update queue
          expect(progressEventSpy.calls.mostRecent().args[0]).toEqual(1);
  
          // Simulate the second zero-level child having loaded with two children.
          quadtree._levelZeroTiles[1].state = QuadtreeTileLoadState.DONE;
          quadtree._levelZeroTiles[1].renderable = true;
          quadtree.beginFrame(scene.frameState);
          quadtree.update(scene.frameState);
          quadtree.endFrame(scene.frameState);
  
          // Now that tile's four children should be in the load queue.
          expect(progressEventSpy.calls.mostRecent().args[0]).toEqual(4);
      });
  
      it('forEachLoadedTile does not enumerate tiles in the START state', function() {
          var tileProvider = createSpyTileProvider();
          tileProvider.getReady.and.returnValue(true);
          tileProvider.computeTileVisibility.and.returnValue(Visibility.FULL);
          tileProvider.computeDistanceToTile.and.returnValue(1e-15);
  
          // Load the root tiles.
          tileProvider.loadTile.and.callFake(function(frameState, tile) {
              tile.state = QuadtreeTileLoadState.DONE;
              tile.renderable = true;
          });
  
          var quadtree = new QuadtreePrimitive({
              tileProvider : tileProvider
          });
  
          // determine what tiles to load
          quadtree.beginFrame(scene.frameState);
          quadtree.update(scene.frameState);
          quadtree.endFrame(scene.frameState);
  
          // load tiles
          quadtree.beginFrame(scene.frameState);
          quadtree.update(scene.frameState);
          quadtree.endFrame(scene.frameState);
  
          // Don't load further tiles.
          tileProvider.loadTile.and.callFake(function(frameState, tile) {
              tile.state = QuadtreeTileLoadState.START;
          });
  
          quadtree.beginFrame(scene.frameState);
          quadtree.update(scene.frameState);
          quadtree.endFrame(scene.frameState);
  
          quadtree.forEachLoadedTile(function(tile) {
              expect(tile.state).not.toBe(QuadtreeTileLoadState.START);
          });
      });
  
      it('add and remove callbacks to tiles', function() {
          var tileProvider = createSpyTileProvider();
          tileProvider.getReady.and.returnValue(true);
          tileProvider.computeTileVisibility.and.returnValue(Visibility.FULL);
          tileProvider.computeDistanceToTile.and.returnValue(1e-15);
  
          // Load the root tiles.
          tileProvider.loadTile.and.callFake(function(frameState, tile) {
              tile.state = QuadtreeTileLoadState.DONE;
              tile.renderable = true;
              tile.data = {
                  pick : function() {
                      return undefined;
                  }
              };
          });
  
          var quadtree = new QuadtreePrimitive({
              tileProvider : tileProvider
          });
  
          var removeFunc = quadtree.updateHeight(Cartographic.fromDegrees(-72.0, 40.0), function(position) {
          });
  
          // determine what tiles to load
          quadtree.beginFrame(scene.frameState);
          quadtree.update(scene.frameState);
          quadtree.endFrame(scene.frameState);
  
          // load tiles
          quadtree.beginFrame(scene.frameState);
          quadtree.update(scene.frameState);
          quadtree.endFrame(scene.frameState);
  
          var addedCallback = false;
          quadtree.forEachLoadedTile(function(tile) {
              addedCallback = addedCallback || tile.customData.length > 0;
          });
  
          expect(addedCallback).toEqual(true);
  
          removeFunc();
          quadtree.beginFrame(scene.frameState);
          quadtree.update(scene.frameState);
          quadtree.endFrame(scene.frameState);
  
          var removedCallback = true;
          quadtree.forEachLoadedTile(function(tile) {
              removedCallback = removedCallback && tile.customData.length === 0;
          });
  
          expect(removedCallback).toEqual(true);
      });
  
      it('updates heights', function() {
          var tileProvider = createSpyTileProvider();
          tileProvider.getReady.and.returnValue(true);
          tileProvider.computeTileVisibility.and.returnValue(Visibility.FULL);
          tileProvider.computeDistanceToTile.and.returnValue(1e-15);
  
          tileProvider.terrainProvider = {
              getTileDataAvailable : function() {
                  return true;
              }
          };
  
          var position = Cartesian3.clone(Cartesian3.ZERO);
          var updatedPosition = Cartesian3.clone(Cartesian3.UNIT_X);
          var currentPosition = position;
  
          // Load the root tiles.
          tileProvider.loadTile.and.callFake(function(frameState, tile) {
              tile.state = QuadtreeTileLoadState.DONE;
              tile.renderable = true;
              tile.data = {
                  pick : function() {
                      return currentPosition;
                  }
              };
          });
  
          var quadtree = new QuadtreePrimitive({
              tileProvider : tileProvider
          });
  
          quadtree.updateHeight(Cartographic.fromDegrees(-72.0, 40.0), function(p) {
              Cartesian3.clone(p, position);
          });
  
          // determine what tiles to load
          quadtree.beginFrame(scene.frameState);
          quadtree.update(scene.frameState);
          quadtree.endFrame(scene.frameState);
  
          // load tiles
          quadtree.beginFrame(scene.frameState);
          quadtree.update(scene.frameState);
          quadtree.endFrame(scene.frameState);
  
          expect(position).toEqual(Cartesian3.ZERO);
  
          currentPosition = updatedPosition;
  
          quadtree.beginFrame(scene.frameState);
          quadtree.update(scene.frameState);
          quadtree.endFrame(scene.frameState);
  
          expect(position).toEqual(updatedPosition);
      });
  
      it('gives correct priority to tile loads', function() {
          var tileProvider = createSpyTileProvider();
          tileProvider.getReady.and.returnValue(true);
          tileProvider.computeTileVisibility.and.returnValue(Visibility.FULL);
  
          var quadtree = new QuadtreePrimitive({
              tileProvider : tileProvider
          });
  
          quadtree.beginFrame(scene.frameState);
          quadtree.update(scene.frameState);
          quadtree.endFrame(scene.frameState);
  
          // The root tiles should be in the high priority load queue
          expect(quadtree._tileLoadQueueHigh.length).toBe(2);
          expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[0]);
          expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[1]);
          expect(quadtree._tileLoadQueueMedium.length).toBe(0);
          expect(quadtree._tileLoadQueueLow.length).toBe(0);
          
          // Mark the first root tile renderable (but not done loading)
          quadtree._levelZeroTiles[0].renderable = true;
  
          quadtree.beginFrame(scene.frameState);
          quadtree.update(scene.frameState);
          quadtree.endFrame(scene.frameState);
  
          // That root tile should now load with low priority while its children should load with high.
          expect(quadtree._tileLoadQueueHigh.length).toBe(5);
          expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[1]);
          expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[0].children[0]);
          expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[0].children[1]);
          expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[0].children[2]);
          expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[0].children[3]);
          expect(quadtree._tileLoadQueueMedium.length).toBe(0);
          expect(quadtree._tileLoadQueueLow.length).toBe(1);
          expect(quadtree._tileLoadQueueLow).toContain(quadtree._levelZeroTiles[0]);
  
          // Mark the children of that root tile renderable too, so we can refine it
          quadtree._levelZeroTiles[0].children[0].renderable = true;
          quadtree._levelZeroTiles[0].children[1].renderable = true;
          quadtree._levelZeroTiles[0].children[2].renderable = true;
          quadtree._levelZeroTiles[0].children[3].renderable = true;
  
          quadtree.beginFrame(scene.frameState);
          quadtree.update(scene.frameState);
          quadtree.endFrame(scene.frameState);
  
          expect(quadtree._tileLoadQueueHigh.length).toBe(17); // levelZeroTiles[1] plus levelZeroTiles[0]'s 16 grandchildren
          expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[1]);
          expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[0].children[0].children[0]);
          expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[0].children[0].children[1]);
          expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[0].children[0].children[2]);
          expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[0].children[0].children[3]);
          expect(quadtree._tileLoadQueueMedium.length).toBe(0);
          expect(quadtree._tileLoadQueueLow.length).toBe(5);
          expect(quadtree._tileLoadQueueLow).toContain(quadtree._levelZeroTiles[0]);
          expect(quadtree._tileLoadQueueLow).toContain(quadtree._levelZeroTiles[0].children[0]);
          expect(quadtree._tileLoadQueueLow).toContain(quadtree._levelZeroTiles[0].children[1]);
          expect(quadtree._tileLoadQueueLow).toContain(quadtree._levelZeroTiles[0].children[2]);
          expect(quadtree._tileLoadQueueLow).toContain(quadtree._levelZeroTiles[0].children[3]);
  
          // Mark the children of levelZeroTiles[0] upsampled
          quadtree._levelZeroTiles[0].children[0].upsampledFromParent = true;
          quadtree._levelZeroTiles[0].children[1].upsampledFromParent = true;
          quadtree._levelZeroTiles[0].children[2].upsampledFromParent = true;
          quadtree._levelZeroTiles[0].children[3].upsampledFromParent = true;
          
          quadtree.beginFrame(scene.frameState);
          quadtree.update(scene.frameState);
          quadtree.endFrame(scene.frameState);
  
          // levelZeroTiles[0] should move to medium priority.
          // Its descendents should continue loading, so they have a chance to decide they're not upsampled later.
          expect(quadtree._tileLoadQueueHigh.length).toBe(1);
          expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[1]);
          expect(quadtree._tileLoadQueueMedium.length).toBe(1);
          expect(quadtree._tileLoadQueueMedium).toContain(quadtree._levelZeroTiles[0]);
          expect(quadtree._tileLoadQueueLow.length).toBe(4);
          expect(quadtree._tileLoadQueueLow).toContain(quadtree._levelZeroTiles[0].children[0]);
          expect(quadtree._tileLoadQueueLow).toContain(quadtree._levelZeroTiles[0].children[1]);
          expect(quadtree._tileLoadQueueLow).toContain(quadtree._levelZeroTiles[0].children[2]);
          expect(quadtree._tileLoadQueueLow).toContain(quadtree._levelZeroTiles[0].children[3]);
      });
  
      it('renders tiles in approximate near-to-far order', function() {
          var tileProvider = createSpyTileProvider();
          tileProvider.getReady.and.returnValue(true);
          tileProvider.computeTileVisibility.and.returnValue(Visibility.FULL);
  
          var quadtree = new QuadtreePrimitive({
              tileProvider : tileProvider
          });
  
          tileProvider.loadTile.and.callFake(function(frameState, tile) {
              if (tile.level <= 1) {
                  tile.state = QuadtreeTileLoadState.DONE;
                  tile.renderable = true;
              }
          });
  
          scene.camera.setView({
              destination : Cartesian3.fromDegrees(1.0, 1.0, 15000.0)
          });
          scene.camera.update(scene.mode);
  
          return pollToPromise(function() {
              quadtree.beginFrame(scene.frameState);
              quadtree.update(scene.frameState);
              quadtree.endFrame(scene.frameState);
  
              return quadtree._tilesToRender.filter(function(tile) { return tile.level === 1; }).length === 8;
          }).then(function() {
              quadtree.beginFrame(scene.frameState);
              quadtree.update(scene.frameState);
              quadtree.endFrame(scene.frameState);
  
              // Rendered tiles:
              // +----+----+----+----+
              // |w.nw|w.ne|e.nw|e.ne|
              // +----+----+----+----+
              // |w.sw|w.se|e.sw|e.se|
              // +----+----+----+----+
              // camera is located in e.nw (east.northwestChild)
  
              var west = quadtree._levelZeroTiles.filter(function(tile) { return tile.x === 0; })[0];
              var east = quadtree._levelZeroTiles.filter(function(tile) { return tile.x === 1; })[0];
              expect(quadtree._tilesToRender[0]).toBe(east.northwestChild);
              expect(quadtree._tilesToRender[1] === east.southwestChild || quadtree._tilesToRender[1] === east.northeastChild).toBe(true);
              expect(quadtree._tilesToRender[2] === east.southwestChild || quadtree._tilesToRender[2] === east.northeastChild).toBe(true);
              expect(quadtree._tilesToRender[3]).toBe(east.southeastChild);
              expect(quadtree._tilesToRender[4]).toBe(west.northeastChild);
              expect(quadtree._tilesToRender[5] === west.northwestChild || quadtree._tilesToRender[5] === west.southeastChild).toBe(true);
              expect(quadtree._tilesToRender[6] === west.northwestChild || quadtree._tilesToRender[6] === west.southeastChild).toBe(true);
              expect(quadtree._tilesToRender[7]).toBe(west.southwestChild);
          });
      });
  }, 'WebGL');
  