import { BoundingSphere } from "../../Source/Cesium.js";
import { Cartesian2 } from "../../Source/Cesium.js";
import { Cartesian3 } from "../../Source/Cesium.js";
import { CesiumTerrainProvider } from "../../Source/Cesium.js";
import { Color } from "../../Source/Cesium.js";
import { defined } from "../../Source/Cesium.js";
import { Ellipsoid } from "../../Source/Cesium.js";
import { GeographicProjection } from "../../Source/Cesium.js";
import { GeometryInstance } from "../../Source/Cesium.js";
import { HeadingPitchRoll } from "../../Source/Cesium.js";
import { JulianDate } from "../../Source/Cesium.js";
import { Math as CesiumMath } from "../../Source/Cesium.js";
import { PerspectiveFrustum } from "../../Source/Cesium.js";
import { PixelFormat } from "../../Source/Cesium.js";
import { Rectangle } from "../../Source/Cesium.js";
import { RectangleGeometry } from "../../Source/Cesium.js";
import { RequestScheduler } from "../../Source/Cesium.js";
import { RuntimeError } from "../../Source/Cesium.js";
import { TaskProcessor } from "../../Source/Cesium.js";
import { WebGLConstants } from "../../Source/Cesium.js";
import { WebMercatorProjection } from "../../Source/Cesium.js";
import { DrawCommand } from "../../Source/Cesium.js";
import { Framebuffer } from "../../Source/Cesium.js";
import { Pass } from "../../Source/Cesium.js";
import { PixelDatatype } from "../../Source/Cesium.js";
import { RenderState } from "../../Source/Cesium.js";
import { ShaderProgram } from "../../Source/Cesium.js";
import { ShaderSource } from "../../Source/Cesium.js";
import { Texture } from "../../Source/Cesium.js";
import { Camera } from "../../Source/Cesium.js";
import { DirectionalLight } from "../../Source/Cesium.js";
import { EllipsoidSurfaceAppearance } from "../../Source/Cesium.js";
import { FrameState } from "../../Source/Cesium.js";
import { Globe } from "../../Source/Cesium.js";
import { Material } from "../../Source/Cesium.js";
import { Primitive } from "../../Source/Cesium.js";
import { PrimitiveCollection } from "../../Source/Cesium.js";
import { Scene } from "../../Source/Cesium.js";
import { SceneTransforms } from "../../Source/Cesium.js";
import { ScreenSpaceCameraController } from "../../Source/Cesium.js";
import { SunLight } from "../../Source/Cesium.js";
import { TweenCollection } from "../../Source/Cesium.js";
import { Sun } from "../../Source/Cesium.js";
import { GroundPrimitive } from "../../Source/Cesium.js";
import { PerInstanceColorAppearance } from "../../Source/Cesium.js";
import { ColorGeometryInstanceAttribute } from "../../Source/Cesium.js";
import createCanvas from "../createCanvas.js";
import createScene from "../createScene.js";
import pollToPromise from "../pollToPromise.js";
import render from "../render.js";

