import { BoundingSphere } from "../../Source/Cesium.js";
import { BoxGeometry } from "../../Source/Cesium.js";
import { Cartesian3 } from "../../Source/Cesium.js";
import { ColorGeometryInstanceAttribute } from "../../Source/Cesium.js";
import { ComponentDatatype } from "../../Source/Cesium.js";
import { CylinderGeometry } from "../../Source/Cesium.js";
import { defined } from "../../Source/Cesium.js";
import { DistanceDisplayConditionGeometryInstanceAttribute } from "../../Source/Cesium.js";
import { Ellipsoid } from "../../Source/Cesium.js";
import { Geometry } from "../../Source/Cesium.js";
import { GeometryAttribute } from "../../Source/Cesium.js";
import { GeometryInstance } from "../../Source/Cesium.js";
import { GeometryInstanceAttribute } from "../../Source/Cesium.js";
import { HeadingPitchRange } from "../../Source/Cesium.js";
import { Math as CesiumMath } from "../../Source/Cesium.js";
import { Matrix4 } from "../../Source/Cesium.js";
import { PerspectiveFrustum } from "../../Source/Cesium.js";
import { PolygonGeometry } from "../../Source/Cesium.js";
import { PrimitiveType } from "../../Source/Cesium.js";
import { Rectangle } from "../../Source/Cesium.js";
import { RectangleGeometry } from "../../Source/Cesium.js";
import { ShowGeometryInstanceAttribute } from "../../Source/Cesium.js";
import { Transforms } from "../../Source/Cesium.js";
import { Camera } from "../../Source/Cesium.js";
import { MaterialAppearance } from "../../Source/Cesium.js";
import { PerInstanceColorAppearance } from "../../Source/Cesium.js";
import { Primitive } from "../../Source/Cesium.js";
import { SceneMode } from "../../Source/Cesium.js";
import BadGeometry from "../BadGeometry.js";
import createContext from "../createContext.js";
import createFrameState from "../createFrameState.js";
import createScene from "../createScene.js";
import pollToPromise from "../pollToPromise.js";

