import { Cartesian2 } from "../../Source/Cesium.js";
import { Cartesian3 } from "../../Source/Cesium.js";
import { Cartesian4 } from "../../Source/Cesium.js";
import { Color } from "../../Source/Cesium.js";
import { HeadingPitchRange } from "../../Source/Cesium.js";
import { Matrix2 } from "../../Source/Cesium.js";
import { Matrix3 } from "../../Source/Cesium.js";
import { Matrix4 } from "../../Source/Cesium.js";
import { ContextLimits } from "../../Source/Cesium.js";
import { Batched3DModel3DTileContent } from "../../Source/Cesium.js";
import { Cesium3DTileBatchTable } from "../../Source/Cesium.js";
import { Cesium3DTileStyle } from "../../Source/Cesium.js";
import Cesium3DTilesTester from "../Cesium3DTilesTester.js";
import createScene from "../createScene.js";
import concatTypedArrays from "../concatTypedArrays.js";

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

    var withBatchTableUrl =
      "./Data/Cesium3DTiles/Batched/BatchedWithBatchTable/tileset.json";
    var withoutBatchTableUrl =
      "./Data/Cesium3DTiles/Batched/BatchedWithoutBatchTable/tileset.json";
    var noBatchIdsUrl =
      "./Data/Cesium3DTiles/Batched/BatchedNoBatchIds/tileset.json";
    var batchTableHierarchyUrl =
      "./Data/Cesium3DTiles/Hierarchy/BatchTableHierarchy/tileset.json";
    var batchTableHierarchyBinaryUrl =
      "./Data/Cesium3DTiles/Hierarchy/BatchTableHierarchyBinary/tileset.json";
    var batchTableHierarchyMultipleParentsUrl =
      "./Data/Cesium3DTiles/Hierarchy/BatchTableHierarchyMultipleParents/tileset.json";
    var batchTableHierarchyNoParentsUrl =
      "./Data/Cesium3DTiles/Hierarchy/BatchTableHierarchyNoParents/tileset.json";
    var batchTableHierarchyLegacyUrl =
      "./Data/Cesium3DTiles/Hierarchy/BatchTableHierarchyLegacy/tileset.json";

    var result = new Color();

    var mockTileset = {
      _statistics: {
        texturesByteLength: 0,
      },
      getFeature: function (batchId) {
        return { batchId: batchId };
      },
    };
    mockTileset._tileset = mockTileset;

    beforeAll(function () {
      scene = createScene();

      // One feature is located at the center, point the camera there
      var center = Cartesian3.fromRadians(centerLongitude, centerLatitude);
      scene.camera.lookAt(center, new HeadingPitchRange(0.0, -1.57, 20.0));

      // Keep the error from logging to the console when running tests
      spyOn(Cesium3DTileBatchTable, "_deprecationWarning");
      spyOn(Batched3DModel3DTileContent, "_deprecationWarning");
    });

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

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

    it("setShow throws with invalid batchId", function () {
      var batchTable = new Cesium3DTileBatchTable(mockTileset, 1);
      expect(function () {
        batchTable.setShow();
      }).toThrowDeveloperError();
      expect(function () {
        batchTable.setShow(-1);
      }).toThrowDeveloperError();
      expect(function () {
        batchTable.setShow(1);
      }).toThrowDeveloperError();
    });

    it("setShow throws with undefined value", function () {
      var batchTable = new Cesium3DTileBatchTable(mockTileset, 1);
      expect(function () {
        batchTable.setShow(0);
      }).toThrowDeveloperError();
    });

    it("setShow sets show", function () {
      var batchTable = new Cesium3DTileBatchTable(mockTileset, 1);

      // Show is true by default
      expect(batchTable.getShow(0)).toBe(true);

      // Check that batch values are dirty and resources are created when value changes
      batchTable.setShow(0, false);
      batchTable.update(mockTileset, scene.frameState);
      expect(batchTable.getShow(0)).toEqual(false);
    });

    it("getShow throws with invalid batchId", function () {
      var batchTable = new Cesium3DTileBatchTable(mockTileset, 1);
      expect(function () {
        batchTable.getShow();
      }).toThrowDeveloperError();
      expect(function () {
        batchTable.getShow(-1);
      }).toThrowDeveloperError();
      expect(function () {
        batchTable.getShow(1);
      }).toThrowDeveloperError();
    });

    it("getShow", function () {
      var batchTable = new Cesium3DTileBatchTable(mockTileset, 1);
      // Show is true by default
      expect(batchTable.getShow(0)).toEqual(true);
      batchTable.setShow(0, false);
      expect(batchTable.getShow(0)).toEqual(false);
    });

    it("setColor throws with invalid batchId", function () {
      var batchTable = new Cesium3DTileBatchTable(mockTileset, 1);
      expect(function () {
        batchTable.setColor();
      }).toThrowDeveloperError();
      expect(function () {
        batchTable.setColor(-1);
      }).toThrowDeveloperError();
      expect(function () {
        batchTable.setColor(1);
      }).toThrowDeveloperError();
    });

    it("setColor throws with undefined value", function () {
      var batchTable = new Cesium3DTileBatchTable(mockTileset, 1);
      expect(function () {
        batchTable.setColor(0);
      }).toThrowDeveloperError();
    });

    it("setColor", function () {
      var batchTable = new Cesium3DTileBatchTable(mockTileset, 1);

      batchTable.setColor(0, Color.YELLOW);
      batchTable.update(mockTileset, scene.frameState);
      expect(batchTable.getColor(0, result)).toEqual(Color.YELLOW);
    });

    it("setAllColor throws with undefined value", function () {
      var batchTable = new Cesium3DTileBatchTable(mockTileset, 1);
      expect(function () {
        batchTable.setAllColor();
      }).toThrowDeveloperError();
    });

    it("setAllColor", function () {
      var batchTable = new Cesium3DTileBatchTable(mockTileset, 2);
      batchTable.setAllColor(Color.YELLOW);
      expect(batchTable.getColor(0, result)).toEqual(Color.YELLOW);
      expect(batchTable.getColor(1, result)).toEqual(Color.YELLOW);
    });

    it("setAllShow throws with undefined value", function () {
      var batchTable = new Cesium3DTileBatchTable(mockTileset, 1);
      expect(function () {
        batchTable.setAllShow();
      }).toThrowDeveloperError();
    });

    it("setAllShow", function () {
      var batchTable = new Cesium3DTileBatchTable(mockTileset, 2);
      batchTable.setAllShow(false);
      expect(batchTable.getShow(0)).toBe(false);
      expect(batchTable.getShow(1)).toBe(false);
    });

    it("getColor throws with invalid batchId", function () {
      var batchTable = new Cesium3DTileBatchTable(mockTileset, 1);
      expect(function () {
        batchTable.getColor();
      }).toThrowDeveloperError();
      expect(function () {
        batchTable.getColor(-1);
      }).toThrowDeveloperError();
      expect(function () {
        batchTable.getColor(1);
      }).toThrowDeveloperError();
    });

    it("getColor throws with undefined result", function () {
      var batchTable = new Cesium3DTileBatchTable(mockTileset, 1);
      expect(function () {
        batchTable.getColor(0);
      }).toThrowDeveloperError();
    });

    it("getColor", function () {
      var batchTable = new Cesium3DTileBatchTable(mockTileset, 1);
      // Color is true by default
      expect(batchTable.getColor(0, result)).toEqual(Color.WHITE);
      batchTable.setColor(0, Color.YELLOW);
      expect(batchTable.getColor(0, result)).toEqual(Color.YELLOW);
    });

    it("hasProperty throws with invalid batchId", function () {
      var batchTable = new Cesium3DTileBatchTable(mockTileset, 1);
      expect(function () {
        batchTable.hasProperty();
      }).toThrowDeveloperError();
      expect(function () {
        batchTable.hasProperty(-1);
      }).toThrowDeveloperError();
      expect(function () {
        batchTable.hasProperty(1);
      }).toThrowDeveloperError();
    });

    it("hasProperty throws with undefined name", function () {
      var batchTable = new Cesium3DTileBatchTable(mockTileset, 1);
      expect(function () {
        batchTable.hasProperty(0);
      }).toThrowDeveloperError();
    });

    it("hasProperty", function () {
      var batchTableJson = {
        height: [0.0],
      };
      var batchTable = new Cesium3DTileBatchTable(
        mockTileset,
        1,
        batchTableJson
      );
      expect(batchTable.hasProperty(0, "height")).toEqual(true);
      expect(batchTable.hasProperty(0, "id")).toEqual(false);
    });

    it("getPropertyNames throws with invalid batchId", function () {
      var batchTable = new Cesium3DTileBatchTable(mockTileset, 1);
      expect(function () {
        batchTable.getPropertyNames();
      }).toThrowDeveloperError();
      expect(function () {
        batchTable.getPropertyNames(-1);
      }).toThrowDeveloperError();
      expect(function () {
        batchTable.getPropertyNames(1);
      }).toThrowDeveloperError();
    });

    it("getPropertyNames", function () {
      var batchTable = new Cesium3DTileBatchTable(mockTileset, 1);
      expect(batchTable.getPropertyNames(0)).toEqual([]);

      var batchTableJson = {
        height: [0.0],
        id: [0],
      };
      batchTable = new Cesium3DTileBatchTable(mockTileset, 1, batchTableJson);
      expect(batchTable.getPropertyNames(0)).toEqual(["height", "id"]);
    });

    it("getPropertyNames works with results argument", function () {
      var batchTableJson = {
        height: [0.0],
        id: [0],
      };
      var batchTable = new Cesium3DTileBatchTable(
        mockTileset,
        1,
        batchTableJson
      );
      var results = [];
      var names = batchTable.getPropertyNames(0, results);
      expect(names).toBe(results);
      expect(names).toEqual(["height", "id"]);
    });

    it("getProperty throws with invalid batchId", function () {
      var batchTable = new Cesium3DTileBatchTable(mockTileset, 1);
      expect(function () {
        batchTable.getProperty();
      }).toThrowDeveloperError();
      expect(function () {
        batchTable.getProperty(-1);
      }).toThrowDeveloperError();
      expect(function () {
        batchTable.getProperty(1);
      }).toThrowDeveloperError();
    });

    it("getProperty throws with undefined name", function () {
      var batchTable = new Cesium3DTileBatchTable(mockTileset, 1);
      expect(function () {
        batchTable.getProperty(0);
      }).toThrowDeveloperError();
    });

    it("getProperty", function () {
      var batchTable = new Cesium3DTileBatchTable(mockTileset, 1);
      expect(batchTable.getProperty(0, "height")).toBeUndefined();

      var batchTableJson = {
        height: [1.0],
      };
      batchTable = new Cesium3DTileBatchTable(mockTileset, 1, batchTableJson);
      expect(batchTable.getProperty(0, "height")).toEqual(1.0);
      expect(batchTable.getProperty(0, "id")).toBeUndefined();
    });

    it("setProperty throws with invalid batchId", function () {
      var batchTable = new Cesium3DTileBatchTable(mockTileset, 1);
      expect(function () {
        batchTable.setProperty();
      }).toThrowDeveloperError();
      expect(function () {
        batchTable.setProperty(-1);
      }).toThrowDeveloperError();
      expect(function () {
        batchTable.setProperty(1);
      }).toThrowDeveloperError();
    });

    it("setProperty throws with undefined name", function () {
      var batchTable = new Cesium3DTileBatchTable(mockTileset, 1);
      expect(function () {
        batchTable.setProperty(0);
      }).toThrowDeveloperError();
    });

    it("setProperty without existing batch table", function () {
      // Check that a batch table is created with a height of 1.0 for the first resource and undefined for the others
      var batchTable = new Cesium3DTileBatchTable(mockTileset, 3);
      batchTable.setProperty(0, "height", 1.0);

      expect(batchTable._properties.height.length).toEqual(3);
      expect(batchTable.getProperty(0, "height")).toEqual(1.0);
      expect(batchTable.getProperty(1, "height")).toBeUndefined();
      expect(batchTable.getProperty(2, "height")).toBeUndefined();
    });

    it("setProperty with existing batch table", function () {
      var batchTableJson = {
        height: [1.0, 2.0],
      };
      var batchTable = new Cesium3DTileBatchTable(
        mockTileset,
        2,
        batchTableJson
      );
      batchTable.setProperty(0, "height", 3.0);

      expect(batchTable.getProperty(0, "height")).toEqual(3.0);
      expect(batchTable.getProperty(1, "height")).toEqual(2.0);
    });

    it("setProperty with object value", function () {
      var batchTableJson = {
        info: [
          { name: "building0", year: 2000 },
          { name: "building1", year: 2001 },
        ],
      };
      var batchTable = new Cesium3DTileBatchTable(
        mockTileset,
        2,
        batchTableJson
      );
      batchTable.setProperty(0, "info", { name: "building0_new", year: 2002 });

      expect(batchTable.getProperty(0, "info")).toEqual({
        name: "building0_new",
        year: 2002,
      });
      expect(batchTable.getProperty(1, "info")).toEqual({
        name: "building1",
        year: 2001,
      });
    });

    it("setProperty with array value", function () {
      var batchTableJson = {
        rooms: [
          ["room1", "room2"],
          ["room3", "room4"],
        ],
      };
      var batchTable = new Cesium3DTileBatchTable(
        mockTileset,
        2,
        batchTableJson
      );
      batchTable.setProperty(0, "rooms", ["room1_new", "room2"]);

      expect(batchTable.getProperty(0, "rooms")).toEqual([
        "room1_new",
        "room2",
      ]);
      expect(batchTable.getProperty(1, "rooms")).toEqual(["room3", "room4"]);
    });

    it("throws if the binary property does not specify a componentType", function () {
      var batchTableJson = {
        propertyScalar: {
          byteOffset: 0,
          type: "SCALAR",
        },
      };
      var batchTableBinary = new Float64Array([0, 1]);
      expect(function () {
        return new Cesium3DTileBatchTable(
          mockTileset,
          2,
          batchTableJson,
          batchTableBinary
        );
      }).toThrowRuntimeError();
    });

    it("throws if the binary property does not specify a type", function () {
      var batchTableJson = {
        propertyScalar: {
          byteOffset: 0,
          componentType: "DOUBLE",
        },
      };
      var batchTableBinary = new Float64Array([0, 1]);
      expect(function () {
        return new Cesium3DTileBatchTable(
          mockTileset,
          2,
          batchTableJson,
          batchTableBinary
        );
      }).toThrowRuntimeError();
    });

    it("throws if a binary property exists but there is no batchTableBinary", function () {
      var batchTableJson = {
        propertyScalar: {
          byteOffset: 0,
          componentType: "DOUBLE",
          type: "SCALAR",
        },
      };
      expect(function () {
        return new Cesium3DTileBatchTable(mockTileset, 2, batchTableJson);
      }).toThrowRuntimeError();
    });

    it("getProperty and setProperty work for binary properties", function () {
      var propertyScalarBinary = new Float64Array([0, 1]);
      var propertyVec2Binary = new Float32Array([2, 3, 4, 5]);
      var propertyVec3Binary = new Int32Array([6, 7, 8, 9, 10, 11]);
      var propertyVec4Binary = new Uint32Array([
        12,
        13,
        14,
        15,
        16,
        17,
        18,
        19,
      ]);
      var propertyMat2Binary = new Int16Array([20, 21, 22, 23, 24, 25, 26, 27]);
      var propertyMat3Binary = new Uint16Array([
        28,
        29,
        30,
        31,
        32,
        33,
        34,
        35,
        36,
        37,
        38,
        39,
        40,
        41,
        42,
        43,
        44,
        45,
      ]);
      var propertyMat4Binary = new Uint8Array([
        46,
        47,
        48,
        49,
        50,
        51,
        52,
        53,
        54,
        55,
        56,
        57,
        58,
        59,
        60,
        61,
        62,
        63,
        64,
        65,
        66,
        67,
        68,
        69,
        70,
        71,
        72,
        73,
        74,
        75,
        76,
        77,
      ]);

      var buffers = [
        propertyScalarBinary,
        propertyVec2Binary,
        propertyVec3Binary,
        propertyVec4Binary,
        propertyMat2Binary,
        propertyMat3Binary,
        propertyMat4Binary,
      ];
      var batchTableBinary = concatTypedArrays(buffers);
      var batchTableJson = {
        propertyScalar: {
          byteOffset: 0,
          componentType: "DOUBLE",
          type: "SCALAR",
        },
        propertyVec2: {
          byteOffset: 16,
          componentType: "FLOAT",
          type: "VEC2",
        },
        propertyVec3: {
          byteOffset: 32,
          componentType: "INT",
          type: "VEC3",
        },
        propertyVec4: {
          byteOffset: 56,
          componentType: "UNSIGNED_INT",
          type: "VEC4",
        },
        propertyMat2: {
          byteOffset: 88,
          componentType: "SHORT",
          type: "MAT2",
        },
        propertyMat3: {
          byteOffset: 104,
          componentType: "UNSIGNED_SHORT",
          type: "MAT3",
        },
        propertyMat4: {
          byteOffset: 140,
          componentType: "UNSIGNED_BYTE",
          type: "MAT4",
        },
      };

      var batchTable = new Cesium3DTileBatchTable(
        mockTileset,
        2,
        batchTableJson,
        batchTableBinary
      );

      expect(batchTable.getProperty(1, "propertyScalar")).toEqual(1);
      expect(batchTable.getProperty(1, "propertyVec2")).toEqual(
        new Cartesian2(4, 5)
      );
      expect(batchTable.getProperty(1, "propertyVec3")).toEqual(
        new Cartesian3(9, 10, 11)
      );
      expect(batchTable.getProperty(1, "propertyVec4")).toEqual(
        new Cartesian4(16, 17, 18, 19)
      );
      expect(batchTable.getProperty(1, "propertyMat2")).toEqual(
        new Matrix2(24, 26, 25, 27)
      ); // Constructor is row-major, data is column major
      expect(batchTable.getProperty(1, "propertyMat3")).toEqual(
        new Matrix3(37, 40, 43, 38, 41, 44, 39, 42, 45)
      ); // Constructor is row-major, data is column major
      expect(batchTable.getProperty(1, "propertyMat4")).toEqual(
        new Matrix4(
          62,
          66,
          70,
          74,
          63,
          67,
          71,
          75,
          64,
          68,
          72,
          76,
          65,
          69,
          73,
          77
        )
      ); // Constructor is row-major, data is column major

      batchTable.setProperty(1, "propertyScalar", 2);
      batchTable.setProperty(1, "propertyVec2", new Cartesian2(5, 6));
      batchTable.setProperty(1, "propertyVec3", new Cartesian3(10, 11, 12));
      batchTable.setProperty(1, "propertyVec4", new Cartesian4(17, 18, 19, 20));
      batchTable.setProperty(1, "propertyMat2", new Matrix2(25, 27, 26, 28));
      batchTable.setProperty(
        1,
        "propertyMat3",
        new Matrix3(38, 41, 44, 39, 42, 45, 40, 43, 46)
      );
      batchTable.setProperty(
        1,
        "propertyMat4",
        new Matrix4(
          63,
          67,
          71,
          75,
          64,
          68,
          72,
          76,
          65,
          69,
          73,
          77,
          66,
          70,
          74,
          78
        )
      );

      expect(batchTable.getProperty(1, "propertyScalar")).toEqual(2);
      expect(batchTable.getProperty(1, "propertyVec2")).toEqual(
        new Cartesian2(5, 6)
      );
      expect(batchTable.getProperty(1, "propertyVec3")).toEqual(
        new Cartesian3(10, 11, 12)
      );
      expect(batchTable.getProperty(1, "propertyVec4")).toEqual(
        new Cartesian4(17, 18, 19, 20)
      );
      expect(batchTable.getProperty(1, "propertyMat2")).toEqual(
        new Matrix2(25, 27, 26, 28)
      );
      expect(batchTable.getProperty(1, "propertyMat3")).toEqual(
        new Matrix3(38, 41, 44, 39, 42, 45, 40, 43, 46)
      );
      expect(batchTable.getProperty(1, "propertyMat4")).toEqual(
        new Matrix4(
          63,
          67,
          71,
          75,
          64,
          68,
          72,
          76,
          65,
          69,
          73,
          77,
          66,
          70,
          74,
          78
        )
      );
    });

    it("renders tileset with batch table", function () {
      return Cesium3DTilesTester.loadTileset(scene, withBatchTableUrl).then(
        function (tileset) {
          var content = tileset.root.content;

          // Each feature in the b3dm file has an id property from 0 to 9,
          // check that the 2nd resource has an id of 2
          expect(content.getFeature(2).getProperty("id")).toEqual(2);

          // Check that a property can be an array
          expect(content.getFeature(2).getProperty("rooms")).toEqual([
            "room2_a",
            "room2_b",
            "room2_c",
          ]);

          // Check that a property can be an object
          expect(content.getFeature(2).getProperty("info")).toEqual({
            name: "building2",
            year: 2,
          });

          Cesium3DTilesTester.expectRenderTileset(scene, tileset);
        }
      );
    });

    it("renders tileset without batch table", function () {
      return Cesium3DTilesTester.loadTileset(scene, withoutBatchTableUrl).then(
        function (tileset) {
          var content = tileset.root.content;

          expect(content.getFeature(2).getProperty("id")).toBeUndefined();

          Cesium3DTilesTester.expectRenderTileset(scene, tileset);
        }
      );
    });

    it("renders when vertex texture fetch is not supported", function () {
      // Disable VTF
      var maximumVertexTextureImageUnits =
        ContextLimits.maximumVertexTextureImageUnits;
      ContextLimits._maximumVertexTextureImageUnits = 0;

      return Cesium3DTilesTester.loadTileset(scene, withoutBatchTableUrl).then(
        function (tileset) {
          Cesium3DTilesTester.expectRenderTileset(scene, tileset);

          // Re-enable VTF
          ContextLimits._maximumVertexTextureImageUnits = maximumVertexTextureImageUnits;
        }
      );
    });

    it("renders with featuresLength greater than maximumTextureSize", function () {
      // Set maximum texture size to 4 temporarily. Batch length of b3dm file is 10.
      var maximumTextureSize = ContextLimits.maximumTextureSize;
      ContextLimits._maximumTextureSize = 4;

      return Cesium3DTilesTester.loadTileset(scene, withoutBatchTableUrl).then(
        function (tileset) {
          var content = tileset.root.content;
          expect(content.featuresLength).toBeGreaterThan(
            ContextLimits._maximumTextureSize
          );
          Cesium3DTilesTester.expectRenderTileset(scene, tileset);

          // Reset maximum texture size
          ContextLimits._maximumTextureSize = maximumTextureSize;
        }
      );
    });

    it("renders with featuresLength of zero", function () {
      return Cesium3DTilesTester.loadTileset(scene, noBatchIdsUrl).then(
        function (tileset) {
          Cesium3DTilesTester.expectRender(scene, tileset);

          expect(scene).toPickAndCall(function (result) {
            expect(result).toBeDefined();
            expect(result.primitive).toBe(tileset);
          });
        }
      );
    });

    function expectRenderTranslucent(tileset) {
      var batchTable = tileset.root.content.batchTable;

      // Get initial color
      var opaqueColor;
      Cesium3DTilesTester.expectRender(scene, tileset, function (rgba) {
        opaqueColor = rgba;
      });

      // Render translucent
      batchTable.setAllColor(new Color(1.0, 1.0, 1.0, 0.5));
      Cesium3DTilesTester.expectRender(scene, tileset, function (rgba) {
        expect(rgba).not.toEqual(opaqueColor);
      });

      // Render restored to opaque
      batchTable.setAllColor(Color.WHITE);
      Cesium3DTilesTester.expectRender(scene, tileset, function (rgba) {
        expect(rgba).toEqual(opaqueColor);
      });

      // Generate both translucent and opaque commands
      batchTable.setColor(0, new Color(1.0, 1.0, 1.0, 0.5));
      Cesium3DTilesTester.expectRender(scene, tileset);

      // Fully transparent
      batchTable.setAllColor(new Color(1.0, 1.0, 1.0, 0.0));
      Cesium3DTilesTester.expectRenderBlank(scene, tileset);
    }

    it("renders translucent style", function () {
      return Cesium3DTilesTester.loadTileset(scene, withoutBatchTableUrl).then(
        function (tileset) {
          expectRenderTranslucent(tileset);
        }
      );
    });

    it("renders translucent style when vertex texture fetch is not supported", function () {
      // Disable VTF
      var maximumVertexTextureImageUnits =
        ContextLimits.maximumVertexTextureImageUnits;
      ContextLimits._maximumVertexTextureImageUnits = 0;
      return Cesium3DTilesTester.loadTileset(scene, withoutBatchTableUrl).then(
        function (tileset) {
          expectRenderTranslucent(tileset);
          // Re-enable VTF
          ContextLimits._maximumVertexTextureImageUnits = maximumVertexTextureImageUnits;
        }
      );
    });

    it("isExactClass throws with invalid batchId", function () {
      var batchTable = new Cesium3DTileBatchTable(mockTileset, 1);
      expect(function () {
        batchTable.isExactClass();
      }).toThrowDeveloperError();
      expect(function () {
        batchTable.isExactClass(1, "door");
      }).toThrowDeveloperError();
      expect(function () {
        batchTable.isExactClass(-1, "door");
      }).toThrowDeveloperError();
    });

    it("isExactClass throws with undefined className", function () {
      var batchTable = new Cesium3DTileBatchTable(mockTileset, 1);
      expect(function () {
        batchTable.isExactClass(0);
      }).toThrowDeveloperError();
    });

    it("isClass throws with invalid batchId", function () {
      var batchTable = new Cesium3DTileBatchTable(mockTileset, 1);
      expect(function () {
        batchTable.isClass();
      }).toThrowDeveloperError();
      expect(function () {
        batchTable.isClass(1, "door");
      }).toThrowDeveloperError();
      expect(function () {
        batchTable.isClass(-1, "door");
      }).toThrowDeveloperError();
    });

    it("isClass throws with undefined className", function () {
      var batchTable = new Cesium3DTileBatchTable(mockTileset, 1);
      expect(function () {
        batchTable.isClass(0);
      }).toThrowDeveloperError();
    });

    it("getExactClassName throws with invalid batchId", function () {
      var batchTable = new Cesium3DTileBatchTable(mockTileset, 1);
      expect(function () {
        batchTable.getExactClassName();
      }).toThrowDeveloperError();
      expect(function () {
        batchTable.getExactClassName(1);
      }).toThrowDeveloperError();
      expect(function () {
        batchTable.getExactClassName(-1);
      }).toThrowDeveloperError();
    });

    function checkHierarchyStyling(tileset) {
      // Check that a feature is colored from a generic batch table property.
      tileset.style = new Cesium3DTileStyle({
        color: "${height} === 6.0 ? color('red') : color('green')",
      });
      expect(scene).toRenderAndCall(function (rgba) {
        expect(rgba[0]).toBeGreaterThan(0); // Expect red
      });

      // Check that a feature is colored from a class property.
      tileset.style = new Cesium3DTileStyle({
        color: "${roof_name} === 'roof2' ? color('red') : color('green')",
      });
      expect(scene).toRenderAndCall(function (rgba) {
        expect(rgba[0]).toBeGreaterThan(0); // Expect red
      });

      // Check that a feature is colored from an inherited property.
      tileset.style = new Cesium3DTileStyle({
        color:
          "${building_name} === 'building2' ? color('red') : color('green')",
      });
      expect(scene).toRenderAndCall(function (rgba) {
        expect(rgba[0]).toBeGreaterThan(0); // Expect red
      });

      // Check isExactClass
      tileset.style = new Cesium3DTileStyle({
        color: "isExactClass('roof') ? color('red') : color('green')",
      });
      expect(scene).toRenderAndCall(function (rgba) {
        expect(rgba[0]).toBeGreaterThan(0); // Expect red
      });
      tileset.style = new Cesium3DTileStyle({
        color: "isExactClass('door') ? color('red') : color('green')",
      });
      expect(scene).toRenderAndCall(function (rgba) {
        expect(rgba[1]).toBeGreaterThan(1); // Expect green
      });

      // Check isClass
      tileset.style = new Cesium3DTileStyle({
        color: "isClass('roof') ? color('red') : color('green')",
      });
      expect(scene).toRenderAndCall(function (rgba) {
        expect(rgba[0]).toBeGreaterThan(0); // Expect red
      });
      tileset.style = new Cesium3DTileStyle({
        color: "isClass('zone') ? color('red') : color('green')",
      });
      expect(scene).toRenderAndCall(function (rgba) {
        expect(rgba[0]).toBeGreaterThan(0); // Expect red
      });

      // Check getExactClassName
      tileset.style = new Cesium3DTileStyle({
        color: "getExactClassName() === 'roof' ? color('red') : color('green')",
      });
      expect(scene).toRenderAndCall(function (rgba) {
        expect(rgba[0]).toBeGreaterThan(0); // Expect red
      });
      tileset.style = new Cesium3DTileStyle({
        color: "getExactClassName() === 'zone' ? color('red') : color('green')",
      });
      expect(scene).toRenderAndCall(function (rgba) {
        expect(rgba[1]).toBeGreaterThan(0); // Expect green
      });
    }

    function checkHierarchyStylingNoParents(tileset) {
      // Check that a feature is colored from a generic batch table property.
      tileset.style = new Cesium3DTileStyle({
        color: "${height} === 6.0 ? color('red') : color('green')",
      });
      expect(scene).toRenderAndCall(function (rgba) {
        expect(rgba[0]).toBeGreaterThan(0); // Expect red
      });

      // Check that a feature is colored from a class property.
      tileset.style = new Cesium3DTileStyle({
        color: "${roof_name} === 'roof2' ? color('red') : color('green')",
      });
      expect(scene).toRenderAndCall(function (rgba) {
        expect(rgba[0]).toBeGreaterThan(0); // Expect red
      });

      // Check isExactClass
      tileset.style = new Cesium3DTileStyle({
        color: "isExactClass('roof') ? color('red') : color('green')",
      });
      expect(scene).toRenderAndCall(function (rgba) {
        expect(rgba[0]).toBeGreaterThan(0); // Expect red
      });

      // Check isClass
      tileset.style = new Cesium3DTileStyle({
        color: "isClass('roof') ? color('red') : color('green')",
      });
      expect(scene).toRenderAndCall(function (rgba) {
        expect(rgba[0]).toBeGreaterThan(0); // Expect red
      });

      // Check getExactClassName
      tileset.style = new Cesium3DTileStyle({
        color: "getExactClassName() === 'roof' ? color('red') : color('green')",
      });
      expect(scene).toRenderAndCall(function (rgba) {
        expect(rgba[0]).toBeGreaterThan(0); // Expect red
      });
    }

    function checkHierarchyProperties(tileset, multipleParents) {
      // Check isExactClass, isClass, and getExactClassName in Cesium3DTileFeature
      var content = tileset.root.content;
      var batchTable = content.batchTable;
      var hierarchy = batchTable._batchTableHierarchy;

      var doorFeature = content.getFeature(4);
      var roofFeature = content.getFeature(8);
      expect(doorFeature.isExactClass("door")).toBe(true);
      expect(doorFeature.isExactClass("building")).toBe(false);
      expect(doorFeature.isClass("door")).toBe(true);
      expect(doorFeature.isClass("doorknob")).toBe(false);
      expect(doorFeature.isClass("building")).toBe(true);
      expect(doorFeature.getExactClassName()).toBe("door");
      expect(doorFeature.hasProperty("door_name")).toBe(true);
      expect(doorFeature.hasProperty("height")).toBe(true);

      // Includes batch table properties and hierarchy properties from all inherited classes
      var expectedPropertyNames = [
        "height",
        "area",
        "door_mass",
        "door_width",
        "door_name",
        "building_area",
        "building_name",
        "zone_buildings",
        "zone_name",
      ];

      // door0 has two parents - building0 and classifier_old
      // building0 has two parents - zone0 and classifier_new
      if (multipleParents) {
        expectedPropertyNames.push("year", "color", "name", "architect"); // classier_new
        expectedPropertyNames.push("description", "inspection"); // classifier_old
      }

      var propertyNames = doorFeature.getPropertyNames();
      expect(expectedPropertyNames.sort()).toEqual(propertyNames.sort());

      expect(doorFeature.getProperty("height")).toBe(5.0); // Gets generic property
      expect(doorFeature.getProperty("door_name")).toBe("door0"); // Gets class property
      expect(doorFeature.getProperty("building_name")).toBe("building0"); // Gets inherited property

      // Sets generic property
      doorFeature.setProperty("height", 10.0);
      expect(doorFeature.getProperty("height")).toBe(10.0);

      // Sets class property
      doorFeature.setProperty("door_name", "new_door");
      expect(doorFeature.getProperty("door_name")).toBe("new_door");
      expect(roofFeature.getProperty("door_name")).toBeUndefined();

      // Throws error when setting inherited property
      expect(function () {
        doorFeature.setProperty("building_name", "new_building");
      }).toThrowDeveloperError();

      // Check properties when there is no hierarchy
      batchTable._batchTableHierarchy = undefined;
      expect(doorFeature.isExactClass("door")).toBe(false);
      expect(doorFeature.isClass("door")).toBe(false);
      expect(doorFeature.getExactClassName()).toBeUndefined();
      expect(doorFeature.hasProperty("door_name")).toBe(false);
      expect(doorFeature.hasProperty("height")).toBe(true);
      expect(doorFeature.getPropertyNames()).toEqual(["height", "area"]);
      expect(doorFeature.getProperty("height")).toBe(10.0);
      expect(doorFeature.getProperty("door_name")).toBeUndefined();
      expect(doorFeature.getProperty("building_name")).toBeUndefined();
      batchTable._batchTableHierarchy = hierarchy;
    }

    function checkHierarchyPropertiesNoParents(tileset) {
      // Check isExactClass, isClass, and getExactClassName in Cesium3DTileFeature
      var content = tileset.root.content;
      var doorFeature = content.getFeature(4);
      expect(doorFeature.isExactClass("door")).toBe(true);
      expect(doorFeature.isExactClass("doorknob")).toBe(false);
      expect(doorFeature.isClass("door")).toBe(true);
      expect(doorFeature.isClass("doorknob")).toBe(false);
      expect(doorFeature.getExactClassName()).toBe("door");
      expect(doorFeature.hasProperty("door_name")).toBe(true);
      expect(doorFeature.hasProperty("height")).toBe(true);

      // Includes batch table properties and hierarchy properties from all inherited classes
      var expectedPropertyNames = [
        "height",
        "area",
        "door_mass",
        "door_width",
        "door_name",
      ];

      var propertyNames = doorFeature.getPropertyNames();
      expect(expectedPropertyNames.sort()).toEqual(propertyNames.sort());

      expect(doorFeature.getProperty("height")).toBe(5.0); // Gets generic property
      expect(doorFeature.getProperty("door_name")).toBe("door0"); // Gets class property

      // Sets generic property
      doorFeature.setProperty("height", 10.0);
      expect(doorFeature.getProperty("height")).toBe(10.0);

      // Sets class property
      doorFeature.setProperty("door_name", "new_door");
      expect(doorFeature.getProperty("door_name")).toBe("new_door");
    }

    function checkBatchTableHierarchy(url, multipleParents) {
      return Cesium3DTilesTester.loadTileset(scene, url).then(function (
        tileset
      ) {
        checkHierarchyStyling(tileset);
        checkHierarchyProperties(tileset, multipleParents);
      });
    }

    function checkBatchTableHierarchyNoParents(url) {
      return Cesium3DTilesTester.loadTileset(scene, url).then(function (
        tileset
      ) {
        checkHierarchyStylingNoParents(tileset);
        checkHierarchyPropertiesNoParents(tileset);
      });
    }

    it("renders tileset with batch table hierarchy extension", function () {
      return checkBatchTableHierarchy(batchTableHierarchyUrl, false);
    });

    it("renders tileset with batch table hierarchy using binary properties", function () {
      return checkBatchTableHierarchy(batchTableHierarchyBinaryUrl, true);
    });

    it("renders tileset with batch table hierarchy with multiple parent classes", function () {
      return checkBatchTableHierarchy(
        batchTableHierarchyMultipleParentsUrl,
        true
      );
    });

    it("renders tileset with batch table hierarchy with no parents", function () {
      return checkBatchTableHierarchyNoParents(batchTableHierarchyNoParentsUrl);
    });

    it("renders tileset with legacy batch table hierarchy (pre-version 1.0)", function () {
      return checkBatchTableHierarchy(batchTableHierarchyLegacyUrl, false);
    });

    it("warns about deprecated batch hierarchy (pre-version 1.0)", function () {
      return checkBatchTableHierarchy(batchTableHierarchyLegacyUrl, false).then(
        function (tileset) {
          expect(Cesium3DTileBatchTable._deprecationWarning).toHaveBeenCalled();
        }
      );
    });

    it("destroys", function () {
      return Cesium3DTilesTester.loadTileset(scene, withoutBatchTableUrl).then(
        function (tileset) {
          var content = tileset.root.content;
          var batchTable = content.batchTable;
          expect(batchTable.isDestroyed()).toEqual(false);
          scene.primitives.remove(tileset);
          expect(batchTable.isDestroyed()).toEqual(true);
        }
      );
    });
  },
  "WebGL"
);