describe(
  "Scene/Scene",
  function () {
    var scene;
    var simpleShaderProgram;
    var simpleRenderState;

    beforeAll(function () {
      scene = createScene();
      simpleShaderProgram = ShaderProgram.fromCache({
        context: scene.context,
        vertexShaderSource: new ShaderSource({
          sources: ["void main() { gl_Position = vec4(1.0); }"],
        }),
        fragmentShaderSource: new ShaderSource({
          sources: ["void main() { gl_FragColor = vec4(1.0); }"],
        }),
      });
      simpleRenderState = new RenderState();

      return GroundPrimitive.initializeTerrainHeights();
    });

    afterEach(function () {
      scene.backgroundColor = new Color(0.0, 0.0, 0.0, 0.0);
      scene.debugCommandFilter = undefined;
      scene.postProcessStages.fxaa.enabled = false;
      scene.primitives.removeAll();
      scene.morphTo3D(0.0);

      var camera = scene.camera;
      camera.frustum = new PerspectiveFrustum();
      camera.frustum.aspectRatio =
        scene.drawingBufferWidth / scene.drawingBufferHeight;
      camera.frustum.fov = CesiumMath.toRadians(60.0);
    });

    afterAll(function () {
      scene.destroyForSpecs();
    });

    function createRectangle(rectangle, height) {
      return new Primitive({
        geometryInstances: new GeometryInstance({
          geometry: new RectangleGeometry({
            rectangle: rectangle,
            vertexFormat: EllipsoidSurfaceAppearance.VERTEX_FORMAT,
            height: height,
          }),
        }),
        appearance: new EllipsoidSurfaceAppearance({
          aboveGround: false,
        }),
        asynchronous: false,
      });
    }

    it("constructor has expected defaults", function () {
      expect(scene.canvas).toBeInstanceOf(HTMLCanvasElement);
      expect(scene.primitives).toBeInstanceOf(PrimitiveCollection);
      expect(scene.camera).toBeInstanceOf(Camera);
      expect(scene.screenSpaceCameraController).toBeInstanceOf(
        ScreenSpaceCameraController
      );
      expect(scene.mapProjection).toBeInstanceOf(GeographicProjection);
      expect(scene.frameState).toBeInstanceOf(FrameState);
      expect(scene.tweens).toBeInstanceOf(TweenCollection);

      var contextAttributes = scene.context._gl.getContextAttributes();
      // Do not check depth and antialias since they are requests not requirements
      expect(contextAttributes.alpha).toEqual(false);
      expect(contextAttributes.stencil).toEqual(true);
      expect(contextAttributes.premultipliedAlpha).toEqual(true);
      expect(contextAttributes.preserveDrawingBuffer).toEqual(false);
    });

    it("constructor sets options", function () {
      var webglOptions = {
        alpha: true,
        depth: true, //TODO Change to false when https://bugzilla.mozilla.org/show_bug.cgi?id=745912 is fixed.
        stencil: true,
        antialias: false,
        premultipliedAlpha: true, // Workaround IE 11.0.8, which does not honor false.
        preserveDrawingBuffer: true,
      };
      var mapProjection = new WebMercatorProjection();

      var s = createScene({
        contextOptions: {
          webgl: webglOptions,
        },
        mapProjection: mapProjection,
      });

      var contextAttributes = s.context._gl.getContextAttributes();
      expect(contextAttributes.alpha).toEqual(webglOptions.alpha);
      expect(contextAttributes.depth).toEqual(webglOptions.depth);
      expect(contextAttributes.stencil).toEqual(webglOptions.stencil);
      expect(contextAttributes.antialias).toEqual(webglOptions.antialias);
      expect(contextAttributes.premultipliedAlpha).toEqual(
        webglOptions.premultipliedAlpha
      );
      expect(contextAttributes.preserveDrawingBuffer).toEqual(
        webglOptions.preserveDrawingBuffer
      );
      expect(s.mapProjection).toEqual(mapProjection);

      s.destroyForSpecs();
    });

    it("constructor throws without options", function () {
      expect(function () {
        return new Scene();
      }).toThrowDeveloperError();
    });

    it("constructor throws without options.canvas", function () {
      expect(function () {
        return new Scene({});
      }).toThrowDeveloperError();
    });

    it("draws background color", function () {
      expect(scene).toRender([0, 0, 0, 255]);

      scene.backgroundColor = Color.BLUE;
      expect(scene).toRender([0, 0, 255, 255]);
    });

    it("calls afterRender functions", function () {
      var spyListener = jasmine.createSpy("listener");

      var primitive = {
        update: function (frameState) {
          frameState.afterRender.push(spyListener);
        },
        destroy: function () {},
      };
      scene.primitives.add(primitive);

      scene.renderForSpecs();
      expect(spyListener).toHaveBeenCalled();
    });

    function CommandMockPrimitive(command) {
      this.update = function (frameState) {
        frameState.commandList.push(command);
      };
      this.destroy = function () {};
    }

    it("debugCommandFilter filters commands", function () {
      var c = new DrawCommand({
        shaderProgram: simpleShaderProgram,
        renderState: simpleRenderState,
        pass: Pass.OPAQUE,
      });
      c.execute = function () {};
      spyOn(c, "execute");

      scene.primitives.add(new CommandMockPrimitive(c));

      scene.debugCommandFilter = function (command) {
        return command !== c; // Do not execute command
      };

      scene.renderForSpecs();
      expect(c.execute).not.toHaveBeenCalled();
    });

    it("debugCommandFilter does not filter commands", function () {
      var originalLogDepth = scene.logarithmicDepthBuffer;
      scene.logarithmicDepthBuffer = false;

      var c = new DrawCommand({
        shaderProgram: simpleShaderProgram,
        renderState: simpleRenderState,
        pass: Pass.OPAQUE,
      });
      c.execute = function () {};
      spyOn(c, "execute");

      scene.primitives.add(new CommandMockPrimitive(c));

      expect(scene.debugCommandFilter).toBeUndefined();
      scene.renderForSpecs();
      expect(c.execute).toHaveBeenCalled();

      scene.logarithmicDepthBuffer = originalLogDepth;
    });

    it("debugShowBoundingVolume draws a bounding sphere", function () {
      var originalLogDepth = scene.logarithmicDepthBuffer;
      scene.logarithmicDepthBuffer = false;

      var radius = 10.0;
      var center = Cartesian3.add(
        scene.camera.position,
        scene.camera.direction,
        new Cartesian3()
      );

      var c = new DrawCommand({
        shaderProgram: simpleShaderProgram,
        renderState: simpleRenderState,
        pass: Pass.OPAQUE,
        debugShowBoundingVolume: true,
        boundingVolume: new BoundingSphere(center, radius),
      });
      c.execute = function () {};

      scene.primitives.add(new CommandMockPrimitive(c));
      scene.depthTestAgainstTerrain = true;

      expect(scene).toRenderAndCall(function (rgba) {
        expect(rgba[0]).not.toEqual(0); // Red bounding sphere
      });

      scene.logarithmicDepthBuffer = originalLogDepth;
    });

    it("debugShowCommands tints commands", function () {
      var originalLogDepth = scene.logarithmicDepthBuffer;
      scene.logarithmicDepthBuffer = false;

      var c = new DrawCommand({
        shaderProgram: simpleShaderProgram,
        renderState: simpleRenderState,
        pass: Pass.OPAQUE,
      });
      c.execute = function () {};

      var originalShallowClone = DrawCommand.shallowClone;
      spyOn(DrawCommand, "shallowClone").and.callFake(function (
        command,
        result
      ) {
        result = originalShallowClone(command, result);
        result.execute = function () {
          result.uniformMap.debugShowCommandsColor();
        };
        return result;
      });

      scene.primitives.add(new CommandMockPrimitive(c));

      scene.debugShowCommands = true;
      scene.renderForSpecs();
      expect(c._debugColor).toBeDefined();
      scene.debugShowCommands = false;

      scene.logarithmicDepthBuffer = originalLogDepth;
    });

    it("debugShowFramesPerSecond", function () {
      scene.debugShowFramesPerSecond = true;
      scene.renderForSpecs();
      expect(scene._performanceDisplay).toBeDefined();
      scene.debugShowFramesPerSecond = false;
    });

    it("debugShowGlobeDepth", function () {
      if (!scene.context.depthTexture) {
        return;
      }

      var rectangle = Rectangle.fromDegrees(-100.0, 30.0, -90.0, 40.0);
      scene.camera.setView({ destination: rectangle });

      var rectanglePrimitive = createRectangle(rectangle);
      rectanglePrimitive.appearance.material.uniforms.color = new Color(
        1.0,
        0.0,
        0.0,
        1.0
      );

      scene.primitives.add(rectanglePrimitive);
      expect(scene).toRender([255, 0, 0, 255]);

      scene.debugShowGlobeDepth = true;
      expect(scene).notToRender([255, 0, 0, 255]);

      scene.debugShowGlobeDepth = false;
    });

    it("opaque/translucent render order (1)", function () {
      var rectangle = Rectangle.fromDegrees(-100.0, 30.0, -90.0, 40.0);

      var rectanglePrimitive1 = createRectangle(rectangle);
      rectanglePrimitive1.appearance.material.uniforms.color = new Color(
        1.0,
        0.0,
        0.0,
        1.0
      );

      var rectanglePrimitive2 = createRectangle(rectangle, 1000.0);
      rectanglePrimitive2.appearance.material.uniforms.color = new Color(
        0.0,
        1.0,
        0.0,
        0.5
      );

      var primitives = scene.primitives;
      primitives.add(rectanglePrimitive1);
      primitives.add(rectanglePrimitive2);

      scene.camera.setView({ destination: rectangle });
      expect(scene).toRenderAndCall(function (rgba) {
        expect(rgba[0]).not.toEqual(0);
        expect(rgba[1]).not.toEqual(0);
        expect(rgba[2]).toEqual(0);
      });

      primitives.raiseToTop(rectanglePrimitive1);
      expect(scene).toRenderAndCall(function (rgba) {
        expect(rgba[0]).not.toEqual(0);
        expect(rgba[1]).not.toEqual(0);
        expect(rgba[2]).toEqual(0);
      });
    });

    it("opaque/translucent render order (2)", function () {
      var rectangle = Rectangle.fromDegrees(-100.0, 30.0, -90.0, 40.0);

      var rectanglePrimitive1 = createRectangle(rectangle, 1000.0);
      rectanglePrimitive1.appearance.material.uniforms.color = new Color(
        1.0,
        0.0,
        0.0,
        1.0
      );

      var rectanglePrimitive2 = createRectangle(rectangle);
      rectanglePrimitive2.appearance.material.uniforms.color = new Color(
        0.0,
        1.0,
        0.0,
        0.5
      );

      var primitives = scene.primitives;
      primitives.add(rectanglePrimitive1);
      primitives.add(rectanglePrimitive2);

      scene.camera.setView({ destination: rectangle });
      expect(scene).toRenderAndCall(function (rgba) {
        expect(rgba[0]).not.toEqual(0);
        expect(rgba[1]).toEqual(0);
        expect(rgba[2]).toEqual(0);
      });

      primitives.raiseToTop(rectanglePrimitive1);
      expect(scene).toRenderAndCall(function (rgba) {
        expect(rgba[0]).not.toEqual(0);
        expect(rgba[1]).toEqual(0);
        expect(rgba[2]).toEqual(0);
      });
    });

    it("renders with OIT and without FXAA", function () {
      var rectangle = Rectangle.fromDegrees(-100.0, 30.0, -90.0, 40.0);

      var rectanglePrimitive = createRectangle(rectangle, 1000.0);
      rectanglePrimitive.appearance.material.uniforms.color = new Color(
        1.0,
        0.0,
        0.0,
        0.5
      );

      var primitives = scene.primitives;
      primitives.add(rectanglePrimitive);

      scene.camera.setView({ destination: rectangle });
      scene.postProcessStages.fxaa.enabled = false;
      expect(scene).toRenderAndCall(function (rgba) {
        expect(rgba[0]).not.toEqual(0);
        expect(rgba[1]).toEqual(0);
        expect(rgba[2]).toEqual(0);
      });
    });

    it("renders with forced FXAA", function () {
      var context = scene.context;

      // Workaround for Firefox on Mac, which does not support RGBA + depth texture
      // attachments, which is allowed by the spec.
      if (context.depthTexture) {
        var framebuffer = new Framebuffer({
          context: context,
          colorTextures: [
            new Texture({
              context: context,
              width: 1,
              height: 1,
              pixelFormat: PixelFormat.RGBA,
              pixelDatatype: PixelDatatype.UNSIGNED_BYTE,
            }),
          ],
          depthTexture: new Texture({
            context: context,
            width: 1,
            height: 1,
            pixelFormat: PixelFormat.DEPTH_COMPONENT,
            pixelDatatype: PixelDatatype.UNSIGNED_SHORT,
          }),
        });

        var status = framebuffer.status;
        framebuffer.destroy();

        if (status !== WebGLConstants.FRAMEBUFFER_COMPLETE) {
          return;
        }
      }

      var s = createScene();

      if (defined(s._oit)) {
        s._oit._translucentMRTSupport = false;
        s._oit._translucentMultipassSupport = false;
      }

      s.postProcessStages.fxaa.enabled = false;

      var rectangle = Rectangle.fromDegrees(-100.0, 30.0, -90.0, 40.0);

      var rectanglePrimitive = createRectangle(rectangle, 1000.0);
      rectanglePrimitive.appearance.material.uniforms.color = new Color(
        1.0,
        0.0,
        0.0,
        1.0
      );

      var primitives = s.primitives;
      primitives.add(rectanglePrimitive);

      s.camera.setView({ destination: rectangle });

      expect(s).toRenderAndCall(function (rgba) {
        expect(rgba[0]).not.toEqual(0);
        expect(rgba[1]).toEqual(0);
        expect(rgba[2]).toEqual(0);
      });

      s.destroyForSpecs();
    });

    it("setting a globe", function () {
      var scene = createScene();
      var ellipsoid = Ellipsoid.UNIT_SPHERE;
      var globe = new Globe(ellipsoid);
      scene.globe = globe;

      expect(scene.globe).toBe(globe);

      scene.destroyForSpecs();
    });

    it("destroys primitive on set globe", function () {
      var scene = createScene();
      var globe = new Globe(Ellipsoid.UNIT_SPHERE);

      scene.globe = globe;
      expect(globe.isDestroyed()).toEqual(false);

      scene.globe = undefined;
      expect(globe.isDestroyed()).toEqual(true);

      scene.destroyForSpecs();
    });

    describe("render tests", function () {
      var s;

      beforeEach(function () {
        s = createScene();
      });

      afterEach(function () {
        s.destroyForSpecs();
      });

      it("renders a globe", function () {
        s.globe = new Globe(Ellipsoid.UNIT_SPHERE);
        s.camera.position = new Cartesian3(1.02, 0.0, 0.0);
        s.camera.up = Cartesian3.clone(Cartesian3.UNIT_Z);
        s.camera.direction = Cartesian3.negate(
          Cartesian3.normalize(s.camera.position, new Cartesian3()),
          new Cartesian3()
        );

        // To avoid Jasmine's spec has no expectations error
        expect(true).toEqual(true);

        return expect(s).toRenderAndCall(function () {
          return pollToPromise(function () {
            render(s.frameState, s.globe);
            return !jasmine.matchersUtil.equals(s._context.readPixels(), [
              0,
              0,
              0,
              0,
            ]);
          });
        });
      });

      it("renders a globe with an ElevationContour", function () {
        s.globe = new Globe(Ellipsoid.UNIT_SPHERE);
        s.globe.material = Material.fromType("ElevationContour");
        s.camera.position = new Cartesian3(1.02, 0.0, 0.0);
        s.camera.up = Cartesian3.clone(Cartesian3.UNIT_Z);
        s.camera.direction = Cartesian3.negate(
          Cartesian3.normalize(s.camera.position, new Cartesian3()),
          new Cartesian3()
        );

        // To avoid Jasmine's spec has no expectations error
        expect(true).toEqual(true);

        return expect(s).toRenderAndCall(function () {
          return pollToPromise(function () {
            render(s.frameState, s.globe);
            return !jasmine.matchersUtil.equals(s._context.readPixels(), [
              0,
              0,
              0,
              0,
            ]);
          });
        });
      });

      it("renders a globe with a SlopeRamp", function () {
        s.globe = new Globe(Ellipsoid.UNIT_SPHERE);
        s.globe.material = Material.fromType("SlopeRamp");
        s.camera.position = new Cartesian3(1.02, 0.0, 0.0);
        s.camera.up = Cartesian3.clone(Cartesian3.UNIT_Z);
        s.camera.direction = Cartesian3.negate(
          Cartesian3.normalize(s.camera.position, new Cartesian3()),
          new Cartesian3()
        );

        // To avoid Jasmine's spec has no expectations error
        expect(true).toEqual(true);

        return expect(s).toRenderAndCall(function () {
          return pollToPromise(function () {
            render(s.frameState, s.globe);
            return !jasmine.matchersUtil.equals(s._context.readPixels(), [
              0,
              0,
              0,
              0,
            ]);
          });
        });
      });

      it("renders a globe with AspectRamp", function () {
        s.globe = new Globe(Ellipsoid.UNIT_SPHERE);
        s.globe.material = Material.fromType("AspectRamp");
        s.camera.position = new Cartesian3(1.02, 0.0, 0.0);
        s.camera.up = Cartesian3.clone(Cartesian3.UNIT_Z);
        s.camera.direction = Cartesian3.negate(
          Cartesian3.normalize(s.camera.position, new Cartesian3()),
          new Cartesian3()
        );

        // To avoid Jasmine's spec has no expectations error
        expect(true).toEqual(true);

        return expect(s).toRenderAndCall(function () {
          return pollToPromise(function () {
            render(s.frameState, s.globe);
            return !jasmine.matchersUtil.equals(s._context.readPixels(), [
              0,
              0,
              0,
              0,
            ]);
          });
        });
      });

      it("renders a globe with a ElevationRamp", function () {
        s.globe = new Globe(Ellipsoid.UNIT_SPHERE);
        s.globe.material = Material.fromType("ElevationRamp");
        s.camera.position = new Cartesian3(1.02, 0.0, 0.0);
        s.camera.up = Cartesian3.clone(Cartesian3.UNIT_Z);
        s.camera.direction = Cartesian3.negate(
          Cartesian3.normalize(s.camera.position, new Cartesian3()),
          new Cartesian3()
        );

        // To avoid Jasmine's spec has no expectations error
        expect(true).toEqual(true);

        return expect(s).toRenderAndCall(function () {
          return pollToPromise(function () {
            render(s.frameState, s.globe);
            return !jasmine.matchersUtil.equals(s._context.readPixels(), [
              0,
              0,
              0,
              0,
            ]);
          });
        });
      });

      it("renders a globe with an ElevationBand", function () {
        s.globe = new Globe(Ellipsoid.UNIT_SPHERE);
        s.globe.material = Material.fromType("ElevationBand");
        s.camera.position = new Cartesian3(1.02, 0.0, 0.0);
        s.camera.up = Cartesian3.clone(Cartesian3.UNIT_Z);
        s.camera.direction = Cartesian3.negate(
          Cartesian3.normalize(s.camera.position, new Cartesian3()),
          new Cartesian3()
        );

        // To avoid Jasmine's spec has no expectations error
        expect(true).toEqual(true);

        return expect(s).toRenderAndCall(function () {
          return pollToPromise(function () {
            render(s.frameState, s.globe);
            return !jasmine.matchersUtil.equals(s._context.readPixels(), [
              0,
              0,
              0,
              0,
            ]);
          });
        });
      });
    });

    it("renders with multipass OIT if MRT is available", function () {
      if (scene.context.drawBuffers) {
        var s = createScene();
        if (defined(s._oit)) {
          s._oit._translucentMRTSupport = false;
          s._oit._translucentMultipassSupport = true;

          var rectangle = Rectangle.fromDegrees(-100.0, 30.0, -90.0, 40.0);

          var rectanglePrimitive = createRectangle(rectangle, 1000.0);
          rectanglePrimitive.appearance.material.uniforms.color = new Color(
            1.0,
            0.0,
            0.0,
            0.5
          );

          var primitives = s.primitives;
          primitives.add(rectanglePrimitive);

          s.camera.setView({ destination: rectangle });

          expect(s).toRenderAndCall(function (rgba) {
            expect(rgba[0]).not.toEqual(0);
            expect(rgba[1]).toEqual(0);
            expect(rgba[2]).toEqual(0);
          });
        }

        s.destroyForSpecs();
      }
    });

    it("renders with alpha blending if floating point textures are available", function () {
      if (!scene.context.floatingPointTexture) {
        return;
      }
      var s = createScene();
      if (defined(s._oit)) {
        s._oit._translucentMRTSupport = false;
        s._oit._translucentMultipassSupport = false;

        var rectangle = Rectangle.fromDegrees(-100.0, 30.0, -90.0, 40.0);

        var rectanglePrimitive = createRectangle(rectangle, 1000.0);
        rectanglePrimitive.appearance.material.uniforms.color = new Color(
          1.0,
          0.0,
          0.0,
          0.5
        );

        var primitives = s.primitives;
        primitives.add(rectanglePrimitive);

        s.camera.setView({ destination: rectangle });

        expect(s).toRenderAndCall(function (rgba) {
          expect(rgba[0]).not.toEqual(0);
          expect(rgba[1]).toEqual(0);
          expect(rgba[2]).toEqual(0);
        });
      }
      s.destroyForSpecs();
    });

    it("renders map twice when in 2D", function () {
      scene.morphTo2D(0.0);

      var rectangle = Rectangle.fromDegrees(-180.0, -90.0, 180.0, 90.0);

      var rectanglePrimitive1 = createRectangle(rectangle, 0.0);
      rectanglePrimitive1.appearance.material.uniforms.color = new Color(
        1.0,
        0.0,
        0.0,
        1.0
      );

      var primitives = scene.primitives;
      primitives.add(rectanglePrimitive1);

      scene.camera.setView({
        destination: new Cartesian3(
          Ellipsoid.WGS84.maximumRadius * Math.PI + 10000.0,
          0.0,
          10.0
        ),
        convert: false,
      });

      expect(scene).toRenderAndCall(function (rgba) {
        expect(rgba[0]).not.toEqual(0);
        expect(rgba[1]).toEqual(0);
        expect(rgba[2]).toEqual(0);
      });
    });

    it("renders map when the camera is on the IDL in 2D", function () {
      var s = createScene({
        canvas: createCanvas(5, 5),
      });
      s.morphTo2D(0.0);

      var rectangle = Rectangle.fromDegrees(-180.0, -90.0, 180.0, 90.0);

      var rectanglePrimitive1 = createRectangle(rectangle, 0.0);
      rectanglePrimitive1.appearance.material.uniforms.color = new Color(
        1.0,
        0.0,
        0.0,
        1.0
      );

      var primitives = s.primitives;
      primitives.add(rectanglePrimitive1);

      s.camera.setView({
        destination: new Cartesian3(
          Ellipsoid.WGS84.maximumRadius * Math.PI,
          0.0,
          10.0
        ),
        convert: false,
      });

      expect(s).toRenderAndCall(function (rgba) {
        expect(rgba[0]).not.toEqual(0);
        expect(rgba[1]).toEqual(0);
        expect(rgba[2]).toEqual(0);
      });

      s.destroyForSpecs();
    });

    it("renders with HDR when available", function () {
      if (!scene.highDynamicRangeSupported) {
        return;
      }

      var s = createScene();
      s.highDynamicRange = true;

      var rectangle = Rectangle.fromDegrees(-100.0, 30.0, -90.0, 40.0);

      var rectanglePrimitive = createRectangle(rectangle, 1000.0);
      rectanglePrimitive.appearance.material.uniforms.color = new Color(
        10.0,
        0.0,
        0.0,
        1.0
      );

      var primitives = s.primitives;
      primitives.add(rectanglePrimitive);

      s.camera.setView({ destination: rectangle });

      expect(s).toRenderAndCall(function (rgba) {
        expect(rgba[0]).toBeGreaterThan(0);
        expect(rgba[0]).toBeLessThanOrEqualTo(255);
        expect(rgba[1]).toEqual(0);
        expect(rgba[2]).toEqual(0);
      });

      s.destroyForSpecs();
    });

    it("copies the globe depth", function () {
      var scene = createScene();
      if (scene.context.depthTexture) {
        var rectangle = Rectangle.fromDegrees(-100.0, 30.0, -90.0, 40.0);

        var rectanglePrimitive = createRectangle(rectangle, 1000.0);
        rectanglePrimitive.appearance.material.uniforms.color = new Color(
          1.0,
          0.0,
          0.0,
          0.5
        );

        var primitives = scene.primitives;
        primitives.add(rectanglePrimitive);

        scene.camera.setView({ destination: rectangle });

        var uniformState = scene.context.uniformState;

        expect(scene).toRenderAndCall(function (rgba) {
          expect(uniformState.globeDepthTexture).toBeDefined();
        });
      }

      scene.destroyForSpecs();
    });

    it("pickPosition", function () {
      if (!scene.pickPositionSupported) {
        return;
      }

      var rectangle = Rectangle.fromDegrees(-0.0001, -0.0001, 0.0001, 0.0001);
      scene.camera.setView({ destination: rectangle });

      var canvas = scene.canvas;
      var windowPosition = new Cartesian2(
        canvas.clientWidth / 2,
        canvas.clientHeight / 2
      );

      expect(scene).toRenderAndCall(function () {
        var position = scene.pickPosition(windowPosition);
        expect(position).not.toBeDefined();

        var rectanglePrimitive = createRectangle(rectangle);
        rectanglePrimitive.appearance.material.uniforms.color = new Color(
          1.0,
          0.0,
          0.0,
          1.0
        );

        var primitives = scene.primitives;
        primitives.add(rectanglePrimitive);
      });

      expect(scene).toRenderAndCall(function () {
        var position = scene.pickPosition(windowPosition);
        expect(position.x).toBeGreaterThan(Ellipsoid.WGS84.minimumRadius);
        expect(position.y).toEqualEpsilon(0.0, CesiumMath.EPSILON5);
        expect(position.z).toEqualEpsilon(0.0, CesiumMath.EPSILON5);
      });
    });

    it("pickPosition in CV", function () {
      if (!scene.pickPositionSupported) {
        return;
      }

      scene.morphToColumbusView(0.0);

      var rectangle = Rectangle.fromDegrees(-0.0001, -0.0001, 0.0001, 0.0001);
      scene.camera.setView({ destination: rectangle });

      var canvas = scene.canvas;
      var windowPosition = new Cartesian2(
        canvas.clientWidth / 2,
        canvas.clientHeight / 2
      );

      expect(scene).toRenderAndCall(function () {
        var position = scene.pickPosition(windowPosition);
        expect(position).not.toBeDefined();

        var rectanglePrimitive = createRectangle(rectangle);
        rectanglePrimitive.appearance.material.uniforms.color = new Color(
          1.0,
          0.0,
          0.0,
          1.0
        );

        var primitives = scene.primitives;
        primitives.add(rectanglePrimitive);
      });

      expect(scene).toRenderAndCall(function () {
        var position = scene.pickPosition(windowPosition);
        expect(position.x).toBeGreaterThan(Ellipsoid.WGS84.minimumRadius);
        expect(position.y).toEqualEpsilon(0.0, CesiumMath.EPSILON5);
        expect(position.z).toEqualEpsilon(0.0, CesiumMath.EPSILON5);
      });
    });

    it("pickPosition in 2D", function () {
      if (!scene.pickPositionSupported) {
        return;
      }

      scene.morphTo2D(0.0);

      var rectangle = Rectangle.fromDegrees(-0.0001, -0.0001, 0.0001, 0.0001);
      scene.camera.setView({ destination: rectangle });

      var canvas = scene.canvas;
      var windowPosition = new Cartesian2(
        canvas.clientWidth / 2,
        canvas.clientHeight / 2
      );

      expect(scene).toRenderAndCall(function () {
        var position = scene.pickPosition(windowPosition);
        expect(position).not.toBeDefined();

        var rectanglePrimitive = createRectangle(rectangle);
        rectanglePrimitive.appearance.material.uniforms.color = new Color(
          1.0,
          0.0,
          0.0,
          1.0
        );

        var primitives = scene.primitives;
        primitives.add(rectanglePrimitive);
      });

      expect(scene).toRenderAndCall(function () {
        var position = scene.pickPosition(windowPosition);
        expect(position.x).toBeGreaterThan(Ellipsoid.WGS84.minimumRadius);
        expect(position.y).toEqualEpsilon(0.0, CesiumMath.EPSILON5);
        expect(position.z).toEqualEpsilon(0.0, CesiumMath.EPSILON5);
      });
    });

    it("pickPosition returns undefined when useDepthPicking is false", function () {
      if (!scene.pickPositionSupported) {
        return;
      }

      var rectangle = Rectangle.fromDegrees(-100.0, 30.0, -90.0, 40.0);
      scene.camera.setView({
        destination: rectangle,
      });

      var canvas = scene.canvas;
      var windowPosition = new Cartesian2(
        canvas.clientWidth / 2,
        canvas.clientHeight / 2
      );

      var rectanglePrimitive = createRectangle(rectangle);
      rectanglePrimitive.appearance.material.uniforms.color = new Color(
        1.0,
        0.0,
        0.0,
        1.0
      );

      var primitives = scene.primitives;
      primitives.add(rectanglePrimitive);

      scene.useDepthPicking = false;
      expect(scene).toRenderAndCall(function () {
        var position = scene.pickPosition(windowPosition);
        expect(position).not.toBeDefined();
      });

      scene.useDepthPicking = true;
      expect(scene).toRenderAndCall(function () {
        var position = scene.pickPosition(windowPosition);
        expect(position).toBeDefined();
      });
    });

    it("pickPosition picks translucent geometry when pickTranslucentDepth is true", function () {
      if (!scene.pickPositionSupported) {
        return;
      }

      var rectangle = Rectangle.fromDegrees(-100.0, 30.0, -90.0, 40.0);
      scene.camera.setView({
        destination: rectangle,
      });

      var canvas = scene.canvas;
      var windowPosition = new Cartesian2(
        canvas.clientWidth / 2,
        canvas.clientHeight / 2
      );

      var rectanglePrimitive = scene.primitives.add(createRectangle(rectangle));
      rectanglePrimitive.appearance.material.uniforms.color = new Color(
        1.0,
        0.0,
        0.0,
        0.5
      );

      scene.useDepthPicking = true;
      scene.pickTranslucentDepth = false;
      expect(scene).toRenderAndCall(function () {
        var position = scene.pickPosition(windowPosition);
        expect(position).not.toBeDefined();
      });

      scene.pickTranslucentDepth = true;
      expect(scene).toRenderAndCall(function () {
        var position = scene.pickPosition(windowPosition);
        expect(position).toBeDefined();
      });
    });

    it("pickPosition caches results per frame", function () {
      if (!scene.pickPositionSupported) {
        return;
      }

      var rectangle = Rectangle.fromDegrees(-100.0, 30.0, -90.0, 40.0);
      scene.camera.setView({ destination: rectangle });

      var canvas = scene.canvas;
      var windowPosition = new Cartesian2(
        canvas.clientWidth / 2,
        canvas.clientHeight / 2
      );
      spyOn(
        SceneTransforms,
        "transformWindowToDrawingBuffer"
      ).and.callThrough();

      expect(scene).toRenderAndCall(function () {
        scene.pickPosition(windowPosition);
        expect(
          SceneTransforms.transformWindowToDrawingBuffer
        ).toHaveBeenCalled();

        scene.pickPosition(windowPosition);
        expect(
          SceneTransforms.transformWindowToDrawingBuffer.calls.count()
        ).toEqual(1);

        var rectanglePrimitive = createRectangle(rectangle);
        rectanglePrimitive.appearance.material.uniforms.color = new Color(
          1.0,
          0.0,
          0.0,
          1.0
        );

        var primitives = scene.primitives;
        primitives.add(rectanglePrimitive);
      });

      expect(scene).toRenderAndCall(function () {
        scene.pickPosition(windowPosition);
        expect(
          SceneTransforms.transformWindowToDrawingBuffer.calls.count()
        ).toEqual(2);

        scene.pickPosition(windowPosition);
        expect(
          SceneTransforms.transformWindowToDrawingBuffer.calls.count()
        ).toEqual(2);
      });
    });

    it("pickPosition throws without windowPosition", function () {
      expect(function () {
        scene.pickPosition();
      }).toThrowDeveloperError();
    });

    it("isDestroyed", function () {
      var s = createScene();
      expect(s.isDestroyed()).toEqual(false);
      s.destroyForSpecs();
      expect(s.isDestroyed()).toEqual(true);
    });

    it("raises renderError when render throws", function () {
      var s = createScene({
        rethrowRenderErrors: false,
      });

      var spyListener = jasmine.createSpy("listener");
      s.renderError.addEventListener(spyListener);

      var error = "foo";
      s.primitives.update = function () {
        throw error;
      };

      s.render();

      expect(spyListener).toHaveBeenCalledWith(s, error);

      s.destroyForSpecs();
    });

    it("a render error is rethrown if rethrowRenderErrors is true", function () {
      var s = createScene();
      s.rethrowRenderErrors = true;

      var spyListener = jasmine.createSpy("listener");
      s.renderError.addEventListener(spyListener);

      var error = new RuntimeError("error");
      s.primitives.update = function () {
        throw error;
      };

      expect(function () {
        s.render();
      }).toThrowRuntimeError();

      expect(spyListener).toHaveBeenCalledWith(s, error);

      s.destroyForSpecs();
    });

    it("alwayas raises preUpdate event prior to updating", function () {
      var s = createScene();

      var spyListener = jasmine.createSpy("listener");
      s.preUpdate.addEventListener(spyListener);

      s.render();

      expect(spyListener.calls.count()).toBe(1);

      s.requestRenderMode = true;
      s.maximumRenderTimeChange = undefined;

      s.render();

      expect(spyListener.calls.count()).toBe(2);

      s.destroyForSpecs();
    });

    it("always raises preUpdate event after updating", function () {
      var s = createScene();

      var spyListener = jasmine.createSpy("listener");
      s.preUpdate.addEventListener(spyListener);

      s.render();

      expect(spyListener.calls.count()).toBe(1);

      s.requestRenderMode = true;
      s.maximumRenderTimeChange = undefined;

      s.render();

      expect(spyListener.calls.count()).toBe(2);

      s.destroyForSpecs();
    });

    it("raises the preRender event prior to rendering only if the scene renders", function () {
      var s = createScene();

      var spyListener = jasmine.createSpy("listener");
      s.preRender.addEventListener(spyListener);

      s.render();

      expect(spyListener.calls.count()).toBe(1);

      s.requestRenderMode = true;
      s.maximumRenderTimeChange = undefined;

      s.render();

      expect(spyListener.calls.count()).toBe(1);

      s.destroyForSpecs();
    });

    it("raises the postRender event after rendering if the scene rendered", function () {
      var s = createScene();

      var spyListener = jasmine.createSpy("listener");
      s.postRender.addEventListener(spyListener);

      s.render();

      expect(spyListener.calls.count()).toBe(1);

      s.requestRenderMode = true;
      s.maximumRenderTimeChange = undefined;

      s.render();

      expect(spyListener.calls.count()).toBe(1);

      s.destroyForSpecs();
    });

    it("raises the cameraMoveStart event after moving the camera", function () {
      var s = createScene();
      s.render();

      var spyListener = jasmine.createSpy("listener");
      s.camera.moveStart.addEventListener(spyListener);
      s._cameraStartFired = false; // reset this value after camera changes for initial render trigger the event

      s.camera.moveLeft();
      s.render();

      expect(spyListener.calls.count()).toBe(1);

      s.destroyForSpecs();
    });

    it("raises the cameraMoveEvent event when the camera stops moving", function () {
      var s = createScene();
      s.render();

      var spyListener = jasmine.createSpy("listener");
      s.camera.moveEnd.addEventListener(spyListener);

      // We use negative time here to ensure the event runs on the next frame.
      s.cameraEventWaitTime = -1.0;
      s.camera.moveLeft();
      // The first render will trigger the moveStart event.
      s.render();
      // The second will trigger the moveEnd.
      s.render();

      expect(spyListener.calls.count()).toBe(1);

      s.destroyForSpecs();
    });

    it("raises the camera changed event on direction changed", function () {
      var s = createScene();

      var spyListener = jasmine.createSpy("listener");
      s.camera.changed.addEventListener(spyListener);

      s.initializeFrame();
      s.render();

      s.camera.lookLeft(
        s.camera.frustum.fov * (s.camera.percentageChanged + 0.1)
      );

      s.initializeFrame();
      s.render();

      expect(spyListener.calls.count()).toBe(1);

      var args = spyListener.calls.allArgs();
      expect(args.length).toEqual(1);
      expect(args[0].length).toEqual(1);
      expect(args[0][0]).toBeGreaterThan(s.camera.percentageChanged);

      s.destroyForSpecs();
    });

    it("raises the camera changed event on position changed", function () {
      var s = createScene();

      var spyListener = jasmine.createSpy("listener");
      s.camera.changed.addEventListener(spyListener);

      s.initializeFrame();
      s.render();

      s.camera.moveLeft(
        s.camera.positionCartographic.height *
          (s.camera.percentageChanged + 0.1)
      );

      s.initializeFrame();
      s.render();

      expect(spyListener.calls.count()).toBe(1);

      var args = spyListener.calls.allArgs();
      expect(args.length).toEqual(1);
      expect(args[0].length).toEqual(1);
      expect(args[0][0]).toBeGreaterThan(s.camera.percentageChanged);

      s.destroyForSpecs();
    });

    it("raises the camera changed event in 2D", function () {
      var s = createScene();
      s.morphTo2D(0);

      var spyListener = jasmine.createSpy("listener");
      s.camera.changed.addEventListener(spyListener);

      s.initializeFrame();
      s.render();

      s.camera.moveLeft(
        s.camera.positionCartographic.height *
          (s.camera.percentageChanged + 0.1)
      );

      s.initializeFrame();
      s.render();

      expect(spyListener.calls.count()).toBe(1);

      var args = spyListener.calls.allArgs();
      expect(args.length).toEqual(1);
      expect(args[0].length).toEqual(1);
      expect(args[0][0]).toBeGreaterThan(s.camera.percentageChanged);

      s.destroyForSpecs();
    });

    it("get maximumAliasedLineWidth", function () {
      var s = createScene();
      expect(s.maximumAliasedLineWidth).toBeGreaterThanOrEqualTo(1);
      s.destroyForSpecs();
    });

    it("get maximumCubeMapSize", function () {
      var s = createScene();
      expect(s.maximumCubeMapSize).toBeGreaterThanOrEqualTo(16);
      s.destroyForSpecs();
    });

    it("does not throw with debugShowCommands", function () {
      var s = createScene();
      if (s.context.drawBuffers) {
        s.debugShowCommands = true;

        var rectangle = Rectangle.fromDegrees(-100.0, 30.0, -90.0, 40.0);

        var rectanglePrimitive = createRectangle(rectangle, 1000.0);
        rectanglePrimitive.appearance.material.uniforms.color = new Color(
          1.0,
          0.0,
          0.0,
          0.5
        );

        var primitives = s.primitives;
        primitives.add(rectanglePrimitive);

        s.camera.setView({ destination: rectangle });

        expect(function () {
          s.renderForSpecs();
        }).not.toThrowRuntimeError();
      }
      s.destroyForSpecs();
    });

    it("does not throw with debugShowFrustums", function () {
      var s = createScene();
      if (s.context.drawBuffers) {
        s.debugShowFrustums = true;

        var rectangle = Rectangle.fromDegrees(-100.0, 30.0, -90.0, 40.0);

        var rectanglePrimitive = createRectangle(rectangle, 1000.0);
        rectanglePrimitive.appearance.material.uniforms.color = new Color(
          1.0,
          0.0,
          0.0,
          0.5
        );

        var primitives = s.primitives;
        primitives.add(rectanglePrimitive);

        s.camera.setView({ destination: rectangle });

        expect(function () {
          s.renderForSpecs();
        }).not.toThrowRuntimeError();
      }
      s.destroyForSpecs();
    });

    it("throws when minimumDisableDepthTestDistance is set less than 0.0", function () {
      expect(function () {
        scene.minimumDisableDepthTestDistance = -1.0;
      }).toThrowDeveloperError();
    });

    it("converts to canvas coordinates", function () {
      var mockPosition = new Cartesian3();
      spyOn(SceneTransforms, "wgs84ToWindowCoordinates");
      scene.cartesianToCanvasCoordinates(mockPosition);

      expect(SceneTransforms.wgs84ToWindowCoordinates).toHaveBeenCalledWith(
        scene,
        mockPosition,
        undefined
      );
    });

    it("converts to canvas coordinates and return it in a variable", function () {
      var result = new Cartesian2();
      var mockPosition = new Cartesian3();
      spyOn(SceneTransforms, "wgs84ToWindowCoordinates");
      scene.cartesianToCanvasCoordinates(mockPosition, result);

      expect(SceneTransforms.wgs84ToWindowCoordinates).toHaveBeenCalledWith(
        scene,
        mockPosition,
        result
      );
    });

    it("Gets imageryLayers", function () {
      var scene = createScene();
      var globe = (scene.globe = new Globe(Ellipsoid.UNIT_SPHERE));
      expect(scene.imageryLayers).toBe(globe.imageryLayers);

      scene.globe = undefined;
      expect(scene.imageryLayers).toBeUndefined();

      scene.destroyForSpecs();
    });

    it("Gets terrainProvider", function () {
      var scene = createScene();
      var globe = (scene.globe = new Globe(Ellipsoid.UNIT_SPHERE));
      expect(scene.terrainProvider).toBe(globe.terrainProvider);

      scene.globe = undefined;
      expect(scene.terrainProvider).toBeUndefined();

      scene.destroyForSpecs();
    });

    it("Sets terrainProvider", function () {
      var scene = createScene();
      var globe = (scene.globe = new Globe(Ellipsoid.UNIT_SPHERE));
      scene.terrainProvider = new CesiumTerrainProvider({
        url: "//terrain/tiles",
      });

      expect(scene.terrainProvider).toBe(globe.terrainProvider);

      scene.globe = undefined;
      expect(function () {
        scene.terrainProvider = new CesiumTerrainProvider({
          url: "//newTerrain/tiles",
        });
      }).not.toThrow();

      scene.destroyForSpecs();
    });

    it("Gets terrainProviderChanged", function () {
      var scene = createScene();
      var globe = (scene.globe = new Globe(Ellipsoid.UNIT_SPHERE));
      expect(scene.terrainProviderChanged).toBe(globe.terrainProviderChanged);

      scene.globe = undefined;
      expect(scene.terrainProviderChanged).toBeUndefined();

      scene.destroyForSpecs();
    });

    it("Sets material", function () {
      var scene = createScene();
      var globe = (scene.globe = new Globe(Ellipsoid.UNIT_SPHERE));
      var material = Material.fromType("ElevationContour");
      globe.material = material;
      expect(globe.material).toBe(material);

      globe.material = undefined;
      expect(globe.material).toBeUndefined();

      scene.destroyForSpecs();
    });

    var scratchTime = new JulianDate();

    it("doesn't render scene if requestRenderMode is enabled", function () {
      var scene = createScene();

      scene.renderForSpecs();

      var lastFrameNumber = scene.frameState.frameNumber;

      scene.requestRenderMode = true;
      scene.maximumRenderTimeChange = undefined;
      scene.renderForSpecs();
      expect(scene.frameState.frameNumber).toEqual(lastFrameNumber);

      scene.destroyForSpecs();
    });

    it("requestRender causes a new frame to be rendered in requestRenderMode", function () {
      var scene = createScene();

      scene.renderForSpecs();

      var lastFrameNumber = scene.frameState.frameNumber;
      expect(scene._renderRequested).toBe(false);

      scene.requestRenderMode = true;
      scene.maximumRenderTimeChange = undefined;

      scene.requestRender();
      expect(scene._renderRequested).toBe(true);

      scene.renderForSpecs();
      expect(scene.frameState.frameNumber).not.toEqual(lastFrameNumber);

      scene.destroyForSpecs();
    });

    it("moving the camera causes a new frame to be rendered in requestRenderMode", function () {
      var scene = createScene();

      scene.renderForSpecs();

      var lastFrameNumber = scene.frameState.frameNumber;
      expect(scene._renderRequested).toBe(false);

      scene.requestRenderMode = true;
      scene.maximumRenderTimeChange = undefined;

      scene.camera.moveLeft();

      scene.renderForSpecs();
      expect(scene.frameState.frameNumber).not.toEqual(lastFrameNumber);

      scene.destroyForSpecs();
    });

    it("changing the camera frustum does not cause continuous rendering in requestRenderMode", function () {
      var scene = createScene();

      scene.renderForSpecs();

      var lastFrameNumber = scene.frameState.frameNumber;
      expect(scene._renderRequested).toBe(false);

      scene.requestRenderMode = true;
      scene.maximumRenderTimeChange = undefined;

      scene.camera.frustum.near *= 1.1;

      // Render once properly
      scene.renderForSpecs();
      expect(scene.frameState.frameNumber).not.toEqual(lastFrameNumber);

      // Render again - but this time nothing should happen.
      lastFrameNumber = scene.frameState.frameNumber;
      scene.renderForSpecs();
      expect(scene.frameState.frameNumber).toEqual(lastFrameNumber);

      scene.destroyForSpecs();
    });

    it("successful completed requests causes a new frame to be rendered in requestRenderMode", function () {
      var scene = createScene();

      scene.renderForSpecs();

      var lastFrameNumber = scene.frameState.frameNumber;
      expect(scene._renderRequested).toBe(false);

      scene.requestRenderMode = true;
      scene.maximumRenderTimeChange = undefined;

      RequestScheduler.requestCompletedEvent.raiseEvent();

      scene.renderForSpecs();

      expect(scene._renderRequested).toBe(true);

      scene.renderForSpecs();
      expect(scene.frameState.frameNumber).not.toEqual(lastFrameNumber);

      scene.destroyForSpecs();
    });

    it("data returning from a web worker causes a new frame to be rendered in requestRenderMode", function () {
      var scene = createScene();

      scene.renderForSpecs();

      var lastFrameNumber = scene.frameState.frameNumber;
      expect(scene._renderRequested).toBe(false);

      scene.requestRenderMode = true;
      scene.maximumRenderTimeChange = undefined;

      TaskProcessor.taskCompletedEvent.raiseEvent();

      scene.renderForSpecs();

      expect(scene._renderRequested).toBe(true);

      scene.renderForSpecs();
      expect(scene.frameState.frameNumber).not.toEqual(lastFrameNumber);

      scene.destroyForSpecs();
    });

    it("Executing an after render function causes a new frame to be rendered in requestRenderMode", function () {
      var scene = createScene();

      scene.renderForSpecs();

      var lastFrameNumber = scene.frameState.frameNumber;
      expect(scene._renderRequested).toBe(false);

      scene.requestRenderMode = true;
      scene.maximumRenderTimeChange = undefined;

      var functionCalled = false;
      scene._frameState.afterRender.push(function () {
        functionCalled = true;
      });

      scene.renderForSpecs();

      expect(functionCalled).toBe(true);
      expect(scene._renderRequested).toBe(true);

      scene.renderForSpecs();
      expect(scene.frameState.frameNumber).not.toEqual(lastFrameNumber);

      scene.destroyForSpecs();
    });

    it("Globe tile loading triggers a new frame to be rendered in requestRenderMode", function () {
      var scene = createScene();

      scene.renderForSpecs();

      var lastFrameNumber = scene.frameState.frameNumber;
      expect(scene._renderRequested).toBe(false);

      scene.requestRenderMode = true;
      scene.maximumRenderTimeChange = undefined;

      var ellipsoid = Ellipsoid.UNIT_SPHERE;
      var globe = new Globe(ellipsoid);
      scene.globe = globe;

      scene.requestRender();
      Object.defineProperty(globe, "tilesLoaded", { value: false });
      scene.renderForSpecs();
      lastFrameNumber = scene.frameState.frameNumber;

      expect(scene._renderRequested).toBe(true);
      scene.renderForSpecs();
      expect(scene.frameState.frameNumber).not.toEqual(lastFrameNumber);

      scene.destroyForSpecs();
    });

    it("Globe imagery updates triggers a new frame to be rendered in requestRenderMode", function () {
      var scene = createScene();

      scene.renderForSpecs();

      var lastFrameNumber = scene.frameState.frameNumber;
      expect(scene._renderRequested).toBe(false);

      scene.requestRenderMode = true;
      scene.maximumRenderTimeChange = undefined;

      var ellipsoid = Ellipsoid.UNIT_SPHERE;
      var globe = new Globe(ellipsoid);
      scene.globe = globe;
      globe.imageryLayersUpdatedEvent.raiseEvent();

      scene.renderForSpecs();

      expect(scene._renderRequested).toBe(true);

      scene.renderForSpecs();
      expect(scene.frameState.frameNumber).not.toEqual(lastFrameNumber);

      scene.destroyForSpecs();
    });

    it("Globe changing terrain providers triggers a new frame to be rendered in requestRenderMode", function () {
      var scene = createScene();

      scene.renderForSpecs();

      var lastFrameNumber = scene.frameState.frameNumber;
      expect(scene._renderRequested).toBe(false);

      scene.requestRenderMode = true;
      scene.maximumRenderTimeChange = undefined;

      var ellipsoid = Ellipsoid.UNIT_SPHERE;
      var globe = new Globe(ellipsoid);
      scene.globe = globe;
      globe.terrainProviderChanged.raiseEvent();

      scene.renderForSpecs();

      expect(scene._renderRequested).toBe(true);

      scene.renderForSpecs();
      expect(scene.frameState.frameNumber).not.toEqual(lastFrameNumber);

      scene.destroyForSpecs();
    });

    it("scene morphing causes a new frame to be rendered in requestRenderMode", function () {
      var scene = createScene();
      scene.renderForSpecs();

      var lastFrameNumber = scene.frameState.frameNumber;
      var lastRenderTime = JulianDate.clone(scene.lastRenderTime, scratchTime);
      expect(lastRenderTime).toBeDefined();
      expect(scene._renderRequested).toBe(false);

      scene.requestRenderMode = true;
      scene.maximumRenderTimeChange = undefined;

      scene.morphTo2D(1.0);
      scene.renderForSpecs(
        JulianDate.addSeconds(lastRenderTime, 0.5, new JulianDate())
      );
      expect(scene.frameState.frameNumber).not.toEqual(lastFrameNumber);

      scene.completeMorph();
      scene.renderForSpecs();
      lastFrameNumber = scene.frameState.frameNumber;

      scene.renderForSpecs();
      expect(scene.frameState.frameNumber).toEqual(lastFrameNumber);
      lastFrameNumber = scene.frameState.frameNumber;
      lastRenderTime = JulianDate.clone(scene.lastRenderTime, scratchTime);

      scene.morphToColumbusView(1.0);
      scene.renderForSpecs(
        JulianDate.addSeconds(lastRenderTime, 0.5, new JulianDate())
      );
      expect(scene.frameState.frameNumber).not.toEqual(lastFrameNumber);

      scene.completeMorph();
      scene.renderForSpecs();
      lastFrameNumber = scene.frameState.frameNumber;

      scene.renderForSpecs();
      expect(scene.frameState.frameNumber).toEqual(lastFrameNumber);
      lastFrameNumber = scene.frameState.frameNumber;
      lastRenderTime = JulianDate.clone(scene.lastRenderTime, scratchTime);

      scene.morphTo3D(1.0);
      scene.renderForSpecs(
        JulianDate.addSeconds(lastRenderTime, 0.5, new JulianDate())
      );
      expect(scene.frameState.frameNumber).not.toEqual(lastFrameNumber);

      scene.completeMorph();
      scene.renderForSpecs();
      lastFrameNumber = scene.frameState.frameNumber;

      scene.renderForSpecs();
      expect(scene.frameState.frameNumber).toEqual(lastFrameNumber);

      scene.destroyForSpecs();
    });

    it("time change exceeding maximumRenderTimeChange causes a new frame to be rendered in requestRenderMode", function () {
      var scene = createScene();

      scene.renderForSpecs();

      var lastFrameNumber = scene.frameState.frameNumber;
      var lastRenderTime = JulianDate.clone(scene.lastRenderTime, scratchTime);
      expect(lastRenderTime).toBeDefined();
      expect(scene._renderRequested).toBe(false);

      scene.requestRenderMode = true;

      scene.renderForSpecs(lastRenderTime);
      expect(scene.frameState.frameNumber).toEqual(lastFrameNumber);

      scene.maximumRenderTimeChange = 100.0;

      scene.renderForSpecs(
        JulianDate.addSeconds(lastRenderTime, 50.0, new JulianDate())
      );
      expect(scene.frameState.frameNumber).toEqual(lastFrameNumber);

      scene.renderForSpecs(
        JulianDate.addSeconds(lastRenderTime, 150.0, new JulianDate())
      );
      expect(scene.frameState.frameNumber).not.toEqual(lastFrameNumber);

      scene.destroyForSpecs();
    });

    it("undefined maximumRenderTimeChange will not cause a new frame to be rendered in requestRenderMode", function () {
      var scene = createScene();

      scene.renderForSpecs();

      var lastFrameNumber = scene.frameState.frameNumber;
      var lastRenderTime = JulianDate.clone(scene.lastRenderTime, scratchTime);
      expect(lastRenderTime).toBeDefined();
      expect(scene._renderRequested).toBe(false);

      scene.requestRenderMode = true;
      scene.maximumRenderTimeChange = undefined;

      var farFuture = JulianDate.addDays(
        lastRenderTime,
        10000,
        new JulianDate()
      );

      scene.renderForSpecs();
      scene.renderForSpecs(farFuture);

      expect(scene.frameState.frameNumber).toEqual(lastFrameNumber);

      scene.destroyForSpecs();
    });

    it("forceRender renders a scene regardless of whether a render was requested", function () {
      var scene = createScene();

      scene.renderForSpecs();

      var lastFrameNumber = scene.frameState.frameNumber;
      expect(scene._renderRequested).toBe(false);

      scene.requestRenderMode = true;
      scene.maximumRenderTimeChange = undefined;

      scene.forceRender();
      expect(scene.frameState.frameNumber).not.toEqual(lastFrameNumber);

      scene.destroyForSpecs();
    });

    function getFrustumCommandsLength(scene, pass) {
      var commandsLength = 0;
      var frustumCommandsList = scene.frustumCommandsList;
      var frustumsLength = frustumCommandsList.length;
      for (var i = 0; i < frustumsLength; ++i) {
        var frustumCommands = frustumCommandsList[i];
        for (var j = 0; j < Pass.NUMBER_OF_PASSES; ++j) {
          if (!defined(pass) || j === pass) {
            commandsLength += frustumCommands.indices[j];
          }
        }
      }
      return commandsLength;
    }

    it("occludes primitive", function () {
      var scene = createScene();
      scene.globe = new Globe(Ellipsoid.WGS84);

      var rectangle = Rectangle.fromDegrees(-100.0, 30.0, -90.0, 40.0);
      var rectanglePrimitive = createRectangle(rectangle, 10);
      scene.primitives.add(rectanglePrimitive);

      scene.camera.setView({
        destination: new Cartesian3(
          -588536.1057451078,
          -10512475.371849751,
          6737159.100747835
        ),
        orientation: new HeadingPitchRoll(
          6.283185307179586,
          -1.5688261558859757,
          0.0
        ),
      });
      scene.renderForSpecs();
      expect(getFrustumCommandsLength(scene)).toBe(1);

      scene.camera.setView({
        destination: new Cartesian3(
          -5754647.167415793,
          14907694.100240812,
          -483807.2406259497
        ),
        orientation: new HeadingPitchRoll(
          6.283185307179586,
          -1.5698869547885104,
          0.0
        ),
      });
      scene.renderForSpecs();
      expect(getFrustumCommandsLength(scene)).toBe(0);

      // Still on opposite side of globe but now show is false, the command should not be occluded anymore
      scene.globe.show = false;
      scene.renderForSpecs();
      expect(getFrustumCommandsLength(scene)).toBe(1);

      scene.destroyForSpecs();
    });

    it("does not occlude if DrawCommand.occlude is false", function () {
      var scene = createScene();
      scene.globe = new Globe(Ellipsoid.WGS84);

      var rectangle = Rectangle.fromDegrees(-100.0, 30.0, -90.0, 40.0);
      var rectanglePrimitive = createRectangle(rectangle, 10);
      scene.primitives.add(rectanglePrimitive);

      scene.renderForSpecs();
      rectanglePrimitive._colorCommands[0].occlude = false;

      scene.camera.setView({
        destination: new Cartesian3(
          -5754647.167415793,
          14907694.100240812,
          -483807.2406259497
        ),
        orientation: new HeadingPitchRoll(
          6.283185307179586,
          -1.5698869547885104,
          0.0
        ),
      });
      scene.renderForSpecs();
      expect(getFrustumCommandsLength(scene)).toBe(1);

      scene.destroyForSpecs();
    });

    it("sets light", function () {
      var scene = createScene();
      var uniformState = scene.context.uniformState;
      var lightDirectionWC = uniformState._lightDirectionWC;
      var sunDirectionWC = uniformState._sunDirectionWC;
      var lightColor = uniformState._lightColor;
      var lightColorHdr = uniformState._lightColorHdr;

      // Default light is a sun light
      scene.renderForSpecs();
      expect(lightDirectionWC).toEqual(sunDirectionWC);
      expect(lightColor).toEqual(new Cartesian3(1.0, 1.0, 1.0));
      expect(lightColorHdr).toEqual(new Cartesian3(2.0, 2.0, 2.0));

      // Test directional light
      scene.light = new DirectionalLight({
        direction: new Cartesian3(1.0, 0.0, 0.0),
        color: Color.RED,
        intensity: 2.0,
      });
      scene.renderForSpecs();
      expect(lightDirectionWC).toEqual(new Cartesian3(-1.0, 0.0, 0.0)); // Negated because the uniform is the direction to the light, not from the light
      expect(lightColor).toEqual(new Cartesian3(1.0, 0.0, 0.0));
      expect(lightColorHdr).toEqual(new Cartesian3(2.0, 0.0, 0.0));

      // Test sun light
      scene.light = new SunLight({
        color: Color.BLUE,
        intensity: 0.5,
      });
      scene.renderForSpecs();
      expect(lightDirectionWC).toEqual(sunDirectionWC);
      expect(lightColor).toEqual(new Cartesian3(0.0, 0.0, 0.5));
      expect(lightColorHdr).toEqual(new Cartesian3(0.0, 0.0, 0.5));

      // Test light set to undefined
      scene.light = undefined;
      scene.renderForSpecs();
      expect(lightDirectionWC).toEqual(sunDirectionWC);
      expect(lightColor).toEqual(new Cartesian3(1.0, 1.0, 1.0));
      expect(lightColorHdr).toEqual(new Cartesian3(2.0, 2.0, 2.0));

      scene.destroyForSpecs();
    });

    function updateGlobeUntilDone(scene) {
      return pollToPromise(function () {
        scene.renderForSpecs();
        return scene.globe.tilesLoaded;
      });
    }

    it("detects when camera is underground", function () {
      var scene = createScene();
      var globe = new Globe();
      scene.globe = globe;

      scene.camera.setView({
        destination: new Rectangle(0.0001, 0.0001, 0.003, 0.003),
      });

      return updateGlobeUntilDone(scene)
        .then(function () {
          expect(scene.cameraUnderground).toBe(false);

          // Look underground
          scene.camera.setView({
            destination: new Cartesian3(
              -746658.0557573901,
              -5644191.0002196245,
              2863585.099969967
            ),
            orientation: new HeadingPitchRoll(
              0.3019699121236403,
              0.07316306869231592,
              0.0007089903642230055
            ),
          });
          return updateGlobeUntilDone(scene);
        })
        .then(function () {
          expect(scene.cameraUnderground).toBe(true);
          scene.destroyForSpecs();
        });
    });

    it("detects that camera is above ground if globe is undefined", function () {
      var scene = createScene();
      scene.renderForSpecs();
      expect(scene.cameraUnderground).toBe(false);
      scene.destroyForSpecs();
    });

    it("detects that camera is above ground if scene mode is 2D", function () {
      var scene = createScene();
      var globe = new Globe();
      scene.globe = globe;
      scene.morphTo2D(0.0);
      expect(scene.cameraUnderground).toBe(false);
      scene.destroyForSpecs();
    });

    it("detects that camera is above ground if scene mode is morphing", function () {
      var scene = createScene();
      var globe = new Globe();
      scene.globe = globe;
      scene.morphTo2D(1.0);
      expect(scene.cameraUnderground).toBe(false);
      scene.destroyForSpecs();
    });

    it("detects that camera is underground in Columbus View", function () {
      var scene = createScene();
      var globe = new Globe();
      scene.globe = globe;

      // Look underground
      scene.camera.setView({
        destination: new Cartesian3(
          -4643042.379120885,
          4314056.579506199,
          -451828.8968118975
        ),
        orientation: new HeadingPitchRoll(
          6.283185307179586,
          -0.7855491933100796,
          6.283185307179586
        ),
      });
      scene.morphToColumbusView(0.0);

      return updateGlobeUntilDone(scene).then(function () {
        expect(scene.cameraUnderground).toBe(true);
        scene.destroyForSpecs();
      });
    });

    it("does not occlude primitives when camera is underground", function () {
      var scene = createScene();
      var globe = new Globe();
      scene.globe = globe;

      // A primitive at height -25000.0 is less than the minor axis for WGS84 and will get culled unless the camera is underground
      var center = Cartesian3.fromRadians(
        2.3929070618374535,
        -0.07149851443375346,
        -25000.0,
        globe.ellipsoid
      );
      var radius = 10.0;

      var command = new DrawCommand({
        shaderProgram: simpleShaderProgram,
        renderState: simpleRenderState,
        pass: Pass.OPAQUE,
        boundingVolume: new BoundingSphere(center, radius),
      });

      scene.primitives.add(new CommandMockPrimitive(command));

      spyOn(DrawCommand.prototype, "execute"); // Don't execute any commands, just watch what gets added to the frustum commands list

      return updateGlobeUntilDone(scene)
        .then(function () {
          expect(getFrustumCommandsLength(scene, Pass.OPAQUE)).toBe(0);

          // Look underground at the primitive
          scene.camera.setView({
            destination: new Cartesian3(
              -4643042.379120885,
              4314056.579506199,
              -451828.8968118975
            ),
            orientation: new HeadingPitchRoll(
              6.283185307179586,
              -0.7855491933100796,
              6.283185307179586
            ),
          });
          return updateGlobeUntilDone(scene);
        })
        .then(function () {
          expect(getFrustumCommandsLength(scene, Pass.OPAQUE)).toBe(1);
          scene.destroyForSpecs();
        });
    });

    it("does not occlude primitives when the globe is translucent", function () {
      var scene = createScene();
      var globe = new Globe();
      scene.globe = globe;

      // A primitive at height -25000.0 is less than the minor axis for WGS84 and will get culled unless the globe is translucent
      var center = Cartesian3.fromRadians(
        2.3929070618374535,
        -0.07149851443375346,
        -25000.0,
        globe.ellipsoid
      );
      var radius = 10.0;

      var command = new DrawCommand({
        shaderProgram: simpleShaderProgram,
        renderState: simpleRenderState,
        pass: Pass.OPAQUE,
        boundingVolume: new BoundingSphere(center, radius),
      });

      scene.primitives.add(new CommandMockPrimitive(command));

      spyOn(DrawCommand.prototype, "execute"); // Don't execute any commands, just watch what gets added to the frustum commands list

      scene.renderForSpecs();
      expect(getFrustumCommandsLength(scene, Pass.OPAQUE)).toBe(0);

      scene.globe.translucency.enabled = true;
      scene.globe.translucency.frontFaceAlpha = 0.5;

      scene.renderForSpecs();
      expect(getFrustumCommandsLength(scene, Pass.OPAQUE)).toBe(1);

      scene.destroyForSpecs();
    });

    it("does not render environment when camera is underground and translucency is disabled", function () {
      var scene = createScene();
      var globe = new Globe();
      scene.globe = globe;
      scene.sun = new Sun();

      // Look underground at the sun
      scene.camera.setView({
        destination: new Cartesian3(
          2838477.9315700866,
          -4939120.816857662,
          1978094.4576285738
        ),
        orientation: new HeadingPitchRoll(
          5.955798516387474,
          -1.0556025616093283,
          0.39098563693868016
        ),
      });

      return updateGlobeUntilDone(scene).then(function () {
        var time = JulianDate.fromIso8601(
          "2020-04-25T03:07:26.04924034334544558Z"
        );
        globe.translucency.enabled = true;
        globe.translucency.frontFaceAlpha = 0.5;
        scene.renderForSpecs(time);

        expect(scene.environmentState.isSunVisible).toBe(true);
        globe.translucency.enabled = false;
        scene.renderForSpecs(time);
        expect(scene.environmentState.isSunVisible).toBe(false);
        scene.destroyForSpecs(time);
      });
    });

    it("renders globe with translucency", function () {
      var scene = createScene();
      var globe = new Globe();
      scene.globe = globe;

      scene.camera.setView({
        destination: new Cartesian3(
          2764681.3022502237,
          -20999839.371941473,
          14894754.464869803
        ),
        orientation: new HeadingPitchRoll(
          6.283185307179586,
          -1.5687983447998315,
          0
        ),
      });

      return updateGlobeUntilDone(scene).then(function () {
        var opaqueColor;
        expect(scene).toRenderAndCall(function (rgba) {
          opaqueColor = rgba;
        });

        globe.translucency.enabled = true;
        globe.translucency.frontFaceAlpha = 0.5;

        expect(scene).toRenderAndCall(function (rgba) {
          expect(rgba).not.toEqual(opaqueColor);
          scene.destroyForSpecs();
        });
      });
    });

    it("renders ground primitive on translucent globe", function () {
      var scene = createScene();
      var globe = new Globe();
      scene.globe = globe;
      globe.baseColor = Color.BLACK;
      globe.translucency.enabled = true;
      globe.translucency.frontFaceAlpha = 0.5;

      scene.camera.setView({
        destination: new Cartesian3(
          -557278.4840232887,
          -6744284.200717078,
          2794079.461722868
        ),
        orientation: new HeadingPitchRoll(
          6.283185307179586,
          -1.5687983448015541,
          0
        ),
      });

      var redRectangleInstance = new GeometryInstance({
        geometry: new RectangleGeometry({
          rectangle: Rectangle.fromDegrees(-110.0, 20.0, -80.0, 25.0),
          vertexFormat: PerInstanceColorAppearance.VERTEX_FORMAT,
        }),
        attributes: {
          color: ColorGeometryInstanceAttribute.fromColor(
            new Color(1.0, 0.0, 0.0, 0.5)
          ),
        },
      });

      scene.primitives.add(
        new GroundPrimitive({
          geometryInstances: [redRectangleInstance],
          appearance: new PerInstanceColorAppearance({
            closed: true,
          }),
          asynchronous: false,
        })
      );

      return updateGlobeUntilDone(scene).then(function () {
        expect(scene).toRenderAndCall(function (rgba) {
          expect(rgba[0]).toBeGreaterThan(0);
          scene.destroyForSpecs();
        });
      });
    });

    it("picks ground primitive on translucent globe", function () {
      var scene = createScene();
      var globe = new Globe();
      scene.globe = globe;
      globe.baseColor = Color.BLACK;
      globe.translucency.enabled = true;
      globe.translucency.frontFaceAlpha = 0.5;

      scene.camera.setView({
        destination: new Cartesian3(
          -557278.4840232887,
          -6744284.200717078,
          2794079.461722868
        ),
        orientation: new HeadingPitchRoll(
          6.283185307179586,
          -1.5687983448015541,
          0
        ),
      });

      var redRectangleInstance = new GeometryInstance({
        geometry: new RectangleGeometry({
          rectangle: Rectangle.fromDegrees(-110.0, 20.0, -80.0, 25.0),
          vertexFormat: PerInstanceColorAppearance.VERTEX_FORMAT,
        }),
        attributes: {
          color: ColorGeometryInstanceAttribute.fromColor(
            new Color(1.0, 0.0, 0.0, 0.5)
          ),
        },
      });

      var primitive = scene.primitives.add(
        new GroundPrimitive({
          geometryInstances: [redRectangleInstance],
          appearance: new PerInstanceColorAppearance({
            closed: true,
          }),
          asynchronous: false,
        })
      );

      return updateGlobeUntilDone(scene).then(function () {
        expect(scene).toPickPrimitive(primitive);
        scene.destroyForSpecs();
      });
    });
  },

  "WebGL"
);
