import {
  Cartesian3,
  Cesium3DTilePass,
  Cesium3DTileRefine,
  Cesium3DTileStyle,
  ClippingPlane,
  ClippingPlaneCollection,
  Color,
  ComponentDatatype,
  defined,
  DracoLoader,
  Expression,
  HeadingPitchRange,
  HeadingPitchRoll,
  Math as CesiumMath,
  MetadataClass,
  GroupMetadata,
  Pass,
  PerspectiveFrustum,
  Transforms,
  when,
} from "../../Source/Cesium.js";
import Cesium3DTilesTester from "../Cesium3DTilesTester.js";
import createCanvas from "../createCanvas.js";
import createScene from "../createScene.js";
import pollToPromise from "../pollToPromise.js";

describe(
  "Scene/PointCloud3DTileContent",
  function () {
    var scene;
    var centerLongitude = -1.31968;
    var centerLatitude = 0.698874;

    var pointCloudRGBUrl =
      "./Data/Cesium3DTiles/PointCloud/PointCloudRGB/tileset.json";
    var pointCloudRGBAUrl =
      "./Data/Cesium3DTiles/PointCloud/PointCloudRGBA/tileset.json";
    var pointCloudRGB565Url =
      "./Data/Cesium3DTiles/PointCloud/PointCloudRGB565/tileset.json";
    var pointCloudNoColorUrl =
      "./Data/Cesium3DTiles/PointCloud/PointCloudNoColor/tileset.json";
    var pointCloudConstantColorUrl =
      "./Data/Cesium3DTiles/PointCloud/PointCloudConstantColor/tileset.json";
    var pointCloudNormalsUrl =
      "./Data/Cesium3DTiles/PointCloud/PointCloudNormals/tileset.json";
    var pointCloudNormalsOctEncodedUrl =
      "./Data/Cesium3DTiles/PointCloud/PointCloudNormalsOctEncoded/tileset.json";
    var pointCloudQuantizedUrl =
      "./Data/Cesium3DTiles/PointCloud/PointCloudQuantized/tileset.json";
    var pointCloudQuantizedOctEncodedUrl =
      "./Data/Cesium3DTiles/PointCloud/PointCloudQuantizedOctEncoded/tileset.json";
    var pointCloudDracoUrl =
      "./Data/Cesium3DTiles/PointCloud/PointCloudDraco/tileset.json";
    var pointCloudDracoPartialUrl =
      "./Data/Cesium3DTiles/PointCloud/PointCloudDracoPartial/tileset.json";
    var pointCloudDracoBatchedUrl =
      "./Data/Cesium3DTiles/PointCloud/PointCloudDracoBatched/tileset.json";
    var pointCloudWGS84Url =
      "./Data/Cesium3DTiles/PointCloud/PointCloudWGS84/tileset.json";
    var pointCloudBatchedUrl =
      "./Data/Cesium3DTiles/PointCloud/PointCloudBatched/tileset.json";
    var pointCloudWithPerPointPropertiesUrl =
      "./Data/Cesium3DTiles/PointCloud/PointCloudWithPerPointProperties/tileset.json";
    var pointCloudWithUnicodePropertyNamesUrl =
      "./Data/Cesium3DTiles/PointCloud/PointCloudWithUnicodePropertyNames/tileset.json";
    var pointCloudWithTransformUrl =
      "./Data/Cesium3DTiles/PointCloud/PointCloudWithTransform/tileset.json";
    var pointCloudTilesetUrl =
      "./Data/Cesium3DTiles/Tilesets/TilesetPoints/tileset.json";

    function setCamera(longitude, latitude) {
      // Point the camera to the center of the tile
      var center = Cartesian3.fromRadians(longitude, latitude, 5.0);
      scene.camera.lookAt(center, new HeadingPitchRange(0.0, -1.57, 5.0));
    }

    beforeAll(function () {
      scene = createScene();
      scene.frameState.passes.render = true;
    });

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

    beforeEach(function () {
      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);

      setCamera(centerLongitude, centerLatitude);
    });

    afterEach(function () {
      scene.primitives.removeAll();
    });

    it("throws with invalid version", function () {
      var arrayBuffer = Cesium3DTilesTester.generatePointCloudTileBuffer({
        version: 2,
      });
      Cesium3DTilesTester.loadTileExpectError(scene, arrayBuffer, "pnts");
    });

    it("throws if featureTableJsonByteLength is 0", function () {
      var arrayBuffer = Cesium3DTilesTester.generatePointCloudTileBuffer({
        featureTableJsonByteLength: 0,
      });
      Cesium3DTilesTester.loadTileExpectError(scene, arrayBuffer, "pnts");
    });

    it("throws if the feature table does not contain POINTS_LENGTH", function () {
      var arrayBuffer = Cesium3DTilesTester.generatePointCloudTileBuffer({
        featureTableJson: {
          POSITION: {
            byteOffset: 0,
          },
        },
      });
      Cesium3DTilesTester.loadTileExpectError(scene, arrayBuffer, "pnts");
    });

    it("throws if the feature table does not contain POSITION or POSITION_QUANTIZED", function () {
      var arrayBuffer = Cesium3DTilesTester.generatePointCloudTileBuffer({
        featureTableJson: {
          POINTS_LENGTH: 1,
        },
      });
      Cesium3DTilesTester.loadTileExpectError(scene, arrayBuffer, "pnts");
    });

    it("throws if the positions are quantized and the feature table does not contain QUANTIZED_VOLUME_SCALE", function () {
      var arrayBuffer = Cesium3DTilesTester.generatePointCloudTileBuffer({
        featureTableJson: {
          POINTS_LENGTH: 1,
          POSITION_QUANTIZED: {
            byteOffset: 0,
          },
          QUANTIZED_VOLUME_OFFSET: [0.0, 0.0, 0.0],
        },
      });
      Cesium3DTilesTester.loadTileExpectError(scene, arrayBuffer, "pnts");
    });

    it("throws if the positions are quantized and the feature table does not contain QUANTIZED_VOLUME_OFFSET", function () {
      var arrayBuffer = Cesium3DTilesTester.generatePointCloudTileBuffer({
        featureTableJson: {
          POINTS_LENGTH: 1,
          POSITION_QUANTIZED: {
            byteOffset: 0,
          },
          QUANTIZED_VOLUME_SCALE: [1.0, 1.0, 1.0],
        },
      });
      Cesium3DTilesTester.loadTileExpectError(scene, arrayBuffer, "pnts");
    });

    it("throws if the BATCH_ID semantic is defined but BATCHES_LENGTH is not", function () {
      var arrayBuffer = Cesium3DTilesTester.generatePointCloudTileBuffer({
        featureTableJson: {
          POINTS_LENGTH: 2,
          POSITION: [0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
          BATCH_ID: [0, 1],
        },
      });
      Cesium3DTilesTester.loadTileExpectError(scene, arrayBuffer, "pnts");
    });

    it("BATCH_ID semantic uses componentType of UNSIGNED_SHORT by default", function () {
      var arrayBuffer = Cesium3DTilesTester.generatePointCloudTileBuffer({
        featureTableJson: {
          POINTS_LENGTH: 2,
          POSITION: [0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
          BATCH_ID: [0, 1],
          BATCH_LENGTH: 2,
        },
      });
      var content = Cesium3DTilesTester.loadTile(scene, arrayBuffer, "pnts");
      expect(
        content._pointCloud._drawCommand._vertexArray._attributes[1]
          .componentDatatype
      ).toEqual(ComponentDatatype.UNSIGNED_SHORT);
    });

    it("gets tileset properties", function () {
      return Cesium3DTilesTester.loadTileset(scene, pointCloudRGBUrl).then(
        function (tileset) {
          var root = tileset.root;
          var content = root.content;
          expect(content.tileset).toBe(tileset);
          expect(content.tile).toBe(root);
          expect(content.url.indexOf(root._header.content.uri) > -1).toBe(true);
        }
      );
    });

    it("resolves readyPromise", function () {
      return Cesium3DTilesTester.resolvesReadyPromise(scene, pointCloudRGBUrl);
    });

    it("renders point cloud with rgb colors", function () {
      return Cesium3DTilesTester.loadTileset(scene, pointCloudRGBUrl).then(
        function (tileset) {
          Cesium3DTilesTester.expectRender(scene, tileset);
        }
      );
    });

    it("renders point cloud with rgba colors", function () {
      return Cesium3DTilesTester.loadTileset(scene, pointCloudRGBAUrl).then(
        function (tileset) {
          Cesium3DTilesTester.expectRender(scene, tileset);
        }
      );
    });

    it("renders point cloud with rgb565 colors", function () {
      return Cesium3DTilesTester.loadTileset(scene, pointCloudRGB565Url).then(
        function (tileset) {
          Cesium3DTilesTester.expectRender(scene, tileset);
        }
      );
    });

    it("renders point cloud with no colors", function () {
      return Cesium3DTilesTester.loadTileset(scene, pointCloudNoColorUrl).then(
        function (tileset) {
          Cesium3DTilesTester.expectRender(scene, tileset);
        }
      );
    });

    it("renders point cloud with constant colors", function () {
      return Cesium3DTilesTester.loadTileset(
        scene,
        pointCloudConstantColorUrl
      ).then(function (tileset) {
        Cesium3DTilesTester.expectRender(scene, tileset);
      });
    });

    it("renders point cloud with normals", function () {
      return Cesium3DTilesTester.loadTileset(scene, pointCloudNormalsUrl).then(
        function (tileset) {
          Cesium3DTilesTester.expectRender(scene, tileset);
        }
      );
    });

    it("renders point cloud with oct encoded normals", function () {
      return Cesium3DTilesTester.loadTileset(
        scene,
        pointCloudNormalsOctEncodedUrl
      ).then(function (tileset) {
        Cesium3DTilesTester.expectRender(scene, tileset);
      });
    });

    it("renders point cloud with quantized positions", function () {
      return Cesium3DTilesTester.loadTileset(
        scene,
        pointCloudQuantizedUrl
      ).then(function (tileset) {
        Cesium3DTilesTester.expectRender(scene, tileset);
      });
    });

    it("renders point cloud with quantized positions and oct-encoded normals", function () {
      return Cesium3DTilesTester.loadTileset(
        scene,
        pointCloudQuantizedOctEncodedUrl
      ).then(function (tileset) {
        Cesium3DTilesTester.expectRender(scene, tileset);
      });
    });

    it("renders point cloud with draco encoded positions, normals, colors, and batch table properties", function () {
      return Cesium3DTilesTester.loadTileset(scene, pointCloudDracoUrl).then(
        function (tileset) {
          Cesium3DTilesTester.expectRender(scene, tileset);
          // Test that Draco-encoded batch table properties are functioning correctly
          tileset.style = new Cesium3DTileStyle({
            color: "vec4(Number(${secondaryColor}[0] < 1.0), 0.0, 0.0, 1.0)",
          });
          expect(scene).toRenderAndCall(function (rgba) {
            // Produces a red color
            expect(rgba[0]).toBeGreaterThan(rgba[1]);
            expect(rgba[0]).toBeGreaterThan(rgba[2]);
          });
        }
      );
    });

    it("renders point cloud with draco encoded positions and uncompressed normals and colors", function () {
      return Cesium3DTilesTester.loadTileset(
        scene,
        pointCloudDracoPartialUrl
      ).then(function (tileset) {
        Cesium3DTilesTester.expectRender(scene, tileset);
      });
    });

    it("renders point cloud with draco encoded positions, colors, and batch ids", function () {
      return Cesium3DTilesTester.loadTileset(
        scene,
        pointCloudDracoBatchedUrl
      ).then(function (tileset) {
        Cesium3DTilesTester.expectRender(scene, tileset);
      });
    });

    it("error decoding a draco point cloud causes loading to fail", function () {
      return pollToPromise(function () {
        return DracoLoader._taskProcessorReady;
      }).then(function () {
        var decoder = DracoLoader._getDecoderTaskProcessor();
        spyOn(decoder, "scheduleTask").and.returnValue(
          when.reject({ message: "my error" })
        );
        return Cesium3DTilesTester.loadTileset(scene, pointCloudDracoUrl).then(
          function (tileset) {
            var root = tileset.root;
            return root.contentReadyPromise
              .then(function () {
                fail("should not resolve");
              })
              .otherwise(function (error) {
                expect(error.message).toBe("my error");
              });
          }
        );
      });
    });

    it("renders point cloud that are not defined relative to center", function () {
      return Cesium3DTilesTester.loadTileset(scene, pointCloudWGS84Url).then(
        function (tileset) {
          Cesium3DTilesTester.expectRender(scene, tileset);
        }
      );
    });

    it("renders point cloud with batch table", function () {
      return Cesium3DTilesTester.loadTileset(scene, pointCloudBatchedUrl).then(
        function (tileset) {
          Cesium3DTilesTester.expectRender(scene, tileset);
        }
      );
    });

    it("renders point cloud with per-point properties", function () {
      return Cesium3DTilesTester.loadTileset(
        scene,
        pointCloudWithPerPointPropertiesUrl
      ).then(function (tileset) {
        Cesium3DTilesTester.expectRender(scene, tileset);
      });
    });

    it("renders point cloud with tile transform", function () {
      return Cesium3DTilesTester.loadTileset(
        scene,
        pointCloudWithTransformUrl
      ).then(function (tileset) {
        Cesium3DTilesTester.expectRender(scene, tileset);

        var newLongitude = -1.31962;
        var newLatitude = 0.698874;
        var newCenter = Cartesian3.fromRadians(newLongitude, newLatitude, 5.0);
        var newHPR = new HeadingPitchRoll();
        var newTransform = Transforms.headingPitchRollToFixedFrame(
          newCenter,
          newHPR
        );

        // Update tile transform
        tileset.root.transform = newTransform;

        // Move the camera to the new location
        setCamera(newLongitude, newLatitude);
        Cesium3DTilesTester.expectRender(scene, tileset);
      });
    });

    it("renders with debug color", function () {
      CesiumMath.setRandomNumberSeed(0);
      return Cesium3DTilesTester.loadTileset(scene, pointCloudRGBUrl).then(
        function (tileset) {
          var color;
          expect(scene).toRenderAndCall(function (rgba) {
            color = rgba;
          });
          tileset.debugColorizeTiles = true;
          expect(scene).notToRender(color);
          tileset.debugColorizeTiles = false;
          expect(scene).toRender(color);
        }
      );
    });

    it("renders in CV", function () {
      return Cesium3DTilesTester.loadTileset(scene, pointCloudRGBUrl).then(
        function (tileset) {
          scene.morphToColumbusView(0.0);
          setCamera(centerLongitude, centerLatitude);
          Cesium3DTilesTester.expectRender(scene, tileset);
        }
      );
    });

    it("renders in 2D", function () {
      return Cesium3DTilesTester.loadTileset(scene, pointCloudRGBUrl).then(
        function (tileset) {
          scene.morphTo2D(0.0);
          setCamera(centerLongitude, centerLatitude);
          tileset.maximumScreenSpaceError = 3;
          Cesium3DTilesTester.expectRender(scene, tileset);
        }
      );
    });

    it("picks", function () {
      return Cesium3DTilesTester.loadTileset(scene, pointCloudRGBUrl).then(
        function (tileset) {
          var content = tileset.root.content;
          tileset.show = false;
          expect(scene).toPickPrimitive(undefined);
          tileset.show = true;
          expect(scene).toPickAndCall(function (result) {
            expect(result).toBeDefined();
            expect(result.primitive).toBe(tileset);
            expect(result.content).toBe(content);
          });
        }
      );
    });

    it("picks based on batchId", function () {
      return Cesium3DTilesTester.loadTileset(scene, pointCloudBatchedUrl).then(
        function (tileset) {
          // Get the original color
          var color;
          expect(scene).toRenderAndCall(function (rgba) {
            color = rgba;
          });

          // Change the color of the picked feature to yellow
          expect(scene).toPickAndCall(function (first) {
            expect(first).toBeDefined();

            first.color = Color.clone(Color.YELLOW, first.color);

            // Expect the pixel color to be some shade of yellow
            expect(scene).notToRender(color);

            // Turn show off. Expect a different feature to get picked.
            first.show = false;
            expect(scene).toPickAndCall(function (second) {
              expect(second).toBeDefined();
              expect(second).not.toBe(first);
            });
          });
        }
      );
    });

    it("point cloud without batch table works", function () {
      return Cesium3DTilesTester.loadTileset(scene, pointCloudRGBUrl).then(
        function (tileset) {
          var content = tileset.root.content;
          expect(content.featuresLength).toBe(0);
          expect(content.innerContents).toBeUndefined();
          expect(content.hasProperty(0, "name")).toBe(false);
          expect(content.getFeature(0)).toBeUndefined();
        }
      );
    });

    it("batched point cloud works", function () {
      return Cesium3DTilesTester.loadTileset(scene, pointCloudBatchedUrl).then(
        function (tileset) {
          var content = tileset.root.content;
          expect(content.featuresLength).toBe(8);
          expect(content.innerContents).toBeUndefined();
          expect(content.hasProperty(0, "name")).toBe(true);
          expect(content.getFeature(0)).toBeDefined();
        }
      );
    });

    it("point cloud with per-point properties work", function () {
      // When the batch table contains per-point properties, aka no batching, then a Cesium3DTileBatchTable is not
      // created. There is no per-point show/color/pickId because the overhead is too high. Instead points are styled
      // based on their properties, and these are not accessible from the API.
      return Cesium3DTilesTester.loadTileset(
        scene,
        pointCloudWithPerPointPropertiesUrl
      ).then(function (tileset) {
        var content = tileset.root.content;
        expect(content.featuresLength).toBe(0);
        expect(content.innerContents).toBeUndefined();
        expect(content.hasProperty(0, "name")).toBe(false);
        expect(content.getFeature(0)).toBeUndefined();
      });
    });

    it("throws when calling getFeature with invalid index", function () {
      return Cesium3DTilesTester.loadTileset(scene, pointCloudBatchedUrl).then(
        function (tileset) {
          var content = tileset.root.content;
          expect(function () {
            content.getFeature(-1);
          }).toThrowDeveloperError();
          expect(function () {
            content.getFeature(1000);
          }).toThrowDeveloperError();
          expect(function () {
            content.getFeature();
          }).toThrowDeveloperError();
        }
      );
    });

    it("Supports back face culling when there are per-point normals", function () {
      return Cesium3DTilesTester.loadTileset(scene, pointCloudBatchedUrl).then(
        function (tileset) {
          var content = tileset.root.content;

          // Get the number of picked sections with back face culling on
          var pickedCountCulling = 0;
          var pickedCount = 0;
          var picked;

          expect(scene).toPickAndCall(function (result) {
            // Set culling to true
            tileset.pointCloudShading.backFaceCulling = true;

            expect(scene).toPickAndCall(function (result) {
              picked = result;
            });

            /* jshint loopfunc: true */
            while (defined(picked)) {
              picked.show = false;
              //eslint-disable-next-line no-loop-func
              expect(scene).toPickAndCall(function (result) {
                picked = result;
              });
              ++pickedCountCulling;
            }

            // Set the shows back to true
            var length = content.featuresLength;
            for (var i = 0; i < length; ++i) {
              var feature = content.getFeature(i);
              feature.show = true;
            }

            // Set culling to false
            tileset.pointCloudShading.backFaceCulling = false;

            expect(scene).toPickAndCall(function (result) {
              picked = result;
            });

            /* jshint loopfunc: true */
            while (defined(picked)) {
              picked.show = false;
              //eslint-disable-next-line no-loop-func
              expect(scene).toPickAndCall(function (result) {
                picked = result;
              });
              ++pickedCount;
            }

            expect(pickedCount).toBeGreaterThan(pickedCountCulling);
          });
        }
      );
    });

    var noAttenuationPixelCount;
    function attenuationTest(postLoadCallback) {
      var scene = createScene({
        canvas: createCanvas(10, 10),
      });
      noAttenuationPixelCount = scene.logarithmicDepthBuffer ? 20 : 16;
      var center = new Cartesian3.fromRadians(
        centerLongitude,
        centerLatitude,
        5.0
      );
      scene.camera.lookAt(center, new HeadingPitchRange(0.0, -1.57, 5.0));
      scene.postProcessStages.fxaa.enabled = false;
      scene.camera.zoomIn(6);

      return Cesium3DTilesTester.loadTileset(scene, pointCloudNoColorUrl).then(
        function (tileset) {
          tileset.pointCloudShading.eyeDomeLighting = false;
          tileset.root.refine = Cesium3DTileRefine.REPLACE;
          postLoadCallback(scene, tileset);
          scene.destroyForSpecs();
        }
      );
    }

    it("attenuates points based on geometric error", function () {
      return attenuationTest(function (scene, tileset) {
        tileset.pointCloudShading.attenuation = true;
        tileset.pointCloudShading.geometricErrorScale = 1.0;
        tileset.pointCloudShading.maximumAttenuation = undefined;
        tileset.pointCloudShading.baseResolution = undefined;
        tileset.maximumScreenSpaceError = 16;
        expect(scene).toRenderPixelCountAndCall(function (pixelCount) {
          expect(pixelCount).toBeGreaterThan(noAttenuationPixelCount);
        });
      });
    });

    it("modulates attenuation using the tileset screen space error", function () {
      return attenuationTest(function (scene, tileset) {
        tileset.pointCloudShading.attenuation = true;
        tileset.pointCloudShading.geometricErrorScale = 1.0;
        tileset.pointCloudShading.maximumAttenuation = undefined;
        tileset.pointCloudShading.baseResolution = undefined;
        tileset.maximumScreenSpaceError = 1;
        expect(scene).toRenderPixelCountAndCall(function (pixelCount) {
          expect(pixelCount).toEqual(noAttenuationPixelCount);
        });
      });
    });

    it("modulates attenuation using the maximumAttenuation parameter", function () {
      return attenuationTest(function (scene, tileset) {
        tileset.pointCloudShading.attenuation = true;
        tileset.pointCloudShading.geometricErrorScale = 1.0;
        tileset.pointCloudShading.maximumAttenuation = 1;
        tileset.pointCloudShading.baseResolution = undefined;
        tileset.maximumScreenSpaceError = 16;
        expect(scene).toRenderPixelCountAndCall(function (pixelCount) {
          expect(pixelCount).toEqual(noAttenuationPixelCount);
        });
      });
    });

    it("modulates attenuation using the baseResolution parameter", function () {
      return attenuationTest(function (scene, tileset) {
        // pointCloudNoColorUrl is a single tile with GeometricError = 0,
        // which results in default baseResolution being computed
        tileset.pointCloudShading.attenuation = true;
        tileset.pointCloudShading.geometricErrorScale = 1.0;
        tileset.pointCloudShading.maximumAttenuation = undefined;
        tileset.pointCloudShading.baseResolution = 0.2;
        tileset.maximumScreenSpaceError = 16;
        expect(scene).toRenderPixelCountAndCall(function (pixelCount) {
          expect(pixelCount).toEqual(noAttenuationPixelCount);
        });
      });
    });

    it("modulates attenuation using the geometricErrorScale parameter", function () {
      return attenuationTest(function (scene, tileset) {
        tileset.pointCloudShading.attenuation = true;
        tileset.pointCloudShading.geometricErrorScale = 0.2;
        tileset.pointCloudShading.maximumAttenuation = undefined;
        tileset.pointCloudShading.baseResolution = 1.0;
        tileset.maximumScreenSpaceError = 1;
        expect(scene).toRenderPixelCountAndCall(function (pixelCount) {
          expect(pixelCount).toEqual(noAttenuationPixelCount);
        });
      });
    });

    it("attenuates points based on geometric error in 2D", function () {
      return attenuationTest(function (scene, tileset) {
        scene.morphTo2D(0);
        tileset.pointCloudShading.attenuation = true;
        tileset.pointCloudShading.geometricErrorScale = 1.0;
        tileset.pointCloudShading.maximumAttenuation = undefined;
        tileset.pointCloudShading.baseResolution = undefined;
        tileset.maximumScreenSpaceError = 16;
        expect(scene).toRenderPixelCountAndCall(function (pixelCount) {
          expect(pixelCount).toBeGreaterThan(noAttenuationPixelCount);
        });
      });
    });

    it("applies shader style", function () {
      return Cesium3DTilesTester.loadTileset(
        scene,
        pointCloudWithPerPointPropertiesUrl
      ).then(function (tileset) {
        var content = tileset.root.content;

        // Solid red color
        tileset.style = new Cesium3DTileStyle({
          color: 'color("red")',
        });
        expect(scene).toRender([255, 0, 0, 255]);
        expect(content._pointCloud._styleTranslucent).toBe(false);

        // Applies translucency
        tileset.style = new Cesium3DTileStyle({
          color: "rgba(255, 0, 0, 0.005)",
        });
        expect(scene).toRenderAndCall(function (rgba) {
          // Pixel is a darker red
          expect(rgba[0]).toBeLessThan(255);
          expect(rgba[1]).toBe(0);
          expect(rgba[2]).toBe(0);
          expect(rgba[3]).toBe(255);
          expect(content._pointCloud._styleTranslucent).toBe(true);
        });

        // Style with property
        tileset.style = new Cesium3DTileStyle({
          color: "color() * ${temperature}",
        });
        expect(scene).toRenderAndCall(function (rgba) {
          // Pixel color is some shade of gray
          expect(rgba[0]).toBe(rgba[1]);
          expect(rgba[0]).toBe(rgba[2]);
          expect(rgba[0]).toBeGreaterThan(0);
          expect(rgba[0]).toBeLessThan(255);
        });

        // When no conditions are met the default color is white
        tileset.style = new Cesium3DTileStyle({
          color: {
            conditions: [
              ["${secondaryColor}[0] > 1.0", 'color("red")'], // This condition will not be met
            ],
          },
        });
        expect(scene).toRender([255, 255, 255, 255]);

        // Apply style with conditions
        tileset.style = new Cesium3DTileStyle({
          color: {
            conditions: [
              ["${temperature} < 0.1", 'color("#000099")'],
              ["${temperature} < 0.2", 'color("#00cc99", 1.0)'],
              ["${temperature} < 0.3", 'color("#66ff33", 0.5)'],
              ["${temperature} < 0.4", "rgba(255, 255, 0, 0.1)"],
              ["${temperature} < 0.5", "rgb(255, 128, 0)"],
              ["${temperature} < 0.6", 'color("red")'],
              ["${temperature} < 0.7", 'color("rgb(255, 102, 102)")'],
              ["${temperature} < 0.8", "hsl(0.875, 1.0, 0.6)"],
              ["${temperature} < 0.9", "hsla(0.83, 1.0, 0.5, 0.1)"],
              ["true", 'color("#FFFFFF", 1.0)'],
            ],
          },
        });
        expect(scene).notToRender([0, 0, 0, 255]);

        // Apply show style
        tileset.style = new Cesium3DTileStyle({
          show: true,
        });
        expect(scene).notToRender([0, 0, 0, 255]);

        // Apply show style that hides all points
        tileset.style = new Cesium3DTileStyle({
          show: false,
        });
        expect(scene).toRender([0, 0, 0, 255]);

        // Apply show style with property
        tileset.style = new Cesium3DTileStyle({
          show: "${temperature} > 0.1",
        });
        expect(scene).notToRender([0, 0, 0, 255]);
        tileset.style = new Cesium3DTileStyle({
          show: "${temperature} > 1.0",
        });
        expect(scene).toRender([0, 0, 0, 255]);

        // Apply style with point cloud semantics
        tileset.style = new Cesium3DTileStyle({
          color: "${COLOR} / 2.0",
          show: "${POSITION}[0] > 0.5",
        });
        expect(scene).notToRender([0, 0, 0, 255]);

        // Apply pointSize style
        tileset.style = new Cesium3DTileStyle({
          pointSize: 5.0,
        });
        expect(scene).notToRender([0, 0, 0, 255]);
      });
    });

    it("applies shader style with unicode property names", function () {
      return Cesium3DTilesTester.loadTileset(
        scene,
        pointCloudWithUnicodePropertyNamesUrl
      ).then(function (tileset) {
        tileset.style = new Cesium3DTileStyle({
          color: "color() * ${feature['temperature ℃']}",
        });
        expect(scene).toRenderAndCall(function (rgba) {
          // Pixel color is some shade of gray
          expect(rgba[0]).toBe(rgba[1]);
          expect(rgba[0]).toBe(rgba[2]);
          expect(rgba[0]).toBeGreaterThan(0);
          expect(rgba[0]).toBeLessThan(255);
        });
      });
    });

    it("rebuilds shader style when expression changes", function () {
      return Cesium3DTilesTester.loadTileset(scene, pointCloudTilesetUrl).then(
        function (tileset) {
          // Solid red color
          tileset.style = new Cesium3DTileStyle({
            color: 'color("red")',
          });
          expect(scene).toRender([255, 0, 0, 255]);

          tileset.style.color = new Expression('color("lime")');
          tileset.makeStyleDirty();
          expect(scene).toRender([0, 255, 0, 255]);

          tileset.style.color = new Expression('color("blue", 0.5)');
          tileset.makeStyleDirty();
          expect(scene).toRenderAndCall(function (rgba) {
            expect(rgba).toEqualEpsilon([0, 0, 255, 255], 5);
          });

          var i;
          var commands = scene.frameState.commandList;
          var commandsLength = commands.length;
          expect(commandsLength).toBeGreaterThan(1); // Just check that at least some children are rendered
          for (i = 0; i < commandsLength; ++i) {
            expect(commands[i].pass).toBe(Pass.TRANSLUCENT);
          }

          tileset.style.color = new Expression('color("yellow")');
          tileset.makeStyleDirty();
          expect(scene).toRender([255, 255, 0, 255]);

          commands = scene.frameState.commandList;
          commandsLength = commands.length;
          for (i = 0; i < commandsLength; ++i) {
            expect(commands[i].pass).not.toBe(Pass.TRANSLUCENT);
          }
        }
      );
    });

    it("applies shader style to point cloud with normals", function () {
      return Cesium3DTilesTester.loadTileset(
        scene,
        pointCloudQuantizedOctEncodedUrl
      ).then(function (tileset) {
        tileset.style = new Cesium3DTileStyle({
          color: 'color("red")',
        });
        expect(scene).toRenderAndCall(function (rgba) {
          expect(rgba[0]).toBeGreaterThan(0);
          expect(rgba[0]).toBeLessThan(255);
        });
      });
    });

    it("applies shader style to point cloud with normals", function () {
      return Cesium3DTilesTester.loadTileset(
        scene,
        pointCloudQuantizedOctEncodedUrl
      ).then(function (tileset) {
        tileset.style = new Cesium3DTileStyle({
          color: 'color("red")',
        });
        expect(scene).toRenderAndCall(function (rgba) {
          expect(rgba[0]).toBeGreaterThan(0);
        });
      });
    });

    it("applies shader style to point cloud without colors", function () {
      return Cesium3DTilesTester.loadTileset(scene, pointCloudNoColorUrl).then(
        function (tileset) {
          tileset.style = new Cesium3DTileStyle({
            color: 'color("red")',
          });
          expect(scene).toRender([255, 0, 0, 255]);
        }
      );
    });

    it("throws if style references the NORMAL semantic but the point cloud does not have per-point normals", function () {
      return Cesium3DTilesTester.loadTileset(scene, pointCloudRGBUrl).then(
        function (tileset) {
          tileset.style = new Cesium3DTileStyle({
            color: "${NORMAL}[0] > 0.5",
          });
          expect(function () {
            scene.renderForSpecs();
          }).toThrowRuntimeError();
        }
      );
    });

    it("throws when shader style reference a non-existent property", function () {
      return Cesium3DTilesTester.loadTileset(
        scene,
        pointCloudWithPerPointPropertiesUrl
      ).then(function (tileset) {
        tileset.style = new Cesium3DTileStyle({
          color: "color() * ${non_existent_property}",
        });
        expect(function () {
          scene.renderForSpecs();
        }).toThrowRuntimeError();
      });
    });

    it("does not apply shader style if the point cloud has a batch table", function () {
      return Cesium3DTilesTester.loadTileset(scene, pointCloudBatchedUrl).then(
        function (tileset) {
          var content = tileset.root.content;
          var shaderProgram = content._pointCloud._drawCommand.shaderProgram;
          tileset.style = new Cesium3DTileStyle({
            color: 'color("red")',
          });
          scene.renderForSpecs();
          expect(content._pointCloud._drawCommand.shaderProgram).toBe(
            shaderProgram
          );

          // Point cloud is styled through the batch table
          expect(scene).notToRender([0, 0, 0, 255]);
        }
      );
    });

    it("throws when shader style is invalid", function () {
      return Cesium3DTilesTester.loadTileset(scene, pointCloudRGBUrl).then(
        function (tileset) {
          tileset.style = new Cesium3DTileStyle({
            show: '1 < "2"',
          });
          expect(function () {
            scene.renderForSpecs();
          }).toThrowRuntimeError();
        }
      );
    });

    it("gets memory usage", function () {
      var promises = [
        Cesium3DTilesTester.loadTileset(scene, pointCloudNoColorUrl),
        Cesium3DTilesTester.loadTileset(scene, pointCloudRGBUrl),
        Cesium3DTilesTester.loadTileset(scene, pointCloudNormalsUrl),
        Cesium3DTilesTester.loadTileset(
          scene,
          pointCloudQuantizedOctEncodedUrl
        ),
      ];

      // 1000 points
      var expectedGeometryMemory = [
        1000 * 12, // 3 floats (xyz)
        1000 * 15, // 3 floats (xyz), 3 bytes (rgb)
        1000 * 27, // 3 floats (xyz), 3 bytes (rgb), 3 floats (normal)
        1000 * 11, // 3 shorts (quantized xyz), 3 bytes (rgb), 2 bytes (oct-encoded normal)
      ];

      return when.all(promises).then(function (tilesets) {
        var length = tilesets.length;
        for (var i = 0; i < length; ++i) {
          var content = tilesets[i].root.content;
          expect(content.geometryByteLength).toEqual(expectedGeometryMemory[i]);
          expect(content.texturesByteLength).toEqual(0);
        }
      });
    });

    it("gets memory usage for batch point cloud", function () {
      return Cesium3DTilesTester.loadTileset(scene, pointCloudBatchedUrl).then(
        function (tileset) {
          var content = tileset.root.content;

          // Point cloud consists of positions, colors, normals, and batchIds
          // 3 floats (xyz), 3 floats (normal), 1 byte (batchId)
          var pointCloudGeometryMemory = 1000 * 25;

          // One RGBA byte pixel per feature
          var batchTexturesByteLength = content.featuresLength * 4;
          var pickTexturesByteLength = content.featuresLength * 4;

          // Features have not been picked or colored yet, so the batch table contribution is 0.
          expect(content.geometryByteLength).toEqual(pointCloudGeometryMemory);
          expect(content.texturesByteLength).toEqual(0);
          expect(content.batchTableByteLength).toEqual(0);

          // Color a feature and expect the texture memory to increase
          content.getFeature(0).color = Color.RED;
          scene.renderForSpecs();
          expect(content.geometryByteLength).toEqual(pointCloudGeometryMemory);
          expect(content.texturesByteLength).toEqual(0);
          expect(content.batchTableByteLength).toEqual(batchTexturesByteLength);

          // Pick the tile and expect the texture memory to increase
          scene.pickForSpecs();
          expect(content.geometryByteLength).toEqual(pointCloudGeometryMemory);
          expect(content.texturesByteLength).toEqual(0);
          expect(content.batchTableByteLength).toEqual(
            batchTexturesByteLength + pickTexturesByteLength
          );
        }
      );
    });

    it("rebuilds shaders when clipping planes are enabled, change between union and intersection, or change count", function () {
      return Cesium3DTilesTester.loadTileset(scene, pointCloudRGBUrl).then(
        function (tileset) {
          var tile = tileset.root;
          tile._isClipped = true;
          var content = tile.content;
          var passOptions = Cesium3DTilePass.getPassOptions(
            Cesium3DTilePass.RENDER
          );

          var noClipFS =
            content._pointCloud._drawCommand.shaderProgram._fragmentShaderText;
          expect(noClipFS.indexOf("clip") !== -1).toBe(false);

          var clippingPlanes = new ClippingPlaneCollection({
            planes: [new ClippingPlane(Cartesian3.UNIT_X, 0.0)],
            unionClippingRegions: false,
          });
          tileset.clippingPlanes = clippingPlanes;

          clippingPlanes.update(scene.frameState);
          tile.update(tileset, scene.frameState, passOptions);
          var clipOneIntersectFS =
            content._pointCloud._drawCommand.shaderProgram._fragmentShaderText;
          expect(clipOneIntersectFS.indexOf("= clip(") !== -1).toBe(true);
          expect(clipOneIntersectFS.indexOf("float clip") !== -1).toBe(true);

          clippingPlanes.unionClippingRegions = true;

          clippingPlanes.update(scene.frameState);
          tile.update(tileset, scene.frameState, passOptions);
          var clipOneUnionFS =
            content._pointCloud._drawCommand.shaderProgram._fragmentShaderText;
          expect(clipOneUnionFS.indexOf("= clip(") !== -1).toBe(true);
          expect(clipOneUnionFS.indexOf("float clip") !== -1).toBe(true);
          expect(clipOneUnionFS).not.toEqual(clipOneIntersectFS);

          clippingPlanes.add(new ClippingPlane(Cartesian3.UNIT_Y, 1.0));

          clippingPlanes.update(scene.frameState);
          tile.update(tileset, scene.frameState, passOptions);
          var clipTwoUnionFS =
            content._pointCloud._drawCommand.shaderProgram._fragmentShaderText;
          expect(clipTwoUnionFS.indexOf("= clip(") !== -1).toBe(true);
          expect(clipTwoUnionFS.indexOf("float clip") !== -1).toBe(true);
          expect(clipTwoUnionFS).not.toEqual(clipOneIntersectFS);
          expect(clipTwoUnionFS).not.toEqual(clipOneUnionFS);
        }
      );
    });

    it("clipping planes selectively disable rendering", function () {
      return Cesium3DTilesTester.loadTileset(scene, pointCloudRGBUrl).then(
        function (tileset) {
          var color;
          expect(scene).toRenderAndCall(function (rgba) {
            color = rgba;
          });

          var clipPlane = new ClippingPlane(Cartesian3.UNIT_Z, -10.0);
          tileset.clippingPlanes = new ClippingPlaneCollection({
            planes: [clipPlane],
          });

          expect(scene).notToRender(color);

          clipPlane.distance = 0.0;

          expect(scene).toRender(color);
        }
      );
    });

    it("clipping planes apply edge styling", function () {
      return Cesium3DTilesTester.loadTileset(scene, pointCloudRGBUrl).then(
        function (tileset) {
          var color;
          expect(scene).toRenderAndCall(function (rgba) {
            color = rgba;
          });

          var clipPlane = new ClippingPlane(Cartesian3.UNIT_Z, -10.0);
          tileset.clippingPlanes = new ClippingPlaneCollection({
            planes: [clipPlane],
            modelMatrix: Transforms.eastNorthUpToFixedFrame(
              tileset.boundingSphere.center
            ),
            edgeWidth: 20.0,
            edgeColor: Color.RED,
          });

          expect(scene).notToRender(color);
        }
      );
    });

    it("clipping planes union regions (Uint8)", function () {
      // Force uint8 mode - there's a slight rendering difference between
      // float and packed uint8 clipping planes for this test due to the small context
      spyOn(ClippingPlaneCollection, "useFloatTexture").and.returnValue(false);
      return Cesium3DTilesTester.loadTileset(scene, pointCloudRGBUrl).then(
        function (tileset) {
          var color;
          expect(scene).toRenderAndCall(function (rgba) {
            color = rgba;
          });

          tileset.clippingPlanes = new ClippingPlaneCollection({
            planes: [
              new ClippingPlane(Cartesian3.UNIT_Z, 0.0),
              new ClippingPlane(Cartesian3.UNIT_X, 0.0),
            ],
            modelMatrix: Transforms.eastNorthUpToFixedFrame(
              tileset.boundingSphere.center
            ),
            unionClippingRegions: true,
          });

          expect(scene).notToRender(color);

          tileset.clippingPlanes.unionClippingRegions = false;

          expect(scene).toRender(color);
        }
      );
    });

    it("clipping planes union regions (Float)", function () {
      if (!ClippingPlaneCollection.useFloatTexture(scene.context)) {
        // This configuration for the test fails in uint8 mode due to the small context
        return;
      }
      return Cesium3DTilesTester.loadTileset(scene, pointCloudRGBUrl).then(
        function (tileset) {
          var color;
          expect(scene).toRenderAndCall(function (rgba) {
            color = rgba;
          });

          tileset.clippingPlanes = new ClippingPlaneCollection({
            planes: [
              new ClippingPlane(Cartesian3.UNIT_Z, -10.0),
              new ClippingPlane(Cartesian3.UNIT_X, 1.0),
            ],
            modelMatrix: Transforms.eastNorthUpToFixedFrame(
              tileset.boundingSphere.center
            ),
            unionClippingRegions: true,
          });

          expect(scene).notToRender(color);

          tileset.clippingPlanes.unionClippingRegions = false;

          expect(scene).toRender(color);
        }
      );
    });

    it("destroys", function () {
      return Cesium3DTilesTester.tileDestroys(scene, pointCloudRGBUrl);
    });

    describe("3DTILES_metadata", function () {
      var metadataClass = new MetadataClass({
        id: "test",
        class: {
          properties: {
            name: {
              type: "STRING",
            },
            height: {
              type: "FLOAT32",
            },
          },
        },
      });
      var groupMetadata = new GroupMetadata({
        id: "testGroup",
        group: {
          properties: {
            name: "Test Group",
            height: 35.6,
          },
        },
        class: metadataClass,
      });

      it("assigns groupMetadata", function () {
        return Cesium3DTilesTester.loadTileset(scene, pointCloudRGBUrl).then(
          function (tileset) {
            var content = tileset.root.content;
            content.groupMetadata = groupMetadata;
            expect(content.groupMetadata).toBe(groupMetadata);
          }
        );
      });
    });
  },
  "WebGL"
);
