import {
  Batched3DModel3DTileContent,
  Cartesian3,
  Cesium3DTile,
  Cesium3DTileRefine,
  Cesium3DTileset,
  Ellipsoid,
  HeadingPitchRange,
  Implicit3DTileContent,
  ImplicitSubdivisionScheme,
  ImplicitTileCoordinates,
  ImplicitTileset,
  Matrix3,
  Matrix4,
  MetadataClass,
  GroupMetadata,
  Multiple3DTileContent,
  Resource,
  TileBoundingSphere,
  TileBoundingS2Cell,
} from "../../Source/Cesium.js";
import CesiumMath from "../../Source/Core/Math.js";
import ImplicitTilingTester from "../ImplicitTilingTester.js";
import Cesium3DTilesTester from "../Cesium3DTilesTester.js";
import createScene from "../createScene.js";

describe(
  "Scene/Implicit3DTileContent",
  function () {
    var tilesetResource = new Resource({
      url: "https://example.com/tileset.json",
    });
    var mockTileset = {
      modelMatrix: Matrix4.IDENTITY,
    };
    var metadataSchema; // intentionally left undefined

    var tileJson = {
      geometricError: 800,
      refine: "ADD",
      boundingVolume: {
        box: [0, 0, 0, 256, 0, 0, 0, 256, 0, 0, 0, 256],
      },
      content: {
        uri: "https://example.com/{level}/{x}/{y}.b3dm",
        extras: {
          author: "Cesium",
        },
      },
      extensions: {
        "3DTILES_implicit_tiling": {
          subdivisionScheme: "QUADTREE",
          subtreeLevels: 2,
          maximumLevel: 1,
          subtrees: {
            uri: "https://example.com/{level}/{x}/{y}.subtree",
          },
        },
      },
      extras: {
        year: "2021",
      },
    };

    var implicitTileset = new ImplicitTileset(
      tilesetResource,
      tileJson,
      metadataSchema
    );

    var quadtreeBuffer = ImplicitTilingTester.generateSubtreeBuffers({
      tileAvailability: {
        descriptor: "11010",
        bitLength: 5,
        isInternal: true,
      },
      contentAvailability: [
        {
          descriptor: "01010",
          bitLength: 5,
          isInternal: true,
        },
      ],
      childSubtreeAvailability: {
        descriptor: "1111000011110000",
        bitLength: 16,
        isInternal: true,
      },
    }).subtreeBuffer;

    var rootCoordinates = new ImplicitTileCoordinates({
      subdivisionScheme: implicitTileset.subdivisionScheme,
      subtreeLevels: implicitTileset.subtreeLevels,
      level: 0,
      x: 0,
      y: 0,
      z: 0,
    });

    function gatherTilesPreorder(tile, minLevel, maxLevel, result) {
      var level = tile.implicitCoordinates.level;
      if (minLevel <= level && level <= maxLevel) {
        result.push(tile);
      }

      for (var i = 0; i < tile.children.length; i++) {
        gatherTilesPreorder(tile.children[i], minLevel, maxLevel, result);
      }
    }

    function getBoundingBoxArray(tile) {
      var boundingBox = tile.boundingVolume.boundingVolume;
      var box = new Array(12);
      Cartesian3.pack(boundingBox.center, box);
      Matrix3.pack(boundingBox.halfAxes, box, 3);
      return box;
    }

    var scene;

    // This scene is the same as Composite/Composite, just rephrased
    // using 3DTILES_multiple_contents
    var centerLongitude = -1.31968;
    var centerLatitude = 0.698874;

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

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

    var mockPlaceholderTile;
    beforeEach(function () {
      mockPlaceholderTile = new Cesium3DTile(mockTileset, tilesetResource, {
        geometricError: 400,
        boundingVolume: {
          box: [0, 0, 0, 256, 0, 0, 0, 256, 0, 0, 0, 256],
        },
      });
      mockPlaceholderTile.implicitCoordinates = rootCoordinates;
      mockPlaceholderTile.implicitTileset = implicitTileset;

      // One item in each data set is always located in the center, so point the camera there
      var center = Cartesian3.fromRadians(centerLongitude, centerLatitude);
      scene.camera.lookAt(center, new HeadingPitchRange(0.0, -1.57, 26.0));
    });

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

    it("expands subtree", function () {
      var content = new Implicit3DTileContent(
        mockTileset,
        mockPlaceholderTile,
        tilesetResource,
        quadtreeBuffer,
        0
      );
      return content.readyPromise.then(function () {
        var expectedChildrenCounts = [2, 4, 0, 0, 0, 0, 4, 0, 0, 0, 0];
        var tiles = [];
        var subtreeRootTile = mockPlaceholderTile.children[0];
        gatherTilesPreorder(subtreeRootTile, 0, 2, tiles);
        expect(expectedChildrenCounts.length).toEqual(tiles.length);
        for (var i = 0; i < tiles.length; i++) {
          expect(tiles[i].children.length).toEqual(expectedChildrenCounts[i]);
        }
      });
    });

    it("sets tile coordinates on each tile", function () {
      var content = new Implicit3DTileContent(
        mockTileset,
        mockPlaceholderTile,
        tilesetResource,
        quadtreeBuffer,
        0
      );
      return content.readyPromise.then(function () {
        var expectedCoordinates = [
          [0, 0, 0],
          [1, 0, 0],
          [2, 0, 0],
          [2, 1, 0],
          [2, 0, 1],
          [2, 1, 1],
          [1, 0, 1],
          [2, 0, 2],
          [2, 1, 2],
          [2, 0, 3],
          [2, 1, 3],
        ];
        var tiles = [];
        var subtreeRootTile = mockPlaceholderTile.children[0];
        gatherTilesPreorder(subtreeRootTile, 0, 2, tiles);
        for (var i = 0; i < tiles.length; i++) {
          var expected = expectedCoordinates[i];
          var coordinates = new ImplicitTileCoordinates({
            subdivisionScheme: implicitTileset.subdivisionScheme,
            subtreeLevels: implicitTileset.subtreeLevels,
            level: expected[0],
            x: expected[1],
            y: expected[2],
          });
          expect(tiles[i].implicitCoordinates).toEqual(coordinates);
        }
      });
    });

    it("handles deeper subtrees correctly", function () {
      mockPlaceholderTile.implicitCoordinates = new ImplicitTileCoordinates({
        subdivisionScheme: implicitTileset.subdivisionScheme,
        subtreeLevels: implicitTileset.subtreeLevels,
        level: 2,
        x: 2,
        y: 1,
      });
      var content = new Implicit3DTileContent(
        mockTileset,
        mockPlaceholderTile,
        tilesetResource,
        quadtreeBuffer,
        0
      );
      var refine =
        implicitTileset.refine === "ADD"
          ? Cesium3DTileRefine.ADD
          : Cesium3DTileRefine.REPLACE;

      var parentCoordinates = mockPlaceholderTile.implicitCoordinates;
      var childCoordinates = parentCoordinates.getChildCoordinates(0);

      var parentGeometricError = implicitTileset.geometricError / 4;
      var childGeometricError = implicitTileset.geometricError / 8;

      var rootBoundingVolume = [0, 0, 0, 256, 0, 0, 0, 256, 0, 0, 0, 256];
      var parentBox = Implicit3DTileContent._deriveBoundingBox(
        rootBoundingVolume,
        parentCoordinates.level,
        parentCoordinates.x,
        parentCoordinates.y
      );
      var childBox = Implicit3DTileContent._deriveBoundingBox(
        rootBoundingVolume,
        childCoordinates.level,
        childCoordinates.x,
        childCoordinates.y
      );

      return content.readyPromise.then(function () {
        var subtreeRootTile = mockPlaceholderTile.children[0];
        var childTile = subtreeRootTile.children[0];
        expect(subtreeRootTile.implicitCoordinates).toEqual(parentCoordinates);
        expect(childTile.implicitCoordinates).toEqual(childCoordinates);

        expect(subtreeRootTile.refine).toEqual(refine);
        expect(childTile.refine).toEqual(refine);

        expect(subtreeRootTile.geometricError).toEqual(parentGeometricError);
        expect(childTile.geometricError).toEqual(childGeometricError);

        expect(getBoundingBoxArray(subtreeRootTile)).toEqual(parentBox);
        expect(getBoundingBoxArray(childTile)).toEqual(childBox);
      });
    });

    it("puts the root tile inside the placeholder tile", function () {
      var content = new Implicit3DTileContent(
        mockTileset,
        mockPlaceholderTile,
        tilesetResource,
        quadtreeBuffer,
        0
      );
      return content.readyPromise.then(function () {
        expect(mockPlaceholderTile.children.length).toEqual(1);
      });
    });

    it("preserves tile extras", function () {
      var content = new Implicit3DTileContent(
        mockTileset,
        mockPlaceholderTile,
        tilesetResource,
        quadtreeBuffer,
        0
      );
      return content.readyPromise.then(function () {
        expect(mockPlaceholderTile.children[0].extras).toEqual(tileJson.extras);
      });
    });

    it("stores a reference to the subtree in each transcoded tile", function () {
      var content = new Implicit3DTileContent(
        mockTileset,
        mockPlaceholderTile,
        tilesetResource,
        quadtreeBuffer,
        0
      );
      return content.readyPromise.then(function () {
        expect(mockPlaceholderTile.implicitSubtree).not.toBeDefined();

        var subtreeRootTile = mockPlaceholderTile.children[0];
        var subtree = subtreeRootTile.implicitSubtree;
        expect(subtree).toBeDefined();

        var tiles = [];
        gatherTilesPreorder(subtreeRootTile, 0, 1, tiles);
        for (var i = 0; i < tiles.length; i++) {
          expect(tiles[i].implicitSubtree).toBe(subtree);
        }
      });
    });

    it("does not store references to subtrees in placeholder tiles", function () {
      var content = new Implicit3DTileContent(
        mockTileset,
        mockPlaceholderTile,
        tilesetResource,
        quadtreeBuffer,
        0
      );
      return content.readyPromise.then(function () {
        expect(mockPlaceholderTile.implicitSubtree).not.toBeDefined();

        var subtreeRootTile = mockPlaceholderTile.children[0];

        var tiles = [];
        gatherTilesPreorder(subtreeRootTile, 2, 2, tiles);
        for (var i = 0; i < tiles.length; i++) {
          expect(tiles[i].implicitSubtree).not.toBeDefined();
        }
      });
    });

    it("destroys", function () {
      var content = new Implicit3DTileContent(
        mockTileset,
        mockPlaceholderTile,
        tilesetResource,
        quadtreeBuffer,
        0
      );
      return content.readyPromise.then(function () {
        var subtree = content._implicitSubtree;
        expect(content.isDestroyed()).toBe(false);
        expect(subtree.isDestroyed()).toBe(false);

        content.destroy();
        expect(content.isDestroyed()).toBe(true);
        expect(subtree.isDestroyed()).toBe(true);
      });
    });

    it("returns default values for most Cesium3DTileContent properties", function () {
      var content = new Implicit3DTileContent(
        mockTileset,
        mockPlaceholderTile,
        tilesetResource,
        quadtreeBuffer,
        0
      );

      expect(content.featurePropertiesDirty).toBe(false);
      expect(content.featuresLength).toBe(0);
      expect(content.pointsLength).toBe(0);
      expect(content.trianglesLength).toBe(0);
      expect(content.geometryByteLength).toBe(0);
      expect(content.texturesByteLength).toBe(0);
      expect(content.batchTableByteLength).toBe(0);
      expect(content.innerContents).not.toBeDefined();
      expect(content.tileset).toBe(mockTileset);
      expect(content.tile).toBe(mockPlaceholderTile);
      expect(content.batchTable).not.toBeDefined();
    });

    it("url returns the subtree url", function () {
      var content = new Implicit3DTileContent(
        mockTileset,
        mockPlaceholderTile,
        tilesetResource,
        quadtreeBuffer,
        0
      );
      expect(content.url).toBe("https://example.com/0/0/0.subtree");
    });

    it("templates content URIs for each tile with content", function () {
      var content = new Implicit3DTileContent(
        mockTileset,
        mockPlaceholderTile,
        tilesetResource,
        quadtreeBuffer,
        0
      );
      return content.readyPromise.then(function () {
        var expectedCoordinates = [
          [0, 0, 0],
          [1, 0, 0],
          [1, 0, 1],
        ];
        var contentAvailability = [false, true, true];
        var templateUri = implicitTileset.contentUriTemplates[0];
        var subtreeRootTile = mockPlaceholderTile.children[0];
        var tiles = [];
        gatherTilesPreorder(subtreeRootTile, 0, 1, tiles);
        expect(expectedCoordinates.length).toEqual(tiles.length);
        for (var i = 0; i < tiles.length; i++) {
          var expected = expectedCoordinates[i];
          var coordinates = new ImplicitTileCoordinates({
            subdivisionScheme: implicitTileset.subdivisionScheme,
            subtreeLevels: implicitTileset.subtreeLevels,
            level: expected[0],
            x: expected[1],
            y: expected[2],
          });
          var expectedResource = templateUri.getDerivedResource({
            templateValues: coordinates.getTemplateValues(),
          });
          if (contentAvailability[i]) {
            expect(tiles[i]._contentResource.url).toEqual(expectedResource.url);
          } else {
            expect(tiles[i]._contentResource).not.toBeDefined();
          }
        }
      });
    });

    it("constructs placeholder tiles for child subtrees", function () {
      var content = new Implicit3DTileContent(
        mockTileset,
        mockPlaceholderTile,
        tilesetResource,
        quadtreeBuffer,
        0
      );
      return content.readyPromise.then(function () {
        var expectedCoordinates = [
          [2, 0, 0],
          [2, 1, 0],
          [2, 0, 1],
          [2, 1, 1],
          [2, 0, 2],
          [2, 1, 2],
          [2, 0, 3],
          [2, 1, 3],
        ];
        var templateUri = implicitTileset.subtreeUriTemplate;
        var subtreeRootTile = mockPlaceholderTile.children[0];
        var tiles = [];
        gatherTilesPreorder(subtreeRootTile, 2, 2, tiles);

        expect(expectedCoordinates.length).toEqual(tiles.length);
        for (var i = 0; i < tiles.length; i++) {
          var expected = expectedCoordinates[i];
          var coordinates = new ImplicitTileCoordinates({
            subdivisionScheme: implicitTileset.subdivisionScheme,
            subtreeLevels: implicitTileset.subtreeLevels,
            level: expected[0],
            x: expected[1],
            y: expected[2],
          });
          var expectedResource = templateUri.getDerivedResource({
            templateValues: coordinates.getTemplateValues(),
          });
          var placeholderTile = tiles[i];
          expect(placeholderTile._contentResource.url).toEqual(
            expectedResource.url
          );
          expect(placeholderTile.implicitTileset).toBeDefined();
          expect(placeholderTile.implicitCoordinates).toBeDefined();
        }
      });
    });

    it("propagates refine down the tree", function () {
      var content = new Implicit3DTileContent(
        mockTileset,
        mockPlaceholderTile,
        tilesetResource,
        quadtreeBuffer,
        0
      );
      var refine =
        implicitTileset.refine === "ADD"
          ? Cesium3DTileRefine.ADD
          : Cesium3DTileRefine.REPLACE;
      return content.readyPromise.then(function () {
        var subtreeRootTile = mockPlaceholderTile.children[0];
        var tiles = [];
        gatherTilesPreorder(subtreeRootTile, 0, 2, tiles);
        for (var i = 0; i < tiles.length; i++) {
          expect(tiles[i].refine).toEqual(refine);
        }
      });
    });

    it("divides the geometricError by 2 for each level of the tree", function () {
      var content = new Implicit3DTileContent(
        mockTileset,
        mockPlaceholderTile,
        tilesetResource,
        quadtreeBuffer,
        0
      );
      var rootGeometricError = implicitTileset.geometricError;
      return content.readyPromise.then(function () {
        var subtreeRootTile = mockPlaceholderTile.children[0];
        var tiles = [];
        gatherTilesPreorder(subtreeRootTile, 0, 2, tiles);
        for (var i = 0; i < tiles.length; i++) {
          var level = tiles[i].implicitCoordinates.level;
          expect(tiles[i].geometricError).toEqual(
            rootGeometricError / Math.pow(2, level)
          );
        }
      });
    });

    it("subdivides bounding volumes for each tile", function () {
      var content = new Implicit3DTileContent(
        mockTileset,
        mockPlaceholderTile,
        tilesetResource,
        quadtreeBuffer,
        0
      );
      return content.readyPromise.then(function () {
        var expectedCoordinates = [
          [0, 0, 0],
          [1, 0, 0],
          [2, 0, 0],
          [2, 1, 0],
          [2, 0, 1],
          [2, 1, 1],
          [1, 0, 1],
          [2, 0, 2],
          [2, 1, 2],
          [2, 0, 3],
          [2, 1, 3],
        ];
        var rootBoundingVolume = [0, 0, 0, 256, 0, 0, 0, 256, 0, 0, 0, 256];

        var subtreeRootTile = mockPlaceholderTile.children[0];
        var tiles = [];
        gatherTilesPreorder(subtreeRootTile, 0, 2, tiles);

        expect(expectedCoordinates.length).toEqual(tiles.length);
        for (var i = 0; i < tiles.length; i++) {
          var coordinates = expectedCoordinates[i];

          var boundingBox = tiles[i].boundingVolume.boundingVolume;
          var childBox = new Array(12);
          Cartesian3.pack(boundingBox.center, childBox);
          Matrix3.pack(boundingBox.halfAxes, childBox, 3);

          var expectedBounds = Implicit3DTileContent._deriveBoundingBox(
            rootBoundingVolume,
            coordinates[0],
            coordinates[1],
            coordinates[2]
          );
          expect(childBox).toEqual(expectedBounds);
        }
      });
    });

    describe("_deriveBoundingVolumeS2", function () {
      var deriveBoundingVolumeS2 =
        Implicit3DTileContent._deriveBoundingVolumeS2;
      var simpleBoundingVolumeS2 = {
        token: "1",
        minimumHeight: 0,
        maximumHeight: 10,
      };
      var simpleBoundingVolumeS2Cell = new TileBoundingS2Cell(
        simpleBoundingVolumeS2
      );
      var implicitTilesetS2 = {
        boundingVolume: {
          extensions: {
            "3DTILES_bounding_volume_S2": simpleBoundingVolumeS2,
          },
        },
        subdivisionScheme: ImplicitSubdivisionScheme.QUADTREE,
      };

      it("throws if parentIsPlaceholderTile is undefined", function () {
        expect(function () {
          deriveBoundingVolumeS2(undefined, {}, 0, 0, 0, 0, 0);
        }).toThrowDeveloperError();
      });

      it("throws if parentTile is undefined", function () {
        expect(function () {
          deriveBoundingVolumeS2(false, undefined, 0, 0, 0, 0, 0);
        }).toThrowDeveloperError();
      });

      it("throws if childIndex is undefined", function () {
        expect(function () {
          deriveBoundingVolumeS2(false, {}, undefined, 0, 0, 0, 0);
        }).toThrowDeveloperError();
      });

      it("throws if level is undefined", function () {
        expect(function () {
          deriveBoundingVolumeS2(false, {}, 0, undefined, 0, 0, 0);
        }).toThrowDeveloperError();
      });

      it("throws if x is undefined", function () {
        expect(function () {
          deriveBoundingVolumeS2(false, {}, 0, 0, undefined, 0, 0);
        }).toThrowDeveloperError();
      });

      it("throws if y is undefined", function () {
        expect(function () {
          deriveBoundingVolumeS2(false, {}, 0, 0, 0, undefined, 0);
        }).toThrowDeveloperError();
      });

      it("throws if z is defined but not a number", function () {
        expect(function () {
          deriveBoundingVolumeS2(false, {}, 0, 0, 0, 0, "");
        }).toThrowDeveloperError();
      });

      it("returns implicit tileset boundingVolume if parentIsPlaceholderTile is true", function () {
        var placeholderTile = {
          _boundingVolume: simpleBoundingVolumeS2Cell,
        };
        var result = deriveBoundingVolumeS2(
          true,
          placeholderTile,
          0,
          0,
          0,
          0,
          0
        );
        expect(result).toEqual(implicitTilesetS2.boundingVolume);
        expect(result).not.toBe(implicitTilesetS2.boundingVolume);
      });

      it("subdivides correctly using QUADTREE", function () {
        var parentTile = {
          _boundingVolume: simpleBoundingVolumeS2Cell,
        };
        var expected = {
          token: "04",
          minimumHeight: 0,
          maximumHeight: 10,
        };
        var result = deriveBoundingVolumeS2(false, parentTile, 0, 1, 0, 0);
        expect(result).toEqual({
          extensions: {
            "3DTILES_bounding_volume_S2": expected,
          },
        });

        parentTile._boundingVolume = new TileBoundingS2Cell({
          token: "3",
          minimumHeight: 0,
          maximumHeight: 10,
        });
        expected = {
          token: "24",
          minimumHeight: 0,
          maximumHeight: 10,
        };
        result = deriveBoundingVolumeS2(false, parentTile, 0, 1, 0, 0);
        expect(result).toEqual({
          extensions: {
            "3DTILES_bounding_volume_S2": expected,
          },
        });
      });

      it("subdivides correctly using OCTREE", function () {
        implicitTilesetS2.subdivisionScheme = ImplicitSubdivisionScheme.OCTREE;
        var parentTile = {
          _boundingVolume: simpleBoundingVolumeS2Cell,
        };
        var expected0 = {
          token: "04",
          minimumHeight: 0,
          maximumHeight: 5,
        };
        var expected1 = {
          token: "04",
          minimumHeight: 5,
          maximumHeight: 10,
        };
        var result0 = deriveBoundingVolumeS2(false, parentTile, 0, 1, 0, 0, 0);
        expect(result0).toEqual({
          extensions: {
            "3DTILES_bounding_volume_S2": expected0,
          },
        });
        var result1 = deriveBoundingVolumeS2(false, parentTile, 4, 1, 0, 0, 0);
        expect(result1).toEqual({
          extensions: {
            "3DTILES_bounding_volume_S2": expected1,
          },
        });
      });
    });

    describe("_deriveBoundingBox", function () {
      var deriveBoundingBox = Implicit3DTileContent._deriveBoundingBox;
      var simpleBoundingBox = [0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1];
      it("throws if rootBox is undefined", function () {
        expect(function () {
          deriveBoundingBox(undefined, 0, 0, 0);
        }).toThrowDeveloperError();
      });

      it("throws if level is undefined", function () {
        expect(function () {
          deriveBoundingBox(simpleBoundingBox, undefined, 0, 0);
        }).toThrowDeveloperError();
      });

      it("throws if x is undefined", function () {
        expect(function () {
          deriveBoundingBox(simpleBoundingBox, 1, undefined, 0);
        }).toThrowDeveloperError();
      });

      it("throws if y is undefined", function () {
        expect(function () {
          deriveBoundingBox(simpleBoundingBox, 1, 0, undefined);
        }).toThrowDeveloperError();
      });

      it("subdivides like a quadtree if z is not given", function () {
        var tile = [1, 3, 0, 4, 0, 0, 0, 2, 0, 0, 0, 1];
        var expected = [0, 1.5, 0, 1, 0, 0, 0, 0.5, 0, 0, 0, 1];
        var result = deriveBoundingBox(tile, 2, 1, 0);
        expect(result).toEqual(expected);
      });

      it("subdivides like an octree if z is given", function () {
        var tile = [1, 3, 0, 4, 0, 0, 0, 2, 0, 0, 0, 1];
        var expected = [0, 1.5, 0.25, 1, 0, 0, 0, 0.5, 0, 0, 0, 0.25];
        var result = deriveBoundingBox(tile, 2, 1, 0, 2);
        expect(result).toEqual(expected);
      });

      it("handles rotation and non-uniform scaling correctly", function () {
        var tile = [1, 2, 3, 0, 4, 0, 0, 0, 2, 1, 0, 0];
        var expected = [1.25, 1, 1.5, 0, 1, 0, 0, 0, 0.5, 0.25, 0, 0];
        var result = deriveBoundingBox(tile, 2, 1, 0, 2);
        expect(result).toEqual(expected);
      });
    });

    describe("_deriveBoundingRegion", function () {
      var deriveBoundingRegion = Implicit3DTileContent._deriveBoundingRegion;
      var simpleRegion = [
        0,
        0,
        CesiumMath.PI_OVER_FOUR,
        CesiumMath.PI_OVER_FOUR,
        0,
        100,
      ];
      it("throws if rootRegion is undefined", function () {
        expect(function () {
          deriveBoundingRegion(undefined, 1, 0, 0);
        }).toThrowDeveloperError();
      });

      it("throws if level is undefined", function () {
        expect(function () {
          deriveBoundingRegion(simpleRegion, undefined, 0, 0);
        }).toThrowDeveloperError();
      });

      it("throws if x is undefined", function () {
        expect(function () {
          deriveBoundingRegion(simpleRegion, 1, undefined, 0);
        }).toThrowDeveloperError();
      });

      it("throws if y is undefined", function () {
        expect(function () {
          deriveBoundingRegion(simpleRegion, 1, 0, undefined);
        }).toThrowDeveloperError();
      });

      function makeRectangle(
        westDegrees,
        southDegrees,
        eastDegrees,
        northDegrees,
        minimumHeight,
        maximumHeight
      ) {
        return [
          CesiumMath.toRadians(westDegrees),
          CesiumMath.toRadians(southDegrees),
          CesiumMath.toRadians(eastDegrees),
          CesiumMath.toRadians(northDegrees),
          minimumHeight,
          maximumHeight,
        ];
      }

      it("subdivides like a quadtree if z is not given", function () {
        // 4x4 degree rectangle so the subdivisions at level 2 will
        // be 1 degree each
        var tile = makeRectangle(-1, -2, 3, 2, 0, 20);
        var expected = makeRectangle(0, 1, 1, 2, 0, 20);

        var result = deriveBoundingRegion(tile, 2, 1, 3);
        expect(result).toEqualEpsilon(expected, CesiumMath.EPSILON9);
      });

      it("deriveVolume subdivides like an octree if z is given", function () {
        // 4x4 degree rectangle so the subdivisions at level 2 will
        // be 1 degree each
        var tile = makeRectangle(-1, -2, 3, 2, 0, 20);
        var expected = makeRectangle(0, 1, 1, 2, 10, 15);

        var result = deriveBoundingRegion(tile, 2, 1, 3, 2);
        expect(result).toEqualEpsilon(expected, CesiumMath.EPSILON9);
      });

      it("handles the IDL", function () {
        var tile = makeRectangle(90, -45, -90, 45, 0, 20);
        var expected = makeRectangle(180, -45, -90, 0, 0, 20);

        var result = deriveBoundingRegion(tile, 1, 1, 0);
        expect(result).toEqualEpsilon(expected, CesiumMath.EPSILON9);
      });
    });

    describe("3DTILES_multiple_contents", function () {
      var implicitMultipleContentsUrl =
        "Data/Cesium3DTiles/Implicit/ImplicitMultipleContents/tileset.json";

      it("a single content is transcoded as a regular tile", function () {
        return Cesium3DTilesTester.loadTileset(
          scene,
          implicitMultipleContentsUrl
        ).then(function (tileset) {
          // The root tile of this tileset only has one available content
          var transcodedRoot = tileset.root.children[0];
          var transcodedRootHeader = transcodedRoot._header;
          expect(transcodedRoot.content).toBeInstanceOf(
            Batched3DModel3DTileContent
          );
          expect(transcodedRootHeader.content).toEqual({
            uri: "ground/0/0/0.b3dm",
          });
          expect(transcodedRootHeader.extensions).not.toBeDefined();
        });
      });

      it("multiple contents are transcoded to a tile with a 3DTILES_multiple_contents extension", function () {
        return Cesium3DTilesTester.loadTileset(
          scene,
          implicitMultipleContentsUrl
        ).then(function (tileset) {
          var childTiles = tileset.root.children[0].children;
          for (var i = 0; i < childTiles.length; i++) {
            var childTile = childTiles[i];
            var content = childTile.content;
            expect(content).toBeInstanceOf(Multiple3DTileContent);

            var childTileHeader = childTile._header;
            expect(childTileHeader.content).not.toBeDefined();
          }
        });
      });

      it("passes extensions through correctly", function () {
        var originalLoadJson = Cesium3DTileset.loadJson;
        var metadataExtension = {
          group: "buildings",
        };
        var otherExtension = {
          someKey: "someValue",
        };

        spyOn(Cesium3DTileset, "loadJson").and.callFake(function (tilesetUrl) {
          return originalLoadJson(tilesetUrl).then(function (tilesetJson) {
            var multiContent =
              tilesetJson.root.extensions["3DTILES_multiple_contents"];
            multiContent.content.forEach(function (content) {
              content.extensions = {
                "3DTILES_metadata": metadataExtension,
              };
            });

            tilesetJson.root.extensions["3DTILES_extension"] = otherExtension;
            return tilesetJson;
          });
        });

        return Cesium3DTilesTester.loadTileset(
          scene,
          implicitMultipleContentsUrl
        ).then(function (tileset) {
          // the placeholder tile does not have any extensions.
          var placeholderTile = tileset.root;
          var placeholderHeader = placeholderTile._header;
          expect(placeholderHeader.extensions).not.toBeDefined();
          expect(placeholderHeader.content.extensions).not.toBeDefined();

          var transcodedRoot = placeholderTile.children[0];
          var transcodedRootHeader = transcodedRoot._header;
          expect(transcodedRootHeader.extensions).toEqual({
            "3DTILES_extension": otherExtension,
          });
          expect(transcodedRootHeader.content.extensions).toEqual({
            "3DTILES_metadata": metadataExtension,
          });

          var childTiles = transcodedRoot.children;
          for (var i = 0; i < childTiles.length; i++) {
            var childTile = childTiles[i];

            var childTileHeader = childTile._header;
            expect(childTileHeader.extensions["3DTILES_extension"]).toEqual(
              otherExtension
            );

            var innerContentHeaders =
              childTileHeader.extensions["3DTILES_multiple_contents"].content;

            innerContentHeaders.forEach(function (header) {
              expect(header.extensions).toEqual({
                "3DTILES_metadata": metadataExtension,
              });
            });
          }
        });
      });
    });

    describe("3DTILES_metadata", function () {
      var implicitTilesetUrl =
        "Data/Cesium3DTiles/Implicit/ImplicitTileset/tileset.json";
      var implicitGroupMetadataUrl =
        "Data/Cesium3DTiles/Metadata/ImplicitGroupMetadata/tileset.json";
      var implicitHeightSemanticsUrl =
        "Data/Cesium3DTiles/Metadata/ImplicitHeightSemantics/tileset.json";
      var implicitS2HeightSemanticsUrl =
        "Data/Cesium3DTiles/Metadata/ImplicitHeightSemantics/s2-tileset.json";
      var implicitTileBoundingVolumeSemanticsUrl =
        "Data/Cesium3DTiles/Metadata/ImplicitTileBoundingVolumeSemantics/tileset.json";
      var implicitContentBoundingVolumeSemanticsUrl =
        "Data/Cesium3DTiles/Metadata/ImplicitContentBoundingVolumeSemantics/tileset.json";

      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, implicitTilesetUrl).then(
          function (tileset) {
            var content = tileset.root.content;
            content.groupMetadata = groupMetadata;
            expect(content.groupMetadata).toBe(groupMetadata);
          }
        );
      });

      it("group metadata gets transcoded correctly", function () {
        return Cesium3DTilesTester.loadTileset(
          scene,
          implicitGroupMetadataUrl
        ).then(function (tileset) {
          var placeholderTile = tileset.root;
          var subtreeRootTile = placeholderTile.children[0];
          var tiles = [];
          gatherTilesPreorder(subtreeRootTile, 0, 2, tiles);

          var groups = tileset.metadata.groups;
          var ground = groups.ground;
          expect(ground.getProperty("color")).toEqual(
            new Cartesian3(120, 68, 32)
          );
          expect(ground.getProperty("priority")).toBe(0);

          var sky = groups.sky;
          expect(sky.getProperty("color")).toEqual(
            new Cartesian3(206, 237, 242)
          );
          expect(sky.getProperty("priority")).toBe(1);

          tiles.forEach(function (tile) {
            if (tile.hasMultipleContents) {
              // child tiles have multiple contents
              var contents = tile.content.innerContents;
              expect(contents[0].groupMetadata).toBe(ground);
              expect(contents[1].groupMetadata).toBe(sky);
            } else {
              // parent tile is a single B3DM tile
              expect(tile.content.groupMetadata).toBe(ground);
            }
          });
        });
      });

      // view (lon, lat, height) = (0, 0, 0) from height meters above
      function viewCartographicOrigin(height) {
        var center = Cartesian3.fromDegrees(0.0, 0.0);
        var offset = new Cartesian3(0, 0, height);
        scene.camera.lookAt(center, offset);
      }

      it("uses height semantics to adjust region bounding volumes", function () {
        viewCartographicOrigin(10000);
        return Cesium3DTilesTester.loadTileset(
          scene,
          implicitHeightSemanticsUrl
        ).then(function (tileset) {
          var placeholderTile = tileset.root;
          var subtreeRootTile = placeholderTile.children[0];

          var implicitRegion =
            placeholderTile.implicitTileset.boundingVolume.region;
          var minimumHeight = implicitRegion[4];
          var maximumHeight = implicitRegion[5];

          // This tileset uses TILE_MINIMUM_HEIGHT and TILE_MAXIMUM_HEIGHT
          // to set tighter bounding volumes
          var tiles = [];
          gatherTilesPreorder(subtreeRootTile, 0, 3, tiles);
          for (var i = 0; i < tiles.length; i++) {
            var tileRegion = tiles[i].boundingVolume;
            expect(tileRegion.minimumHeight).toBeGreaterThan(minimumHeight);
            expect(tileRegion.maximumHeight).toBeLessThan(maximumHeight);
          }
        });
      });

      it("uses height semantics to adjust S2 bounding volumes", function () {
        viewCartographicOrigin(10000);
        return Cesium3DTilesTester.loadTileset(
          scene,
          implicitS2HeightSemanticsUrl
        ).then(function (tileset) {
          var placeholderTile = tileset.root;
          var subtreeRootTile = placeholderTile.children[0];

          var implicitS2Volume =
            placeholderTile.implicitTileset.boundingVolume.extensions[
              "3DTILES_bounding_volume_S2"
            ];
          var minimumHeight = implicitS2Volume.minimumHeight;
          var maximumHeight = implicitS2Volume.maximumHeight;

          // This tileset uses TILE_MINIMUM_HEIGHT and TILE_MAXIMUM_HEIGHT
          // to set tighter bounding volumes
          var tiles = [];
          gatherTilesPreorder(subtreeRootTile, 0, 3, tiles);
          for (var i = 0; i < tiles.length; i++) {
            var tileS2Volume = tiles[i].boundingVolume;
            expect(tileS2Volume.minimumHeight).toBeGreaterThan(minimumHeight);
            expect(tileS2Volume.maximumHeight).toBeLessThan(maximumHeight);
          }
        });
      });

      // get half the bounding cube width from the bounding box's
      // halfAxes matrix
      function getHalfWidth(boundingBox) {
        return boundingBox.boundingVolume.halfAxes[0];
      }

      it("ignores height semantics if the implicit volume is a box", function () {
        var cameraHeight = 100;
        var rootHalfWidth = 10;
        var originalLoadJson = Cesium3DTileset.loadJson;
        spyOn(Cesium3DTileset, "loadJson").and.callFake(function (tilesetUrl) {
          return originalLoadJson(tilesetUrl).then(function (tilesetJson) {
            tilesetJson.root.boundingVolume = {
              box: [
                Ellipsoid.WGS84.radii.x + cameraHeight,
                0,
                0,
                rootHalfWidth,
                0,
                0,
                0,
                rootHalfWidth,
                0,
                0,
                0,
                rootHalfWidth,
              ],
            };
            return tilesetJson;
          });
        });

        viewCartographicOrigin(cameraHeight);
        return Cesium3DTilesTester.loadTileset(
          scene,
          implicitHeightSemanticsUrl
        ).then(function (tileset) {
          var placeholderTile = tileset.root;
          var subtreeRootTile = placeholderTile.children[0];

          var tiles = [];
          gatherTilesPreorder(subtreeRootTile, 0, 3, tiles);

          // TILE_MINIMUM_HEIGHT and TILE_MAXIMUM_HEIGHT only apply to
          // regions, so this will check that they are not used.
          tiles.forEach(function (tile) {
            var level = tile.implicitCoordinates.level;
            var halfWidth = getHalfWidth(tile.boundingVolume);
            // Even for floats, divide by 2 operations are exact as long
            // as there is no overflow.
            expect(halfWidth).toEqual(rootHalfWidth / Math.pow(2, level));
          });
        });
      });

      it("uses tile bounding volume from metadata semantics if present", function () {
        viewCartographicOrigin(124000);
        return Cesium3DTilesTester.loadTileset(
          scene,
          implicitTileBoundingVolumeSemanticsUrl
        ).then(function (tileset) {
          var placeholderTile = tileset.root.children[0];
          var subtreeRootTile = placeholderTile.children[0];

          var rootHalfWidth = 2048;
          expect(getHalfWidth(subtreeRootTile.boundingVolume)).toBe(
            rootHalfWidth
          );

          for (var level = 1; level < 4; level++) {
            var halfWidthAtLevel = rootHalfWidth / (1 << level);
            var tiles = [];
            gatherTilesPreorder(subtreeRootTile, level, level, tiles);
            for (var i = 0; i < tiles.length; i++) {
              // In this tileset, each tile's TILE_BOUNDING_BOX is
              // smaller than the implicit tile bounds. Make sure
              // this is true.
              var tile = tiles[i];
              var halfWidth = getHalfWidth(tile.boundingVolume);
              expect(halfWidth).toBeLessThan(halfWidthAtLevel);
            }
          }
        });
      });

      it("uses content bounding volume from metadata semantics if present", function () {
        viewCartographicOrigin(124000);
        return Cesium3DTilesTester.loadTileset(
          scene,
          implicitContentBoundingVolumeSemanticsUrl
        ).then(function (tileset) {
          var placeholderTile = tileset.root.children[0];
          var subtreeRootTile = placeholderTile.children[0];

          // This tileset defines the content bounding spheres in a
          // property with metadata semantic CONTENT_BOUNDING_SPHERE.
          // Check that each tile has a content bounding volume.
          var tiles = [];
          gatherTilesPreorder(subtreeRootTile, 0, 3, tiles);
          tiles.forEach(function (tile) {
            expect(
              tile.contentBoundingVolume instanceof TileBoundingSphere
            ).toBe(true);
            expect(tile.contentBoundingVolume).not.toBe(tile.boundingVolume);
          });
        });
      });
    });
  },
  "WebGL"
);