describe(
  "Scene/Primitive",
  function () {
    var scene;
    var context;

    var frameStateContext;
    var frameState;

    var ellipsoid;

    var rectangle1;
    var rectangle2;

    var rectangleInstance1;
    var rectangleInstance2;

    var primitive;

    beforeAll(function () {
      scene = createScene();
      scene.primitives.destroyPrimitives = false;
      context = scene.context;
      ellipsoid = Ellipsoid.WGS84;

      frameStateContext = createContext();
    });

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

    beforeEach(function () {
      scene.morphTo3D(0);

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

      scene.frameState.passes.render = true;
      scene.frameState.passes.pick = false;

      // Mock frameState, separate from scene.frameState, used for test that call primitive.update directly
      frameState = createFrameState(frameStateContext);

      rectangle1 = Rectangle.fromDegrees(-80.0, 20.0, -70.0, 30.0);
      rectangle2 = Rectangle.fromDegrees(70.0, 20.0, 80.0, 30.0);

      var translation = Cartesian3.multiplyByScalar(
        Cartesian3.normalize(
          ellipsoid.cartographicToCartesian(Rectangle.center(rectangle1)),
          new Cartesian3()
        ),
        2.0,
        new Cartesian3()
      );
      rectangleInstance1 = new GeometryInstance({
        geometry: new RectangleGeometry({
          vertexFormat: PerInstanceColorAppearance.VERTEX_FORMAT,
          ellipsoid: ellipsoid,
          rectangle: rectangle1,
        }),
        modelMatrix: Matrix4.fromTranslation(translation, new Matrix4()),
        id: "rectangle1",
        attributes: {
          color: new ColorGeometryInstanceAttribute(1.0, 1.0, 0.0, 1.0),
          show: new ShowGeometryInstanceAttribute(true),
        },
      });

      translation = Cartesian3.multiplyByScalar(
        Cartesian3.normalize(
          ellipsoid.cartographicToCartesian(Rectangle.center(rectangle2)),
          new Cartesian3()
        ),
        3.0,
        new Cartesian3()
      );
      rectangleInstance2 = new GeometryInstance({
        geometry: new RectangleGeometry({
          vertexFormat: PerInstanceColorAppearance.VERTEX_FORMAT,
          ellipsoid: ellipsoid,
          rectangle: rectangle2,
        }),
        modelMatrix: Matrix4.fromTranslation(translation, new Matrix4()),
        id: "rectangle2",
        attributes: {
          color: new ColorGeometryInstanceAttribute(0.0, 1.0, 1.0, 1.0),
          show: new ShowGeometryInstanceAttribute(true),
        },
      });
    });

    afterEach(function () {
      scene.primitives.removeAll();
      primitive = primitive && !primitive.isDestroyed() && primitive.destroy();
    });

    it("default constructs", function () {
      primitive = new Primitive();
      expect(primitive.geometryInstances).not.toBeDefined();
      expect(primitive.appearance).not.toBeDefined();
      expect(primitive.depthFailAppearance).not.toBeDefined();
      expect(primitive.modelMatrix).toEqual(Matrix4.IDENTITY);
      expect(primitive.show).toEqual(true);
      expect(primitive.vertexCacheOptimize).toEqual(false);
      expect(primitive.interleave).toEqual(false);
      expect(primitive.compressVertices).toEqual(true);
      expect(primitive.releaseGeometryInstances).toEqual(true);
      expect(primitive.allowPicking).toEqual(true);
      expect(primitive.cull).toEqual(true);
      expect(primitive.asynchronous).toEqual(true);
      expect(primitive.debugShowBoundingVolume).toEqual(false);
    });

    it("Constructs with options", function () {
      var geometryInstances = {};
      var appearance = {};
      var depthFailAppearance = {};
      var modelMatrix = Matrix4.fromUniformScale(5.0);

      primitive = new Primitive({
        geometryInstances: geometryInstances,
        appearance: appearance,
        depthFailAppearance: depthFailAppearance,
        modelMatrix: modelMatrix,
        show: false,
        vertexCacheOptimize: true,
        interleave: true,
        compressVertices: false,
        releaseGeometryInstances: false,
        allowPicking: false,
        cull: false,
        asynchronous: false,
        debugShowBoundingVolume: true,
      });

      expect(primitive.geometryInstances).toEqual(geometryInstances);
      expect(primitive.appearance).toEqual(appearance);
      expect(primitive.depthFailAppearance).toEqual(depthFailAppearance);
      expect(primitive.modelMatrix).toEqual(modelMatrix);
      expect(primitive.show).toEqual(false);
      expect(primitive.vertexCacheOptimize).toEqual(true);
      expect(primitive.interleave).toEqual(true);
      expect(primitive.compressVertices).toEqual(false);
      expect(primitive.releaseGeometryInstances).toEqual(false);
      expect(primitive.allowPicking).toEqual(false);
      expect(primitive.cull).toEqual(false);
      expect(primitive.asynchronous).toEqual(false);
      expect(primitive.debugShowBoundingVolume).toEqual(true);
    });

    it("releases geometry instances when releaseGeometryInstances is true", function () {
      primitive = new Primitive({
        geometryInstances: [rectangleInstance1, rectangleInstance2],
        appearance: new PerInstanceColorAppearance(),
        releaseGeometryInstances: true,
        asynchronous: false,
      });

      expect(primitive.geometryInstances).toBeDefined();
      scene.primitives.add(primitive);
      scene.renderForSpecs();
      expect(primitive.geometryInstances).not.toBeDefined();
    });

    it("does not release geometry instances when releaseGeometryInstances is false", function () {
      primitive = new Primitive({
        geometryInstances: [rectangleInstance1, rectangleInstance2],
        appearance: new PerInstanceColorAppearance(),
        releaseGeometryInstances: false,
        asynchronous: false,
      });

      expect(primitive.geometryInstances).toBeDefined();
      scene.primitives.add(primitive);
      scene.renderForSpecs();
      expect(primitive.geometryInstances).toBeDefined();
    });

    it("adds afterRender promise to frame state", function () {
      primitive = new Primitive({
        geometryInstances: [rectangleInstance1, rectangleInstance2],
        appearance: new PerInstanceColorAppearance(),
        releaseGeometryInstances: false,
        asynchronous: false,
      });

      scene.primitives.add(primitive);
      scene.renderForSpecs();

      return primitive.readyPromise.then(function (param) {
        expect(param.ready).toBe(true);
      });
    });

    it("does not render when geometryInstances is an empty array", function () {
      primitive = new Primitive({
        geometryInstances: [],
        appearance: new PerInstanceColorAppearance(),
        asynchronous: false,
      });

      frameState.commandList.length = 0;
      primitive.update(frameState);
      expect(frameState.commandList.length).toEqual(0);
    });

    it("does not render when show is false", function () {
      primitive = new Primitive({
        geometryInstances: [rectangleInstance1, rectangleInstance2],
        appearance: new PerInstanceColorAppearance(),
        asynchronous: false,
      });

      primitive.update(frameState);
      expect(frameState.commandList.length).toBeGreaterThan(0);

      frameState.commandList.length = 0;
      primitive.show = false;
      primitive.update(frameState);
      expect(frameState.commandList.length).toEqual(0);
    });

    it("does not render other than for the color or pick pass", function () {
      primitive = new Primitive({
        geometryInstances: [rectangleInstance1, rectangleInstance2],
        appearance: new PerInstanceColorAppearance(),
        asynchronous: false,
      });

      frameState.passes.render = false;
      frameState.passes.pick = false;

      primitive.update(frameState);
      expect(frameState.commandList.length).toEqual(0);
    });

    it("does not render when scene3DOnly is true and the scene mode is SCENE2D", function () {
      primitive = new Primitive({
        geometryInstances: [rectangleInstance1, rectangleInstance2],
        appearance: new PerInstanceColorAppearance(),
        asynchronous: false,
      });

      frameState.mode = SceneMode.SCENE2D;
      frameState.scene3DOnly = true;

      primitive.update(frameState);
      expect(frameState.commandList.length).toEqual(0);
    });

    it("does not render when scene3DOnly is true and the scene mode is COLUMBUS_VIEW", function () {
      primitive = new Primitive({
        geometryInstances: [rectangleInstance1, rectangleInstance2],
        appearance: new PerInstanceColorAppearance(),
        asynchronous: false,
      });

      frameState.mode = SceneMode.COLUMBUS_VIEW;
      frameState.scene3DOnly = true;

      primitive.update(frameState);
      expect(frameState.commandList.length).toEqual(0);
    });

    it("renders in two passes for closed, translucent geometry", function () {
      primitive = new Primitive({
        geometryInstances: new GeometryInstance({
          geometry: BoxGeometry.fromDimensions({
            vertexFormat: PerInstanceColorAppearance.VERTEX_FORMAT,
            dimensions: new Cartesian3(500000.0, 500000.0, 500000.0),
          }),
          id: "box",
          attributes: {
            color: new ColorGeometryInstanceAttribute(1.0, 1.0, 0.0, 0.5),
          },
        }),
        appearance: new PerInstanceColorAppearance({
          closed: true,
          translucent: true,
        }),
        asynchronous: false,
      });

      var frameState = scene.frameState;
      frameState.commandList.length = 0;

      // set scene3DOnly to true so that the geometry is not split due to the IDL
      frameState.scene3DOnly = true;
      scene.primitives.add(primitive);
      scene.render();
      expect(frameState.commandList.length).toEqual(2);
    });

    function verifyPrimitiveRender(primitive, rectangle) {
      scene.primitives.removeAll();
      if (defined(rectangle)) {
        scene.camera.setView({ destination: rectangle });
      }
      expect(scene).toRender([0, 0, 0, 255]);

      scene.primitives.add(primitive);
      expect(scene).notToRender([0, 0, 0, 255]);
    }

    it("renders in Columbus view when scene3DOnly is false", function () {
      scene.frameState.scene3DOnly = false;
      primitive = new Primitive({
        geometryInstances: [rectangleInstance1, rectangleInstance2],
        appearance: new PerInstanceColorAppearance(),
        asynchronous: false,
      });

      scene.morphToColumbusView(0);
      verifyPrimitiveRender(primitive, rectangle1);
      verifyPrimitiveRender(primitive, rectangle2);
    });

    it("renders in 2D when scene3DOnly is false", function () {
      scene.frameState.scene3DOnly = false;
      primitive = new Primitive({
        geometryInstances: [rectangleInstance1, rectangleInstance2],
        appearance: new PerInstanceColorAppearance(),
        asynchronous: false,
      });

      scene.morphTo2D(0);
      verifyPrimitiveRender(primitive, rectangle1);
      verifyPrimitiveRender(primitive, rectangle2);
    });

    it("renders RTC", function () {
      var dimensions = new Cartesian3(400.0, 300.0, 500.0);
      var positionOnEllipsoid = Cartesian3.fromDegrees(-105.0, 45.0);
      var boxModelMatrix = Matrix4.multiplyByTranslation(
        Transforms.eastNorthUpToFixedFrame(positionOnEllipsoid),
        new Cartesian3(0.0, 0.0, dimensions.z * 0.5),
        new Matrix4()
      );

      var boxGeometry = BoxGeometry.createGeometry(
        BoxGeometry.fromDimensions({
          vertexFormat: PerInstanceColorAppearance.VERTEX_FORMAT,
          dimensions: dimensions,
        })
      );

      var positions = boxGeometry.attributes.position.values;
      var newPositions = new Float32Array(positions.length);
      for (var i = 0; i < positions.length; ++i) {
        newPositions[i] = positions[i];
      }
      boxGeometry.attributes.position.values = newPositions;
      boxGeometry.attributes.position.componentDatatype =
        ComponentDatatype.FLOAT;

      BoundingSphere.transform(
        boxGeometry.boundingSphere,
        boxModelMatrix,
        boxGeometry.boundingSphere
      );

      var boxGeometryInstance = new GeometryInstance({
        geometry: boxGeometry,
        attributes: {
          color: new ColorGeometryInstanceAttribute(1.0, 0.0, 0.0, 0.5),
        },
      });

      var primitive = new Primitive({
        geometryInstances: boxGeometryInstance,
        appearance: new PerInstanceColorAppearance({
          closed: true,
        }),
        asynchronous: false,
        allowPicking: false,
        rtcCenter: boxGeometry.boundingSphere.center,
      });

      // create test camera
      var camera = scene.camera;
      var testCamera = new Camera(scene);
      testCamera.viewBoundingSphere(boxGeometry.boundingSphere);
      scene.camera = testCamera;

      scene.frameState.scene3DOnly = true;
      verifyPrimitiveRender(primitive);

      scene.camera = camera;
    });

    it("renders with depth fail appearance", function () {
      var rect = Rectangle.fromDegrees(-1.0, -1.0, 1.0, 1.0);
      var translation = Cartesian3.multiplyByScalar(
        Cartesian3.normalize(
          ellipsoid.cartographicToCartesian(Rectangle.center(rect)),
          new Cartesian3()
        ),
        100.0,
        new Cartesian3()
      );
      var rectInstance = new GeometryInstance({
        geometry: new RectangleGeometry({
          vertexFormat: PerInstanceColorAppearance.VERTEX_FORMAT,
          ellipsoid: ellipsoid,
          rectangle: rect,
        }),
        modelMatrix: Matrix4.fromTranslation(translation, new Matrix4()),
        id: "rect",
        attributes: {
          color: new ColorGeometryInstanceAttribute(1.0, 1.0, 0.0, 1.0),
        },
      });
      var p0 = new Primitive({
        geometryInstances: rectInstance,
        appearance: new PerInstanceColorAppearance({
          translucent: false,
        }),
        asynchronous: false,
      });

      var rectInstance2 = new GeometryInstance({
        geometry: new RectangleGeometry({
          vertexFormat: PerInstanceColorAppearance.VERTEX_FORMAT,
          ellipsoid: ellipsoid,
          rectangle: rect,
        }),
        id: "rect2",
        attributes: {
          color: new ColorGeometryInstanceAttribute(1.0, 0.0, 0.0, 1.0),
          depthFailColor: new ColorGeometryInstanceAttribute(
            1.0,
            0.0,
            1.0,
            1.0
          ),
        },
      });
      var p1 = new Primitive({
        geometryInstances: rectInstance2,
        appearance: new PerInstanceColorAppearance({
          translucent: false,
        }),
        depthFailAppearance: new PerInstanceColorAppearance({
          translucent: false,
        }),
        asynchronous: false,
      });

      scene.primitives.add(p0);
      scene.primitives.add(p1);
      scene.camera.setView({ destination: rect });
      scene.renderForSpecs();

      expect(scene).toRender([255, 0, 255, 255]);
    });

    it("pick with depth fail appearance", function () {
      var rect = Rectangle.fromDegrees(-1.0, -1.0, 1.0, 1.0);
      var translation = Cartesian3.multiplyByScalar(
        Cartesian3.normalize(
          ellipsoid.cartographicToCartesian(Rectangle.center(rect)),
          new Cartesian3()
        ),
        100.0,
        new Cartesian3()
      );
      var rectInstance = new GeometryInstance({
        geometry: new RectangleGeometry({
          vertexFormat: PerInstanceColorAppearance.VERTEX_FORMAT,
          ellipsoid: ellipsoid,
          rectangle: rect,
        }),
        modelMatrix: Matrix4.fromTranslation(translation, new Matrix4()),
        id: "rect",
        attributes: {
          color: new ColorGeometryInstanceAttribute(1.0, 1.0, 0.0, 1.0),
        },
      });
      var p0 = new Primitive({
        geometryInstances: rectInstance,
        appearance: new PerInstanceColorAppearance({
          translucent: false,
        }),
        asynchronous: false,
      });

      var rectInstance2 = new GeometryInstance({
        geometry: new RectangleGeometry({
          vertexFormat: PerInstanceColorAppearance.VERTEX_FORMAT,
          ellipsoid: ellipsoid,
          rectangle: rect,
        }),
        id: "rect2",
        attributes: {
          color: new ColorGeometryInstanceAttribute(1.0, 0.0, 0.0, 1.0),
          depthFailColor: new ColorGeometryInstanceAttribute(
            1.0,
            0.0,
            1.0,
            1.0
          ),
        },
      });
      var p1 = new Primitive({
        geometryInstances: rectInstance2,
        appearance: new PerInstanceColorAppearance({
          translucent: false,
        }),
        depthFailAppearance: new PerInstanceColorAppearance({
          translucent: false,
        }),
        asynchronous: false,
      });

      scene.primitives.add(p0);
      scene.primitives.add(p1);
      scene.camera.setView({ destination: rect });
      scene.renderForSpecs();

      expect(scene).toPickAndCall(function (result) {
        expect(result.primitive).toEqual(p1);
        expect(result.id).toEqual("rect2");
      });
    });

    it("RTC throws with more than one instance", function () {
      expect(function () {
        return new Primitive({
          geometryInstances: [rectangleInstance1, rectangleInstance2],
          appearance: new PerInstanceColorAppearance({
            closed: true,
          }),
          asynchronous: false,
          allowPicking: false,
          rtcCenter: Cartesian3.ZERO,
        });
      }).toThrowDeveloperError();
    });

    it("RTC throws if the scene is not 3D only", function () {
      scene.frameState.scene3DOnly = false;
      var primitive = new Primitive({
        geometryInstances: rectangleInstance1,
        appearance: new PerInstanceColorAppearance({
          closed: true,
        }),
        asynchronous: false,
        allowPicking: false,
        rtcCenter: Cartesian3.ZERO,
      });

      expect(function () {
        verifyPrimitiveRender(primitive);
      }).toThrowDeveloperError();
    });

    it("updates model matrix for one instance in 3D", function () {
      primitive = new Primitive({
        geometryInstances: rectangleInstance1,
        appearance: new PerInstanceColorAppearance(),
        asynchronous: false,
      });

      primitive.update(frameState);
      var commands = frameState.commandList;
      expect(commands.length).toEqual(1);
      expect(commands[0].modelMatrix).toEqual(primitive.modelMatrix);

      var modelMatrix = Matrix4.fromUniformScale(10.0);
      primitive.modelMatrix = modelMatrix;

      commands.length = 0;
      primitive.update(frameState);
      expect(commands.length).toEqual(1);
      expect(commands[0].modelMatrix).toEqual(modelMatrix);
    });

    it("updates model matrix for more than one instance in 3D with equal model matrices in 3D only scene", function () {
      var modelMatrix = Matrix4.fromUniformScale(2.0);
      rectangleInstance1.modelMatrix = modelMatrix;
      rectangleInstance2.modelMatrix = modelMatrix;

      primitive = new Primitive({
        geometryInstances: [rectangleInstance1, rectangleInstance2],
        appearance: new PerInstanceColorAppearance(),
        asynchronous: false,
      });

      frameState.scene3DOnly = true;

      var commands = frameState.commandList;
      commands.length = 0;
      primitive.update(frameState);
      expect(commands.length).toEqual(1);
      expect(commands[0].modelMatrix).toEqual(modelMatrix);

      modelMatrix = Matrix4.fromUniformScale(10.0);
      primitive.modelMatrix = modelMatrix;

      commands.length = 0;
      primitive.update(frameState);
      expect(commands.length).toEqual(1);
      expect(commands[0].modelMatrix).toEqual(modelMatrix);
    });

    it("computes model matrix when given one for a single instance and for the primitive in 3D only", function () {
      var instanceModelMatrix = Matrix4.fromUniformScale(2.0);

      var dimensions = new Cartesian3(400000.0, 300000.0, 500000.0);
      var positionOnEllipsoid = Cartesian3.fromDegrees(-105.0, 45.0);
      var primitiveModelMatrix = Matrix4.multiplyByTranslation(
        Transforms.eastNorthUpToFixedFrame(positionOnEllipsoid),
        new Cartesian3(0.0, 0.0, dimensions.z * 0.5),
        new Matrix4()
      );

      var boxGeometry = BoxGeometry.fromDimensions({
        vertexFormat: PerInstanceColorAppearance.VERTEX_FORMAT,
        dimensions: dimensions,
      });
      var boxGeometryInstance = new GeometryInstance({
        geometry: boxGeometry,
        modelMatrix: instanceModelMatrix,
        attributes: {
          color: new ColorGeometryInstanceAttribute(1.0, 0.0, 0.0, 1.0),
        },
      });
      primitive = new Primitive({
        geometryInstances: boxGeometryInstance,
        modelMatrix: primitiveModelMatrix,
        appearance: new PerInstanceColorAppearance({
          translucent: false,
          closed: true,
        }),
        asynchronous: false,
      });

      var expectedModelMatrix = Matrix4.multiplyTransformation(
        primitiveModelMatrix,
        instanceModelMatrix,
        new Matrix4()
      );

      frameState.scene3DOnly = true;

      var commands = frameState.commandList;
      commands.length = 0;
      primitive.update(frameState);
      expect(commands.length).toEqual(1);
      expect(commands[0].modelMatrix).toEqual(expectedModelMatrix);
    });

    it("update model matrix throws in Columbus view", function () {
      primitive = new Primitive({
        geometryInstances: [rectangleInstance1, rectangleInstance2],
        appearance: new PerInstanceColorAppearance(),
        asynchronous: false,
      });

      frameState.mode = SceneMode.COLUMBUS_VIEW;
      frameState.scene3DOnly = false;

      var commands = frameState.commandList;
      commands.length = 0;
      primitive.update(frameState);
      expect(commands.length).toEqual(1);
      expect(commands[0].modelMatrix).toEqual(Matrix4.IDENTITY);

      var modelMatrix = Matrix4.fromUniformScale(10.0);
      primitive.modelMatrix = modelMatrix;

      commands.length = 0;
      expect(function () {
        primitive.update(frameState);
      }).toThrowDeveloperError();
    });

    it("update model matrix throws in 2D", function () {
      primitive = new Primitive({
        geometryInstances: [rectangleInstance1, rectangleInstance2],
        appearance: new PerInstanceColorAppearance(),
        asynchronous: false,
      });

      frameState.mode = SceneMode.SCENE2D;
      frameState.scene3DOnly = false;

      var commands = frameState.commandList;
      primitive.update(frameState);
      expect(commands.length).toEqual(1);
      expect(commands[0].modelMatrix).toEqual(Matrix4.IDENTITY);

      var modelMatrix = Matrix4.fromUniformScale(10.0);
      primitive.modelMatrix = modelMatrix;

      commands.length = 0;
      expect(function () {
        primitive.update(frameState);
      }).toThrowDeveloperError();
    });

    it("renders bounding volume with debugShowBoundingVolume", function () {
      primitive = new Primitive({
        geometryInstances: rectangleInstance1,
        appearance: new PerInstanceColorAppearance(),
        asynchronous: false,
        debugShowBoundingVolume: true,
      });

      scene.primitives.add(primitive);
      scene.camera.setView({ destination: rectangle1 });
      expect(scene).toRenderAndCall(function (rgba) {
        expect(rgba[0]).not.toEqual(0);
        expect(rgba[1]).toBeGreaterThanOrEqualTo(0);
        expect(rgba[2]).toBeGreaterThanOrEqualTo(0);
        expect(rgba[3]).toEqual(255);
      });
    });

    it("transforms to world coordinates", function () {
      primitive = new Primitive({
        geometryInstances: [rectangleInstance1, rectangleInstance2],
        appearance: new PerInstanceColorAppearance(),
        asynchronous: false,
      });

      verifyPrimitiveRender(primitive, rectangle1);
      verifyPrimitiveRender(primitive, rectangle2);
      expect(primitive.modelMatrix).toEqual(Matrix4.IDENTITY);
    });

    it("does not transform to world coordinates", function () {
      rectangleInstance2.modelMatrix = Matrix4.clone(
        rectangleInstance1.modelMatrix
      );
      primitive = new Primitive({
        geometryInstances: [rectangleInstance1, rectangleInstance2],
        appearance: new PerInstanceColorAppearance(),
        asynchronous: false,
      });

      scene.frameState.scene3DOnly = true;
      verifyPrimitiveRender(primitive, rectangle1);
      verifyPrimitiveRender(primitive, rectangle2);
      expect(primitive.modelMatrix).not.toEqual(Matrix4.IDENTITY);
      scene.frameState.scene3DOnly = true;
    });

    it("get common per instance attributes", function () {
      rectangleInstance2.attributes.not_used = new GeometryInstanceAttribute({
        componentDatatype: ComponentDatatype.FLOAT,
        componentsPerAttribute: 1,
        value: [0.5],
      });

      primitive = new Primitive({
        geometryInstances: [rectangleInstance1, rectangleInstance2],
        appearance: new PerInstanceColorAppearance(),
        asynchronous: false,
      });

      scene.primitives.add(primitive);
      scene.renderForSpecs();

      var attributes = primitive.getGeometryInstanceAttributes("rectangle1");
      expect(attributes.color).toBeDefined();
      expect(attributes.show).toBeDefined();

      attributes = primitive.getGeometryInstanceAttributes("rectangle2");
      expect(attributes.color).toBeDefined();
      expect(attributes.show).toBeDefined();
      expect(attributes.not_used).not.toBeDefined();
    });

    it("modify color instance attribute", function () {
      primitive = new Primitive({
        geometryInstances: rectangleInstance1,
        appearance: new PerInstanceColorAppearance(),
        asynchronous: false,
      });

      scene.camera.setView({ destination: rectangle1 });
      scene.primitives.add(primitive);
      var pixels;
      expect(scene).toRenderAndCall(function (rgba) {
        pixels = rgba;
        expect(rgba).not.toEqual([0, 0, 0, 255]);
      });

      var attributes = primitive.getGeometryInstanceAttributes("rectangle1");
      expect(attributes.color).toBeDefined();
      attributes.color = [255, 255, 255, 255];

      expect(scene).toRenderAndCall(function (rgba) {
        expect(rgba).not.toEqual([0, 0, 0, 255]);
        expect(rgba).not.toEqual(pixels);
      });
    });

    it("modify show instance attribute", function () {
      primitive = new Primitive({
        geometryInstances: rectangleInstance1,
        appearance: new PerInstanceColorAppearance(),
        asynchronous: false,
      });

      scene.primitives.add(primitive);
      scene.camera.setView({ destination: rectangle1 });
      expect(scene).notToRender([0, 0, 0, 255]);

      var attributes = primitive.getGeometryInstanceAttributes("rectangle1");
      expect(attributes.show).toBeDefined();
      attributes.show = [0];

      expect(scene).toRender([0, 0, 0, 255]);
    });

    it("get bounding sphere from per instance attribute", function () {
      primitive = new Primitive({
        geometryInstances: rectangleInstance1,
        appearance: new PerInstanceColorAppearance(),
        asynchronous: false,
      });

      verifyPrimitiveRender(primitive, rectangle1);

      var attributes = primitive.getGeometryInstanceAttributes("rectangle1");
      expect(attributes.boundingSphere).toBeDefined();
    });

    it("renders with distance display condition per instance attribute", function () {
      if (!context.floatingPointTexture) {
        return;
      }

      var near = 10000.0;
      var far = 1000000.0;
      var rect = Rectangle.fromDegrees(-1.0, -1.0, 1.0, 1.0);
      var translation = Cartesian3.multiplyByScalar(
        Cartesian3.normalize(
          ellipsoid.cartographicToCartesian(Rectangle.center(rect)),
          new Cartesian3()
        ),
        2.0,
        new Cartesian3()
      );
      var rectInstance = new GeometryInstance({
        geometry: new RectangleGeometry({
          vertexFormat: PerInstanceColorAppearance.VERTEX_FORMAT,
          ellipsoid: ellipsoid,
          rectangle: rect,
        }),
        modelMatrix: Matrix4.fromTranslation(translation, new Matrix4()),
        id: "rect",
        attributes: {
          color: new ColorGeometryInstanceAttribute(1.0, 1.0, 0.0, 1.0),
          distanceDisplayCondition: new DistanceDisplayConditionGeometryInstanceAttribute(
            near,
            far
          ),
        },
      });

      primitive = new Primitive({
        geometryInstances: rectInstance,
        appearance: new PerInstanceColorAppearance(),
        asynchronous: false,
      });

      scene.primitives.add(primitive);
      scene.camera.setView({ destination: rect });
      scene.renderForSpecs();

      var boundingSphere = primitive.getGeometryInstanceAttributes("rect")
        .boundingSphere;
      var center = boundingSphere.center;
      var radius = boundingSphere.radius;

      scene.camera.lookAt(
        center,
        new HeadingPitchRange(0.0, -CesiumMath.PI_OVER_TWO, radius)
      );
      expect(scene).toRender([0, 0, 0, 255]);

      scene.camera.lookAt(
        center,
        new HeadingPitchRange(0.0, -CesiumMath.PI_OVER_TWO, radius + near + 1.0)
      );
      expect(scene).notToRender([0, 0, 0, 255]);

      scene.camera.lookAt(
        center,
        new HeadingPitchRange(0.0, -CesiumMath.PI_OVER_TWO, radius + far + 1.0)
      );
      expect(scene).toRender([0, 0, 0, 255]);
    });

    it("primitive with display condition properly transforms boundingSphere", function () {
      var near = 10000.0;
      var far = 1000000.0;
      var translation = new Cartesian3(10, 20, 30);

      var cylinder = new GeometryInstance({
        id: "cylinder",
        vertexFormat: PerInstanceColorAppearance.VERTEX_FORMAT,
        geometry: new CylinderGeometry({
          length: 10,
          topRadius: 10,
          bottomRadius: 10,
        }),
        attributes: {
          color: new ColorGeometryInstanceAttribute(1.0, 1.0, 0.0, 1.0),
          show: new ShowGeometryInstanceAttribute(true),
          distanceDisplayCondition: new DistanceDisplayConditionGeometryInstanceAttribute(
            near,
            far
          ),
        },
      });

      primitive = new Primitive({
        geometryInstances: cylinder,
        appearance: new PerInstanceColorAppearance(),
        modelMatrix: Matrix4.fromTranslation(translation, new Matrix4()),
        asynchronous: false,
      });

      scene.primitives.add(primitive);
      scene.frameState.scene3DOnly = true;
      scene.renderForSpecs();

      var boundingSphere = primitive.getGeometryInstanceAttributes("cylinder")
        .boundingSphere;
      var center = boundingSphere.center;
      expect(center).toEqual(translation);
    });

    it("primitive without display condition properly transforms boundingSphere", function () {
      var translation = new Cartesian3(10, 20, 30);

      var cylinder = new GeometryInstance({
        id: "cylinder",
        vertexFormat: PerInstanceColorAppearance.VERTEX_FORMAT,
        geometry: new CylinderGeometry({
          length: 10,
          topRadius: 10,
          bottomRadius: 10,
        }),
        attributes: {
          color: new ColorGeometryInstanceAttribute(1.0, 1.0, 0.0, 1.0),
          show: new ShowGeometryInstanceAttribute(true),
        },
      });

      primitive = new Primitive({
        geometryInstances: cylinder,
        appearance: new PerInstanceColorAppearance(),
        modelMatrix: Matrix4.fromTranslation(translation, new Matrix4()),
        asynchronous: false,
      });

      scene.primitives.add(primitive);
      scene.frameState.scene3DOnly = true;
      scene.renderForSpecs();

      var boundingSphere = primitive.getGeometryInstanceAttributes("cylinder")
        .boundingSphere;
      var center = boundingSphere.center;
      expect(center).toEqual(translation);
    });

    it("getGeometryInstanceAttributes returns same object each time", function () {
      primitive = new Primitive({
        geometryInstances: rectangleInstance1,
        appearance: new PerInstanceColorAppearance(),
        asynchronous: false,
      });

      verifyPrimitiveRender(primitive, rectangle1);

      var attributes = primitive.getGeometryInstanceAttributes("rectangle1");
      var attributes2 = primitive.getGeometryInstanceAttributes("rectangle1");
      expect(attributes).toBe(attributes2);
    });

    it("picking", function () {
      primitive = new Primitive({
        geometryInstances: [rectangleInstance1, rectangleInstance2],
        appearance: new PerInstanceColorAppearance(),
        asynchronous: false,
      });

      verifyPrimitiveRender(primitive, rectangle1);

      expect(scene).toPickAndCall(function (result) {
        expect(result.primitive).toEqual(primitive);
        expect(result.id).toEqual("rectangle1");
      });

      verifyPrimitiveRender(primitive, rectangle2);

      expect(scene).toPickAndCall(function (result) {
        expect(result.primitive).toEqual(primitive);
        expect(result.id).toEqual("rectangle2");
      });
    });

    it("does not pick when allowPicking is false", function () {
      primitive = new Primitive({
        geometryInstances: [rectangleInstance1],
        appearance: new PerInstanceColorAppearance(),
        allowPicking: false,
        asynchronous: false,
      });

      verifyPrimitiveRender(primitive, rectangle1);

      expect(scene).notToPick();
    });

    it("does not cull when cull is false", function () {
      primitive = new Primitive({
        geometryInstances: rectangleInstance1,
        appearance: new PerInstanceColorAppearance(),
        asynchronous: false,
        cull: false,
      });

      frameState.commandList.length = 0;
      primitive.update(frameState);
      expect(frameState.commandList[0].cull).toEqual(false);
    });

    it("update throws when geometry primitive types are different", function () {
      primitive = new Primitive({
        geometryInstances: [
          new GeometryInstance({
            geometry: new Geometry({
              attributes: {
                position: new GeometryAttribute({
                  componentDatatype: ComponentDatatype.FLOAT,
                  componentsPerAttribute: 3,
                  values: new Float32Array([1.0, 2.0, 3.0, 4.0]),
                }),
              },
              primitiveType: PrimitiveType.LINES,
            }),
          }),
          new GeometryInstance({
            geometry: new Geometry({
              attributes: {
                position: new GeometryAttribute({
                  componentDatatype: ComponentDatatype.FLOAT,
                  componentsPerAttribute: 3,
                  values: new Float32Array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0]),
                }),
              },
              primitiveType: PrimitiveType.TRIANGLES,
            }),
          }),
        ],
        appearance: new PerInstanceColorAppearance(),
        asynchronous: false,
      });

      expect(function () {
        primitive.update(frameState);
      }).toThrowDeveloperError();
    });

    it("failed geometry rejects promise and throws on next update", function () {
      primitive = new Primitive({
        geometryInstances: [
          new GeometryInstance({
            geometry: new BadGeometry(),
          }),
        ],
        appearance: new MaterialAppearance({
          materialSupport: MaterialAppearance.MaterialSupport.ALL,
        }),
        compressVertices: false,
      });

      scene.frameState.afterRender.length = 0;
      scene.primitives.add(primitive);

      return pollToPromise(function () {
        for (var i = 0; i < frameState.afterRender.length; ++i) {
          frameState.afterRender[i]();
          return true;
        }

        primitive.update(frameState);
        return false;
      }).then(function () {
        return primitive.readyPromise
          .then(function () {
            fail("should not be called");
          })
          .otherwise(function (e) {
            expect(e).toBe(primitive._error);
            // Use toThrow since the error is thrown by RequireJS for the web worker import script
            expect(function () {
              scene.render();
            }).toThrow();
          });
      });
    });

    it("internally invalid asynchronous geometry resolves promise and sets ready", function () {
      primitive = new Primitive({
        geometryInstances: [
          new GeometryInstance({
            geometry: PolygonGeometry.fromPositions({
              positions: [],
            }),
          }),
        ],
        appearance: new MaterialAppearance({
          materialSupport: MaterialAppearance.MaterialSupport.ALL,
        }),
        compressVertices: false,
      });

      scene.frameState.afterRender.length = 0;

      return pollToPromise(function () {
        for (var i = 0; i < frameState.afterRender.length; ++i) {
          frameState.afterRender[i]();
          return true;
        }

        primitive.update(frameState);
        return false;
      }).then(function () {
        return primitive.readyPromise.then(function (arg) {
          expect(arg).toBe(primitive);
          expect(primitive.ready).toBe(true);
        });
      });
    });

    it("internally invalid synchronous geometry resolves promise and sets ready", function () {
      primitive = new Primitive({
        geometryInstances: [
          new GeometryInstance({
            geometry: PolygonGeometry.fromPositions({
              positions: [],
            }),
          }),
        ],
        appearance: new MaterialAppearance({
          materialSupport: MaterialAppearance.MaterialSupport.ALL,
        }),
        asynchronous: false,
        compressVertices: false,
      });

      scene.frameState.afterRender.length = 0;

      return pollToPromise(function () {
        if (scene.frameState.afterRender.length > 0) {
          scene.frameState.afterRender[0]();
          return true;
        }
        primitive.update(scene.frameState);
        return false;
      }).then(function () {
        return primitive.readyPromise.then(function (arg) {
          expect(arg).toBe(primitive);
          expect(primitive.ready).toBe(true);
        });
      });
    });

    it("can mix valid and invalid geometry", function () {
      var instances = [];
      instances.push(rectangleInstance1);
      instances.push(
        new GeometryInstance({
          geometry: PolygonGeometry.fromPositions({
            positions: [],
          }),
          attributes: {
            color: new ColorGeometryInstanceAttribute(1.0, 0.0, 1.0, 1.0),
          },
          id: "invalid",
        })
      );
      instances.push(rectangleInstance2);

      primitive = new Primitive({
        geometryInstances: instances,
        appearance: new PerInstanceColorAppearance({
          flat: true,
        }),
      });

      return pollToPromise(function () {
        primitive.update(frameState);
        if (frameState.afterRender.length > 0) {
          frameState.afterRender[0]();
        }
        return primitive.ready;
      }).then(function () {
        expect(
          primitive.getGeometryInstanceAttributes("rectangle1").boundingSphere
        ).toBeDefined();
        expect(
          primitive.getGeometryInstanceAttributes("rectangle2").boundingSphere
        ).toBeDefined();
        expect(
          primitive.getGeometryInstanceAttributes("invalid").boundingSphere
        ).not.toBeDefined();
      });
    });

    it("shader validation", function () {
      if (!!window.webglStub) {
        return;
      }

      primitive = new Primitive({
        geometryInstances: [rectangleInstance1, rectangleInstance2],
        appearance: new MaterialAppearance({
          materialSupport: MaterialAppearance.MaterialSupport.ALL,
        }),
        asynchronous: false,
        compressVertices: false,
      });

      expect(function () {
        primitive.update(frameState);
      }).toThrowDeveloperError();
    });

    it("setting per instance attribute throws when value is undefined", function () {
      primitive = new Primitive({
        geometryInstances: rectangleInstance1,
        appearance: new PerInstanceColorAppearance(),
        asynchronous: false,
      });

      primitive.update(frameState);
      var attributes = primitive.getGeometryInstanceAttributes("rectangle1");

      expect(function () {
        attributes.color = undefined;
      }).toThrowDeveloperError();
    });

    it("can disable picking when asynchronous", function () {
      primitive = new Primitive({
        geometryInstances: rectangleInstance1,
        appearance: new PerInstanceColorAppearance(),
        asynchronous: true,
        allowPicking: false,
      });

      frameState.afterRender.length = 0;
      scene.primitives.add(primitive);

      return pollToPromise(function () {
        if (frameState.afterRender.length > 0) {
          frameState.afterRender[0]();
        }
        scene.render();
        return primitive.ready;
      }).then(function () {
        var attributes = primitive.getGeometryInstanceAttributes("rectangle1");
        expect(function () {
          attributes.color = undefined;
        }).toThrowDeveloperError();
      });
    });

    it("getGeometryInstanceAttributes throws without id", function () {
      primitive = new Primitive({
        geometryInstances: rectangleInstance1,
        appearance: new PerInstanceColorAppearance(),
        asynchronous: false,
      });

      scene.primitives.add(primitive);
      scene.renderForSpecs();

      expect(function () {
        primitive.getGeometryInstanceAttributes();
      }).toThrowDeveloperError();
    });

    it("getGeometryInstanceAttributes throws if update was not called", function () {
      primitive = new Primitive({
        geometryInstances: rectangleInstance1,
        appearance: new PerInstanceColorAppearance(),
        asynchronous: false,
      });

      expect(function () {
        primitive.getGeometryInstanceAttributes("rectangle1");
      }).toThrowDeveloperError();
    });

    it("getGeometryInstanceAttributes returns undefined if id does not exist", function () {
      primitive = new Primitive({
        geometryInstances: rectangleInstance1,
        appearance: new PerInstanceColorAppearance(),
        asynchronous: false,
      });

      scene.primitives.add(primitive);
      scene.renderForSpecs();

      expect(
        primitive.getGeometryInstanceAttributes("unknown")
      ).not.toBeDefined();
    });

    it("isDestroyed", function () {
      primitive = new Primitive();
      expect(primitive.isDestroyed()).toEqual(false);
      primitive.destroy();
      expect(primitive.isDestroyed()).toEqual(true);
    });

    it("renders when using asynchronous pipeline", function () {
      primitive = new Primitive({
        geometryInstances: rectangleInstance1,
        appearance: new PerInstanceColorAppearance({
          flat: true,
        }),
      });

      var frameState = scene.frameState;
      return pollToPromise(function () {
        primitive.update(frameState);
        for (var i = 0; i < frameState.afterRender.length; ++i) {
          frameState.afterRender[i]();
        }
        return primitive.ready;
      }).then(function () {
        verifyPrimitiveRender(primitive, rectangle1);
      });
    });

    it("destroy before asynchronous pipeline is complete", function () {
      primitive = new Primitive({
        geometryInstances: rectangleInstance1,
        appearance: new PerInstanceColorAppearance(),
      });

      primitive.update(frameState);

      primitive.destroy();
      expect(primitive.isDestroyed()).toEqual(true);
    });
  },
  "WebGL"
);
