#include "Cesium3DTilesSelection/registerAllTileContentTypes.h"
#include "Cesium3DTilesSelection/spdlog-cesium.h"
#include "QuantizedMeshLoader.h"

#include <CesiumGeometry/QuadtreeTilingScheme.h>
#include <CesiumGeometry/Rectangle.h>
#include <CesiumGeospatial/GeographicProjection.h>
#include <CesiumGeospatial/Projection.h>
#include <CesiumGltf/AccessorView.h>
#include <CesiumUtility/Math.h>

#include <catch2/catch.hpp>
#include <glm/glm.hpp>

#include <vector>

using namespace Cesium3DTilesSelection;
using namespace CesiumGeometry;
using namespace CesiumGeospatial;
using namespace CesiumGltf;
using namespace CesiumUtility;

struct QuantizedMeshHeader {
  double centerX;
  double centerY;
  double centerZ;

  float minimumHeight;
  float maximumHeight;

  double boundingSphereCenterX;
  double boundingSphereCenterY;
  double boundingSphereCenterZ;
  double boundingSphereRadius;

  double horizonOcclusionPointX;
  double horizonOcclusionPointY;
  double horizonOcclusionPointZ;
};

template <typename T> struct MeshData {
  std::vector<uint16_t> u;
  std::vector<uint16_t> v;
  std::vector<uint16_t> height;
  std::vector<T> indices;
  std::vector<T> westIndices;
  std::vector<T> southIndices;
  std::vector<T> eastIndices;
  std::vector<T> northIndices;
};

struct Extension {
  uint8_t extensionID;
  std::vector<std::byte> extensionData;
};

template <typename T> struct QuantizedMesh {
  QuantizedMeshHeader header;
  MeshData<T> vertexData;
  std::vector<Extension> extensions;
};

static uint32_t index2DTo1D(uint32_t x, uint32_t y, uint32_t width) {
  return y * width + x;
}

static uint16_t zigzagEncode(int16_t n) {
  return static_cast<uint16_t>((((uint16_t)n << 1) ^ (n >> 15)) & 0xFFFF);
}

static int32_t zigZagDecode(int32_t value) {
  return (value >> 1) ^ (-(value & 1));
}

static void octEncode(glm::vec3 normal, uint8_t& x, uint8_t& y) {
  float inv =
      1.0f / (glm::abs(normal.x) + glm::abs(normal.y) + glm::abs(normal.z));
  glm::vec2 p;
  p.x = normal.x * inv;
  p.y = normal.y * inv;

  if (normal.z <= 0.0) {
    x = static_cast<uint8_t>(Math::toSNorm(
        (1.0f - glm::abs(p.y)) * static_cast<float>(Math::signNotZero(p.x))));
    y = static_cast<uint8_t>(Math::toSNorm(
        (1.0f - glm::abs(p.x)) * static_cast<float>(Math::signNotZero(p.y))));
  } else {
    x = static_cast<uint8_t>(Math::toSNorm(p.x));
    y = static_cast<uint8_t>(Math::toSNorm(p.y));
  }
}

static double calculateSkirtHeight(
    int tileLevel,
    const CesiumGeospatial::Ellipsoid& ellipsoid,
    const QuadtreeTilingScheme& tilingScheme) {
  static const double terrainHeightmapQuality = 0.25;
  static const uint32_t heightmapWidth = 65;
  double levelZeroMaximumGeometricError =
      ellipsoid.getMaximumRadius() * CesiumUtility::Math::TwoPi *
      terrainHeightmapQuality / (heightmapWidth * tilingScheme.getRootTilesX());

  double levelMaximumGeometricError =
      levelZeroMaximumGeometricError / (1 << tileLevel);
  return levelMaximumGeometricError * 5.0;
}

template <typename T>
static std::vector<std::byte>
convertQuantizedMeshToBinary(const QuantizedMesh<T>& quantizedMesh) {
  // compute the total size of mesh to preallocate
  size_t totalSize = sizeof(quantizedMesh.header) +
                     sizeof(uint32_t) + // vertex data
                     quantizedMesh.vertexData.u.size() * sizeof(uint16_t) +
                     quantizedMesh.vertexData.v.size() * sizeof(uint16_t) +
                     quantizedMesh.vertexData.height.size() * sizeof(uint16_t) +
                     sizeof(uint32_t) + // indices data
                     quantizedMesh.vertexData.indices.size() * sizeof(T) +
                     sizeof(uint32_t) + // west edge
                     quantizedMesh.vertexData.westIndices.size() * sizeof(T) +
                     sizeof(uint32_t) + // south edge
                     quantizedMesh.vertexData.southIndices.size() * sizeof(T) +
                     sizeof(uint32_t) + // east edge
                     quantizedMesh.vertexData.eastIndices.size() * sizeof(T) +
                     sizeof(uint32_t) + // north edge
                     quantizedMesh.vertexData.northIndices.size() * sizeof(T);

  for (const Extension& extension : quantizedMesh.extensions) {
    totalSize +=
        sizeof(uint8_t) + sizeof(uint32_t) + extension.extensionData.size();
  }

  size_t offset = 0;
  size_t length = 0;
  std::vector<std::byte> buffer(totalSize);

  // serialize header
  length = sizeof(quantizedMesh.header);
  std::memcpy(
      buffer.data(),
      &quantizedMesh.header,
      sizeof(quantizedMesh.header));

  // vertex count
  offset += length;
  uint32_t vertexCount =
      static_cast<uint32_t>(quantizedMesh.vertexData.u.size());
  length = sizeof(vertexCount);
  std::memcpy(buffer.data() + offset, &vertexCount, length);

  // u buffer
  offset += length;
  length = quantizedMesh.vertexData.u.size() * sizeof(uint16_t);
  std::memcpy(
      buffer.data() + offset,
      quantizedMesh.vertexData.u.data(),
      length);

  // v buffer
  offset += length;
  length = quantizedMesh.vertexData.v.size() * sizeof(uint16_t);
  std::memcpy(
      buffer.data() + offset,
      quantizedMesh.vertexData.v.data(),
      length);

  // height buffer
  offset += length;
  length = quantizedMesh.vertexData.height.size() * sizeof(uint16_t);
  std::memcpy(
      buffer.data() + offset,
      quantizedMesh.vertexData.height.data(),
      length);

  // triangle count
  offset += length;
  uint32_t triangleCount =
      static_cast<uint32_t>(quantizedMesh.vertexData.indices.size()) / 3;
  length = sizeof(triangleCount);
  std::memcpy(buffer.data() + offset, &triangleCount, length);

  // indices buffer
  offset += length;
  length = quantizedMesh.vertexData.indices.size() * sizeof(T);
  std::memcpy(
      buffer.data() + offset,
      quantizedMesh.vertexData.indices.data(),
      length);

  // west edge count
  offset += length;
  uint32_t westIndicesCount =
      static_cast<uint32_t>(quantizedMesh.vertexData.westIndices.size());
  length = sizeof(westIndicesCount);
  std::memcpy(buffer.data() + offset, &westIndicesCount, length);

  // west edge buffer
  offset += length;
  length = quantizedMesh.vertexData.westIndices.size() * sizeof(T);
  std::memcpy(
      buffer.data() + offset,
      quantizedMesh.vertexData.westIndices.data(),
      length);

  // south edge count
  offset += length;
  uint32_t southIndicesCount =
      static_cast<uint32_t>(quantizedMesh.vertexData.southIndices.size());
  length = sizeof(southIndicesCount);
  std::memcpy(buffer.data() + offset, &southIndicesCount, length);

  // south edge buffer
  offset += length;
  length = quantizedMesh.vertexData.southIndices.size() * sizeof(T);
  std::memcpy(
      buffer.data() + offset,
      quantizedMesh.vertexData.southIndices.data(),
      length);

  // east edge count
  offset += length;
  uint32_t eastIndicesCount =
      static_cast<uint32_t>(quantizedMesh.vertexData.eastIndices.size());
  length = sizeof(eastIndicesCount);
  std::memcpy(buffer.data() + offset, &eastIndicesCount, length);

  // east edge buffer
  offset += length;
  length = quantizedMesh.vertexData.eastIndices.size() * sizeof(T);
  std::memcpy(
      buffer.data() + offset,
      quantizedMesh.vertexData.eastIndices.data(),
      length);

  // north edge count
  offset += length;
  uint32_t northIndicesCount =
      static_cast<uint32_t>(quantizedMesh.vertexData.northIndices.size());
  length = sizeof(northIndicesCount);
  std::memcpy(buffer.data() + offset, &northIndicesCount, length);

  // north edge buffer
  offset += length;
  length = quantizedMesh.vertexData.northIndices.size() * sizeof(T);
  std::memcpy(
      buffer.data() + offset,
      quantizedMesh.vertexData.northIndices.data(),
      length);

  // serialize extension
  for (const Extension& extension : quantizedMesh.extensions) {
    // serialize extension ID
    offset += length;
    length = sizeof(extension.extensionID);
    std::memcpy(buffer.data() + offset, &extension.extensionID, length);

    // serialize extension length
    offset += length;
    uint32_t extensionLength =
        static_cast<uint32_t>(extension.extensionData.size());
    length = sizeof(extensionLength);
    std::memcpy(buffer.data() + offset, &extensionLength, length);

    // serialize extension data
    offset += length;
    length = extension.extensionData.size();
    std::memcpy(buffer.data() + offset, extension.extensionData.data(), length);
  }

  return buffer;
}

template <class T>
static QuantizedMesh<T> createGridQuantizedMesh(
    const BoundingRegion& region,
    uint32_t width,
    uint32_t height) {
  if (width * height > std::numeric_limits<T>::max()) {
    throw std::invalid_argument("Number of vertices can be overflowed");
  }

  QuantizedMesh<T> quantizedMesh;
  const Ellipsoid& ellipsoid = Ellipsoid::WGS84;
  Cartographic cartoCenter = region.getRectangle().computeCenter();
  glm::dvec3 center = ellipsoid.cartographicToCartesian(cartoCenter);
  glm::dvec3 corner =
      ellipsoid.cartographicToCartesian(region.getRectangle().getNortheast());

  quantizedMesh.header.centerX = center.x;
  quantizedMesh.header.centerY = center.y;
  quantizedMesh.header.centerZ = center.z;

  quantizedMesh.header.minimumHeight =
      static_cast<float>(region.getMinimumHeight());
  quantizedMesh.header.maximumHeight =
      static_cast<float>(region.getMaximumHeight());

  quantizedMesh.header.boundingSphereCenterX = center.x;
  quantizedMesh.header.boundingSphereCenterY = center.y;
  quantizedMesh.header.boundingSphereCenterZ = center.z;
  quantizedMesh.header.boundingSphereRadius = glm::distance(center, corner);

  quantizedMesh.header.horizonOcclusionPointX = 0.0;
  quantizedMesh.header.horizonOcclusionPointY = 0.0;
  quantizedMesh.header.horizonOcclusionPointZ = 0.0;

  uint16_t lastU = 0;
  uint16_t lastV = 0;

  for (uint32_t y = 0; y < height; ++y) {
    for (uint32_t x = 0; x < width; ++x) {
      // encode u, v, and height buffers
      uint16_t u = static_cast<uint16_t>(
          (static_cast<double>(x) / static_cast<double>(width - 1)) * 32767.0);
      uint16_t v = static_cast<uint16_t>(
          ((static_cast<double>(y) / static_cast<double>(height - 1))) *
          32767.0);
      int16_t deltaU = static_cast<int16_t>(u - lastU);
      int16_t deltaV = static_cast<int16_t>(v - lastV);
      quantizedMesh.vertexData.u.emplace_back(zigzagEncode(deltaU));
      quantizedMesh.vertexData.v.emplace_back(zigzagEncode(deltaV));
      quantizedMesh.vertexData.height.push_back(0);

      lastU = u;
      lastV = v;

      if (x < width - 1 && y < height - 1) {
        quantizedMesh.vertexData.indices.emplace_back(
            static_cast<T>(index2DTo1D(x, y, width)));
        quantizedMesh.vertexData.indices.emplace_back(
            static_cast<T>(index2DTo1D(x + 1, y, width)));
        quantizedMesh.vertexData.indices.emplace_back(
            static_cast<T>(index2DTo1D(x, y + 1, width)));

        quantizedMesh.vertexData.indices.emplace_back(
            static_cast<T>(index2DTo1D(x + 1, y, width)));
        quantizedMesh.vertexData.indices.emplace_back(
            static_cast<T>(index2DTo1D(x + 1, y + 1, width)));
        quantizedMesh.vertexData.indices.emplace_back(
            static_cast<T>(index2DTo1D(x, y + 1, width)));
      }

      if (y == 0) {
        quantizedMesh.vertexData.southIndices.emplace_back(
            static_cast<T>(index2DTo1D(x, y, width)));
      }

      if (y == height - 1) {
        quantizedMesh.vertexData.northIndices.emplace_back(
            static_cast<T>(index2DTo1D(x, y, width)));
      }

      if (x == 0) {
        quantizedMesh.vertexData.westIndices.emplace_back(
            static_cast<T>(index2DTo1D(x, y, width)));
      }

      if (x == width - 1) {
        quantizedMesh.vertexData.eastIndices.emplace_back(
            static_cast<T>(index2DTo1D(x, y, width)));
      }
    }
  }

  // hight water mark encoding indices
  T hightWatermark = 0;
  for (T& index : quantizedMesh.vertexData.indices) {
    T originalIndex = index;
    index = static_cast<T>(hightWatermark - index);
    if (originalIndex == hightWatermark) {
      ++hightWatermark;
    }
  }

  return quantizedMesh;
}

template <class T, class I>
void checkGridMesh(
    const QuantizedMesh<T>& quantizedMesh,
    const AccessorView<I>& indices,
    const AccessorView<glm::vec3>& positions,
    const QuadtreeTilingScheme& tilingScheme,
    const Ellipsoid& ellipsoid,
    const CesiumGeometry::Rectangle& tileRectangle,
    uint32_t verticesWidth,
    uint32_t verticesHeight) {
  double west = tileRectangle.minimumX;
  double south = tileRectangle.minimumY;
  double east = tileRectangle.maximumX;
  double north = tileRectangle.maximumY;

  // check grid mesh without skirt
  const std::vector<uint16_t>& uBuffer = quantizedMesh.vertexData.u;
  const std::vector<uint16_t>& vBuffer = quantizedMesh.vertexData.v;
  int32_t u = 0;
  int32_t v = 0;

  std::vector<glm::dvec2> uvs;
  uvs.reserve(verticesWidth * verticesHeight);
  uint32_t positionIdx = 0;
  uint32_t idx = 0;
  for (uint32_t y = 0; y < verticesHeight; ++y) {
    for (uint32_t x = 0; x < verticesWidth; ++x) {
      u += zigZagDecode(uBuffer[positionIdx]);
      v += zigZagDecode(vBuffer[positionIdx]);

      // check that u ratio and v ratio is similar to grid ratio
      double uRatio = static_cast<double>(u) / 32767.0;
      double vRatio = static_cast<double>(v) / 32767.0;
      REQUIRE(Math::equalsEpsilon(
          uRatio,
          static_cast<double>(x) / static_cast<double>(verticesWidth - 1),
          Math::Epsilon4));
      REQUIRE(Math::equalsEpsilon(
          vRatio,
          static_cast<double>(y) / static_cast<double>(verticesHeight - 1),
          Math::Epsilon4));

      // check grid positions
      double longitude = Math::lerp(west, east, uRatio);
      double latitude = Math::lerp(south, north, vRatio);
      glm::dvec3 expectPosition =
          ellipsoid.cartographicToCartesian(Cartographic(longitude, latitude));

      glm::dvec3 position = static_cast<glm::dvec3>(positions[positionIdx]);
      position += glm::dvec3(
          quantizedMesh.header.boundingSphereCenterX,
          quantizedMesh.header.boundingSphereCenterY,
          quantizedMesh.header.boundingSphereCenterZ);

      REQUIRE(
          Math::equalsEpsilon(position.x, expectPosition.x, Math::Epsilon3));
      REQUIRE(
          Math::equalsEpsilon(position.y, expectPosition.y, Math::Epsilon3));
      REQUIRE(
          Math::equalsEpsilon(position.z, expectPosition.z, Math::Epsilon3));
      ++positionIdx;

      // check indices
      if (x < verticesWidth - 1 && y < verticesHeight - 1) {
        REQUIRE(
            indices[idx++] == static_cast<I>(index2DTo1D(x, y, verticesWidth)));
        REQUIRE(
            indices[idx++] ==
            static_cast<I>(index2DTo1D(x + 1, y, verticesWidth)));
        REQUIRE(
            indices[idx++] ==
            static_cast<I>(index2DTo1D(x, y + 1, verticesWidth)));

        REQUIRE(
            indices[idx++] ==
            static_cast<I>(index2DTo1D(x + 1, y, verticesWidth)));
        REQUIRE(
            indices[idx++] ==
            static_cast<I>(index2DTo1D(x + 1, y + 1, verticesWidth)));
        REQUIRE(
            indices[idx++] ==
            static_cast<I>(index2DTo1D(x, y + 1, verticesWidth)));
      }

      uvs.emplace_back(uRatio, vRatio);
    }
  }

  // make sure there are skirts in there
  size_t westIndicesCount = quantizedMesh.vertexData.westIndices.size();
  size_t southIndicesCount = quantizedMesh.vertexData.southIndices.size();
  size_t eastIndicesCount = quantizedMesh.vertexData.eastIndices.size();
  size_t northIndicesCount = quantizedMesh.vertexData.northIndices.size();

  size_t gridVerticesCount = verticesWidth * verticesHeight;
  size_t gridIndicesCount = (verticesHeight - 1) * (verticesWidth - 1) * 6;
  size_t totalSkirtVertices = westIndicesCount + southIndicesCount +
                              eastIndicesCount + northIndicesCount;
  size_t totalSkirtIndices = (totalSkirtVertices - 4) * 6;

  double skirtHeight = calculateSkirtHeight(10, ellipsoid, tilingScheme);
  double longitudeOffset = (west - east) * 0.0001;
  double latitudeOffset = (north - south) * 0.0001;

  REQUIRE(totalSkirtIndices == size_t(indices.size()) - gridIndicesCount);
  REQUIRE(totalSkirtVertices == size_t(positions.size()) - gridVerticesCount);

  size_t currentVertexCount = gridVerticesCount;
  for (size_t i = 0; i < westIndicesCount; ++i) {
    T westIndex = quantizedMesh.vertexData.westIndices[i];
    double longitude = west + longitudeOffset;
    double latitude = Math::lerp(south, north, uvs[westIndex].y);
    glm::dvec3 expectPosition = ellipsoid.cartographicToCartesian(
        Cartographic(longitude, latitude, -skirtHeight));

    glm::dvec3 position =
        static_cast<glm::dvec3>(positions[int64_t(currentVertexCount + i)]);
    position += glm::dvec3(
        quantizedMesh.header.boundingSphereCenterX,
        quantizedMesh.header.boundingSphereCenterY,
        quantizedMesh.header.boundingSphereCenterZ);
    REQUIRE(Math::equalsEpsilon(position.x, expectPosition.x, Math::Epsilon3));
    REQUIRE(Math::equalsEpsilon(position.y, expectPosition.y, Math::Epsilon3));
    REQUIRE(Math::equalsEpsilon(position.z, expectPosition.z, Math::Epsilon3));
  }

  currentVertexCount += westIndicesCount;
  for (size_t i = 0; i < southIndicesCount; ++i) {
    T southIndex =
        quantizedMesh.vertexData.southIndices[southIndicesCount - 1 - i];
    double longitude = Math::lerp(west, east, uvs[southIndex].x);
    double latitude = south - latitudeOffset;
    glm::dvec3 expectPosition = ellipsoid.cartographicToCartesian(
        Cartographic(longitude, latitude, -skirtHeight));

    glm::dvec3 position =
        static_cast<glm::dvec3>(positions[int64_t(currentVertexCount + i)]);
    position += glm::dvec3(
        quantizedMesh.header.boundingSphereCenterX,
        quantizedMesh.header.boundingSphereCenterY,
        quantizedMesh.header.boundingSphereCenterZ);
    REQUIRE(Math::equalsEpsilon(position.x, expectPosition.x, Math::Epsilon3));
    REQUIRE(Math::equalsEpsilon(position.y, expectPosition.y, Math::Epsilon3));
    REQUIRE(Math::equalsEpsilon(position.z, expectPosition.z, Math::Epsilon3));
  }

  currentVertexCount += southIndicesCount;
  for (size_t i = 0; i < eastIndicesCount; ++i) {
    T eastIndex =
        quantizedMesh.vertexData.eastIndices[eastIndicesCount - 1 - i];
    double longitude = east + longitudeOffset;
    double latitude = Math::lerp(south, north, uvs[eastIndex].y);
    glm::dvec3 expectPosition = ellipsoid.cartographicToCartesian(
        Cartographic(longitude, latitude, -skirtHeight));

    glm::dvec3 position =
        static_cast<glm::dvec3>(positions[int64_t(currentVertexCount + i)]);
    position += glm::dvec3(
        quantizedMesh.header.boundingSphereCenterX,
        quantizedMesh.header.boundingSphereCenterY,
        quantizedMesh.header.boundingSphereCenterZ);
    REQUIRE(Math::equalsEpsilon(position.x, expectPosition.x, Math::Epsilon3));
    REQUIRE(Math::equalsEpsilon(position.y, expectPosition.y, Math::Epsilon2));
    REQUIRE(Math::equalsEpsilon(position.z, expectPosition.z, Math::Epsilon3));
  }

  currentVertexCount += eastIndicesCount;
  for (size_t i = 0; i < northIndicesCount; ++i) {
    T northIndex = quantizedMesh.vertexData.northIndices[i];
    double longitude = Math::lerp(west, east, uvs[northIndex].x);
    double latitude = north + latitudeOffset;
    glm::dvec3 expectPosition = ellipsoid.cartographicToCartesian(
        Cartographic(longitude, latitude, -skirtHeight));

    glm::dvec3 position =
        static_cast<glm::dvec3>(positions[int64_t(currentVertexCount + i)]);
    position += glm::dvec3(
        quantizedMesh.header.boundingSphereCenterX,
        quantizedMesh.header.boundingSphereCenterY,
        quantizedMesh.header.boundingSphereCenterZ);
    REQUIRE(Math::equalsEpsilon(position.x, expectPosition.x, Math::Epsilon3));
    REQUIRE(Math::equalsEpsilon(position.y, expectPosition.y, Math::Epsilon3));
    REQUIRE(Math::equalsEpsilon(position.z, expectPosition.z, Math::Epsilon3));
  }
}

template <class T, class I>
static void checkGeneratedGridNormal(
    const QuantizedMesh<T>& quantizedMesh,
    const AccessorView<glm::vec3>& normals,
    const AccessorView<glm::vec3>& positions,
    const AccessorView<I>& indices,
    const glm::vec3& geodeticNormal,
    uint32_t verticesWidth,
    uint32_t verticesHeight) {
  uint32_t totalGridIndices = (verticesWidth - 1) * (verticesHeight - 1) * 6;
  std::vector<glm::vec3> expectedNormals(verticesWidth * verticesHeight);
  for (uint32_t i = 0; i < totalGridIndices; i += 3) {
    I id0 = indices[i];
    I id1 = indices[i + 1];
    I id2 = indices[i + 2];

    glm::vec3 p0 = positions[id0];
    glm::vec3 p1 = positions[id1];
    glm::vec3 p2 = positions[id2];

    glm::vec3 normal = glm::cross(p1 - p0, p2 - p0);
    expectedNormals[id0] += normal;
    expectedNormals[id1] += normal;
    expectedNormals[id2] += normal;
  }

  for (size_t i = 0; i < expectedNormals.size(); ++i) {
    glm::vec3& expectedNormal = expectedNormals[i];
    glm::vec3 normal = normals[int64_t(i)];

    if (!Math::equalsEpsilon(
            glm::dot(expectedNormals[i], expectedNormals[i]),
            0.0,
            Math::Epsilon7)) {
      expectedNormal = glm::normalize(expectedNormals[i]);

      // make sure normal points to the direction of geodetic normal for grid
      // only
      REQUIRE(glm::dot(normal, geodeticNormal) >= 0.0);

      REQUIRE(Math::equalsEpsilon(normal.x, expectedNormal.x, Math::Epsilon7));
      REQUIRE(Math::equalsEpsilon(normal.y, expectedNormal.y, Math::Epsilon7));
      REQUIRE(Math::equalsEpsilon(normal.z, expectedNormal.z, Math::Epsilon7));
    } else {
      REQUIRE(Math::equalsEpsilon(normal.x, expectedNormal.x, Math::Epsilon7));
      REQUIRE(Math::equalsEpsilon(normal.y, expectedNormal.y, Math::Epsilon7));
      REQUIRE(Math::equalsEpsilon(normal.z, expectedNormal.z, Math::Epsilon7));
    }
  }

  // make sure there are skirts in there
  size_t westIndicesCount = quantizedMesh.vertexData.westIndices.size();
  size_t southIndicesCount = quantizedMesh.vertexData.southIndices.size();
  size_t eastIndicesCount = quantizedMesh.vertexData.eastIndices.size();
  size_t northIndicesCount = quantizedMesh.vertexData.northIndices.size();

  size_t gridVerticesCount = verticesWidth * verticesHeight;
  size_t totalSkirtVertices = westIndicesCount + southIndicesCount +
                              eastIndicesCount + northIndicesCount;

  REQUIRE(totalSkirtVertices == size_t(normals.size()) - gridVerticesCount);

  size_t currentVertexCount = gridVerticesCount;
  uint32_t x = 0;
  uint32_t y = 0;
  for (size_t i = 0; i < westIndicesCount; ++i) {
    glm::vec3 normal = normals[int64_t(currentVertexCount + i)];
    glm::vec3 expectedNormal =
        expectedNormals[index2DTo1D(x, y, verticesWidth)];
    REQUIRE(Math::equalsEpsilon(normal.x, expectedNormal.x, Math::Epsilon7));
    REQUIRE(Math::equalsEpsilon(normal.y, expectedNormal.y, Math::Epsilon7));
    REQUIRE(Math::equalsEpsilon(normal.z, expectedNormal.z, Math::Epsilon7));

    ++y;
  }

  currentVertexCount += westIndicesCount;
  x = verticesWidth - 1;
  y = 0;
  for (size_t i = 0; i < southIndicesCount; ++i) {
    glm::vec3 normal = normals[int64_t(currentVertexCount + i)];
    glm::vec3 expectedNormal =
        expectedNormals[index2DTo1D(x, y, verticesWidth)];
    REQUIRE(Math::equalsEpsilon(normal.x, expectedNormal.x, Math::Epsilon7));
    REQUIRE(Math::equalsEpsilon(normal.y, expectedNormal.y, Math::Epsilon7));
    REQUIRE(Math::equalsEpsilon(normal.z, expectedNormal.z, Math::Epsilon7));

    --x;
  }

  currentVertexCount += southIndicesCount;
  x = verticesWidth - 1;
  y = verticesHeight - 1;
  for (size_t i = 0; i < eastIndicesCount; ++i) {
    glm::vec3 normal = normals[int64_t(currentVertexCount + i)];
    glm::vec3 expectedNormal =
        expectedNormals[index2DTo1D(x, y, verticesWidth)];
    REQUIRE(Math::equalsEpsilon(normal.x, expectedNormal.x, Math::Epsilon7));
    REQUIRE(Math::equalsEpsilon(normal.y, expectedNormal.y, Math::Epsilon7));
    REQUIRE(Math::equalsEpsilon(normal.z, expectedNormal.z, Math::Epsilon7));

    --y;
  }

  currentVertexCount += eastIndicesCount;
  x = 0;
  y = verticesHeight - 1;
  for (size_t i = 0; i < northIndicesCount; ++i) {
    glm::vec3 normal = normals[int64_t(currentVertexCount + i)];
    glm::vec3 expectedNormal =
        expectedNormals[index2DTo1D(x, y, verticesWidth)];
    REQUIRE(Math::equalsEpsilon(normal.x, expectedNormal.x, Math::Epsilon7));
    REQUIRE(Math::equalsEpsilon(normal.y, expectedNormal.y, Math::Epsilon7));
    REQUIRE(Math::equalsEpsilon(normal.z, expectedNormal.z, Math::Epsilon7));

    ++x;
  }
}

TEST_CASE("Test converting quantized mesh to gltf with skirt") {
  registerAllTileContentTypes();

  // mock context
  Ellipsoid ellipsoid = Ellipsoid::WGS84;
  CesiumGeometry::Rectangle rectangle(
      glm::radians(-180.0),
      glm::radians(-90.0),
      glm::radians(180.0),
      glm::radians(90.0));
  QuadtreeTilingScheme tilingScheme(rectangle, 2, 1);

  SECTION("Check quantized mesh that has uint16_t indices") {
    // mock quantized mesh
    uint32_t verticesWidth = 3;
    uint32_t verticesHeight = 3;
    QuadtreeTileID tileID(10, 0, 0);
    CesiumGeometry::Rectangle tileRectangle =
        tilingScheme.tileToRectangle(tileID);
    BoundingRegion boundingVolume = BoundingRegion(
        GlobeRectangle(
            tileRectangle.minimumX,
            tileRectangle.minimumY,
            tileRectangle.maximumX,
            tileRectangle.maximumY),
        0.0,
        0.0);
    QuantizedMesh<uint16_t> quantizedMesh = createGridQuantizedMesh<uint16_t>(
        boundingVolume,
        verticesWidth,
        verticesHeight);

    // convert to gltf
    std::vector<std::byte> quantizedMeshBin =
        convertQuantizedMeshToBinary(quantizedMesh);
    gsl::span<const std::byte> data(
        quantizedMeshBin.data(),
        quantizedMeshBin.size());
    QuantizedMeshLoadResult loadResult =
        QuantizedMeshLoader::load(tileID, boundingVolume, "url", data, false);
    REQUIRE(!loadResult.errors.hasErrors());
    REQUIRE(loadResult.model != std::nullopt);

    // make sure the gltf is the grid
    const CesiumGltf::Model& model = *loadResult.model;
    const CesiumGltf::Mesh& mesh = model.meshes.front();
    const CesiumGltf::MeshPrimitive& primitive = mesh.primitives.front();

    // make sure mesh contains grid mesh and skirts at the end
    AccessorView<uint16_t> indices(model, primitive.indices);
    CHECK(indices.status() == AccessorViewStatus::Valid);
    AccessorView<glm::vec3> positions(
        model,
        primitive.attributes.at("POSITION"));
    CHECK(positions.status() == AccessorViewStatus::Valid);

    checkGridMesh(
        quantizedMesh,
        indices,
        positions,
        tilingScheme,
        ellipsoid,
        tileRectangle,
        verticesWidth,
        verticesHeight);

    // check normal
    AccessorView<glm::vec3> normals(model, primitive.attributes.at("NORMAL"));
    CHECK(normals.status() == AccessorViewStatus::Valid);

    Cartographic center = boundingVolume.getRectangle().computeCenter();
    glm::vec3 geodeticNormal =
        static_cast<glm::vec3>(ellipsoid.geodeticSurfaceNormal(center));
    checkGeneratedGridNormal(
        quantizedMesh,
        normals,
        positions,
        indices,
        geodeticNormal,
        verticesWidth,
        verticesHeight);
  }

  SECTION("Check quantized mesh that has uint32_t indices") {
    // mock quantized mesh
    uint32_t verticesWidth = 300;
    uint32_t verticesHeight = 300;
    QuadtreeTileID tileID(10, 0, 0);
    CesiumGeometry::Rectangle tileRectangle =
        tilingScheme.tileToRectangle(tileID);
    BoundingRegion boundingVolume = BoundingRegion(
        GlobeRectangle(
            tileRectangle.minimumX,
            tileRectangle.minimumY,
            tileRectangle.maximumX,
            tileRectangle.maximumY),
        0.0,
        0.0);
    QuantizedMesh<uint32_t> quantizedMesh = createGridQuantizedMesh<uint32_t>(
        boundingVolume,
        verticesWidth,
        verticesHeight);

    // convert to gltf
    std::vector<std::byte> quantizedMeshBin =
        convertQuantizedMeshToBinary(quantizedMesh);
    gsl::span<const std::byte> data(
        quantizedMeshBin.data(),
        quantizedMeshBin.size());
    auto loadResult =
        QuantizedMeshLoader::load(tileID, boundingVolume, "url", data, false);
    REQUIRE(!loadResult.errors.hasErrors());
    REQUIRE(loadResult.model != std::nullopt);

    // make sure the gltf is the grid
    const CesiumGltf::Model& model = *loadResult.model;
    const CesiumGltf::Mesh& mesh = model.meshes.front();
    const CesiumGltf::MeshPrimitive& primitive = mesh.primitives.front();

    // make sure mesh contains grid mesh and skirts at the end
    AccessorView<uint32_t> indices(model, primitive.indices);
    CHECK(indices.status() == AccessorViewStatus::Valid);
    AccessorView<glm::vec3> positions(
        model,
        primitive.attributes.at("POSITION"));
    CHECK(positions.status() == AccessorViewStatus::Valid);

    checkGridMesh(
        quantizedMesh,
        indices,
        positions,
        tilingScheme,
        ellipsoid,
        tileRectangle,
        verticesWidth,
        verticesHeight);

    // check normal
    AccessorView<glm::vec3> normals(model, primitive.attributes.at("NORMAL"));
    CHECK(normals.status() == AccessorViewStatus::Valid);

    Cartographic center = boundingVolume.getRectangle().computeCenter();
    glm::vec3 geodeticNormal =
        static_cast<glm::vec3>(ellipsoid.geodeticSurfaceNormal(center));
    checkGeneratedGridNormal(
        quantizedMesh,
        normals,
        positions,
        indices,
        geodeticNormal,
        verticesWidth,
        verticesHeight);
  }

  SECTION("Check 16 bit indices turns to 32 bits when adding skirt") {
    // mock quantized mesh
    uint32_t verticesWidth = 255;
    uint32_t verticesHeight = 255;
    QuadtreeTileID tileID(10, 0, 0);
    CesiumGeometry::Rectangle tileRectangle =
        tilingScheme.tileToRectangle(tileID);
    BoundingRegion boundingVolume = BoundingRegion(
        GlobeRectangle(
            tileRectangle.minimumX,
            tileRectangle.minimumY,
            tileRectangle.maximumX,
            tileRectangle.maximumY),
        0.0,
        0.0);
    QuantizedMesh<uint16_t> quantizedMesh = createGridQuantizedMesh<uint16_t>(
        boundingVolume,
        verticesWidth,
        verticesHeight);

    // convert to gltf
    std::vector<std::byte> quantizedMeshBin =
        convertQuantizedMeshToBinary(quantizedMesh);
    gsl::span<const std::byte> data(
        quantizedMeshBin.data(),
        quantizedMeshBin.size());
    auto loadResult =
        QuantizedMeshLoader::load(tileID, boundingVolume, "url", data, false);
    REQUIRE(!loadResult.errors.hasErrors());
    REQUIRE(loadResult.model != std::nullopt);

    // make sure the gltf is the grid
    const CesiumGltf::Model& model = *loadResult.model;
    const CesiumGltf::Mesh& mesh = model.meshes.front();
    const CesiumGltf::MeshPrimitive& primitive = mesh.primitives.front();

    // make sure mesh contains grid mesh and skirts at the end
    AccessorView<uint32_t> indices(model, primitive.indices);
    CHECK(indices.status() == AccessorViewStatus::Valid);
    AccessorView<glm::vec3> positions(
        model,
        primitive.attributes.at("POSITION"));
    CHECK(positions.status() == AccessorViewStatus::Valid);

    checkGridMesh(
        quantizedMesh,
        indices,
        positions,
        tilingScheme,
        ellipsoid,
        tileRectangle,
        verticesWidth,
        verticesHeight);

    // check normal
    AccessorView<glm::vec3> normals(model, primitive.attributes.at("NORMAL"));
    CHECK(normals.status() == AccessorViewStatus::Valid);

    Cartographic center = boundingVolume.getRectangle().computeCenter();
    glm::vec3 geodeticNormal =
        static_cast<glm::vec3>(ellipsoid.geodeticSurfaceNormal(center));
    checkGeneratedGridNormal(
        quantizedMesh,
        normals,
        positions,
        indices,
        geodeticNormal,
        verticesWidth,
        verticesHeight);
  }

  SECTION("Check quantized mesh that has oct normal") {
    // mock quantized mesh
    uint32_t verticesWidth = 3;
    uint32_t verticesHeight = 3;
    QuadtreeTileID tileID(10, 0, 0);
    CesiumGeometry::Rectangle tileRectangle =
        tilingScheme.tileToRectangle(tileID);
    BoundingRegion boundingVolume = BoundingRegion(
        GlobeRectangle(
            tileRectangle.minimumX,
            tileRectangle.minimumY,
            tileRectangle.maximumX,
            tileRectangle.maximumY),
        0.0,
        0.0);
    QuantizedMesh<uint16_t> quantizedMesh = createGridQuantizedMesh<uint16_t>(
        boundingVolume,
        verticesWidth,
        verticesHeight);

    // add oct-encoded normal extension. This is just a random direction and
    // not really normal. We want to make sure the normal is written to the gltf
    glm::vec3 normal = glm::normalize(glm::vec3(0.2, 1.4, 0.3));
    uint8_t x = 0, y = 0;
    octEncode(normal, x, y);
    std::vector<std::byte> octNormals(verticesWidth * verticesHeight * 2);
    for (size_t i = 0; i < octNormals.size(); i += 2) {
      octNormals[i] = std::byte(x);
      octNormals[i + 1] = std::byte(y);
    }

    Extension octNormalExtension;
    octNormalExtension.extensionID = 1;
    octNormalExtension.extensionData = std::move(octNormals);

    quantizedMesh.extensions.emplace_back(std::move(octNormalExtension));

    // convert to gltf
    std::vector<std::byte> quantizedMeshBin =
        convertQuantizedMeshToBinary(quantizedMesh);
    gsl::span<const std::byte> data(
        quantizedMeshBin.data(),
        quantizedMeshBin.size());
    auto loadResult =
        QuantizedMeshLoader::load(tileID, boundingVolume, "url", data, false);
    REQUIRE(!loadResult.errors.hasErrors());
    REQUIRE(loadResult.model != std::nullopt);

    // make sure the gltf has normals
    const CesiumGltf::Model& model = *loadResult.model;
    const CesiumGltf::Mesh& mesh = model.meshes.front();
    const CesiumGltf::MeshPrimitive& primitive = mesh.primitives.front();

    size_t westIndicesCount = quantizedMesh.vertexData.westIndices.size();
    size_t southIndicesCount = quantizedMesh.vertexData.southIndices.size();
    size_t eastIndicesCount = quantizedMesh.vertexData.eastIndices.size();
    size_t northIndicesCount = quantizedMesh.vertexData.northIndices.size();
    size_t totalSkirtVerticesCount = westIndicesCount + southIndicesCount +
                                     eastIndicesCount + northIndicesCount;

    AccessorView<glm::vec3> normals(model, primitive.attributes.at("NORMAL"));
    CHECK(normals.status() == AccessorViewStatus::Valid);

    REQUIRE(
        static_cast<size_t>(normals.size()) ==
        (verticesWidth * verticesHeight + totalSkirtVerticesCount));
    for (int64_t i = 0; i < normals.size(); ++i) {
      REQUIRE(Math::equalsEpsilon(normals[i].x, normal.x, Math::Epsilon2));
      REQUIRE(Math::equalsEpsilon(normals[i].y, normal.y, Math::Epsilon2));
      REQUIRE(Math::equalsEpsilon(normals[i].z, normal.z, Math::Epsilon2));
    }
  }
}

TEST_CASE("Test converting ill-formed quantized mesh") {
  registerAllTileContentTypes();

  // mock context
  CesiumGeometry::Rectangle rectangle(
      glm::radians(-180.0),
      glm::radians(-90.0),
      glm::radians(180.0),
      glm::radians(90.0));
  QuadtreeTilingScheme tilingScheme(rectangle, 2, 1);

  // mock quantized mesh
  uint32_t verticesWidth = 3;
  uint32_t verticesHeight = 3;
  QuadtreeTileID tileID(10, 0, 0);
  CesiumGeometry::Rectangle tileRectangle =
      tilingScheme.tileToRectangle(tileID);
  BoundingRegion boundingVolume = BoundingRegion(
      GlobeRectangle(
          tileRectangle.minimumX,
          tileRectangle.minimumY,
          tileRectangle.maximumX,
          tileRectangle.maximumY),
      0.0,
      0.0);
  QuantizedMesh<uint16_t> quantizedMesh = createGridQuantizedMesh<uint16_t>(
      boundingVolume,
      verticesWidth,
      verticesHeight);

  // add oct-encoded normal extension. This is just a random direction and not
  // really normal. We want to make sure the normal is written to the gltf
  glm::vec3 normal = glm::normalize(glm::vec3(0.2, 1.4, 0.3));
  uint8_t x = 0, y = 0;
  octEncode(normal, x, y);
  std::vector<std::byte> octNormals(verticesWidth * verticesHeight * 2);
  for (size_t i = 0; i < octNormals.size(); i += 2) {
    octNormals[i] = std::byte(x);
    octNormals[i + 1] = std::byte(y);
  }

  Extension octNormalExtension;
  octNormalExtension.extensionID = 1;
  octNormalExtension.extensionData = std::move(octNormals);

  quantizedMesh.extensions.emplace_back(std::move(octNormalExtension));

  SECTION("Quantized mesh with ill-formed header") {
    std::vector<std::byte> quantizedMeshBin(32);
    gsl::span<const std::byte> data(
        quantizedMeshBin.data(),
        quantizedMeshBin.size());
    auto loadResult =
        QuantizedMeshLoader::load(tileID, boundingVolume, "url", data, false);
    REQUIRE(loadResult.model == std::nullopt);
  }

  SECTION("Quantized mesh with ill-formed vertex data") {
    std::vector<std::byte> quantizedMeshBin(
        sizeof(quantizedMesh.header) +                       // header
        sizeof(uint32_t) +                                   // vertex count
        quantizedMesh.vertexData.u.size() * sizeof(uint16_t) // u buffer
    );

    // serialize header
    size_t offset = 0;
    size_t length = sizeof(quantizedMesh.header);
    std::memcpy(quantizedMeshBin.data(), &quantizedMesh.header, length);

    // serialize vertex count
    offset += length;
    uint32_t vertexCount =
        static_cast<uint32_t>(quantizedMesh.vertexData.u.size());
    length = sizeof(vertexCount);
    std::memcpy(quantizedMeshBin.data() + offset, &vertexCount, length);

    // serialize u buffer
    offset += length;
    length = quantizedMesh.vertexData.u.size() * sizeof(uint16_t);
    std::memcpy(
        quantizedMeshBin.data() + offset,
        quantizedMesh.vertexData.u.data(),
        length);

    gsl::span<const std::byte> data(
        quantizedMeshBin.data(),
        quantizedMeshBin.size());
    auto loadResult =
        QuantizedMeshLoader::load(tileID, boundingVolume, "url", data, false);

    REQUIRE(loadResult.model == std::nullopt);
  }

  SECTION("Quantized mesh with ill-formed indices") {
    std::vector<std::byte> quantizedMeshBin(
        sizeof(quantizedMesh.header) +                         // header
        sizeof(uint32_t) +                                     // vertex count
        quantizedMesh.vertexData.u.size() * sizeof(uint16_t) + // u buffer
        quantizedMesh.vertexData.v.size() * sizeof(uint16_t) + // v buffer
        quantizedMesh.vertexData.height.size() *
            sizeof(uint16_t) + // height buffer
        sizeof(uint32_t)       // triangle count
    );

    // serialize header
    size_t offset = 0;
    size_t length = sizeof(quantizedMesh.header);
    std::memcpy(quantizedMeshBin.data(), &quantizedMesh.header, length);

    // serialize vertex count
    offset += length;
    uint32_t vertexCount =
        static_cast<uint32_t>(quantizedMesh.vertexData.u.size());
    length = sizeof(vertexCount);
    std::memcpy(quantizedMeshBin.data() + offset, &vertexCount, length);

    // serialize u buffer
    offset += length;
    length = quantizedMesh.vertexData.u.size() * sizeof(uint16_t);
    std::memcpy(
        quantizedMeshBin.data() + offset,
        quantizedMesh.vertexData.u.data(),
        length);

    // serialize v buffer
    offset += length;
    length = quantizedMesh.vertexData.v.size() * sizeof(uint16_t);
    std::memcpy(
        quantizedMeshBin.data() + offset,
        quantizedMesh.vertexData.v.data(),
        length);

    // serialize height buffer
    offset += length;
    length = quantizedMesh.vertexData.height.size() * sizeof(uint16_t);
    std::memcpy(
        quantizedMeshBin.data() + offset,
        quantizedMesh.vertexData.height.data(),
        length);

    // serialize triangle count
    offset += length;
    uint32_t triangleCount =
        static_cast<uint32_t>(quantizedMesh.vertexData.indices.size() / 3);
    length = sizeof(triangleCount);
    std::memcpy(quantizedMeshBin.data() + offset, &triangleCount, length);

    gsl::span<const std::byte> data(
        quantizedMeshBin.data(),
        quantizedMeshBin.size());
    auto loadResult =
        QuantizedMeshLoader::load(tileID, boundingVolume, "url", data, false);

    REQUIRE(loadResult.model == std::nullopt);
  }

  SECTION("Quantized mesh with ill-formed west edge indices") {
    std::vector<std::byte> quantizedMeshBin(
        sizeof(quantizedMesh.header) +                         // header
        sizeof(uint32_t) +                                     // vertex count
        quantizedMesh.vertexData.u.size() * sizeof(uint16_t) + // u buffer
        quantizedMesh.vertexData.v.size() * sizeof(uint16_t) + // v buffer
        quantizedMesh.vertexData.height.size() *
            sizeof(uint16_t) + // height buffer
        sizeof(uint32_t) +     // triangle count
        quantizedMesh.vertexData.indices.size() * sizeof(uint16_t) +
        sizeof(uint32_t) // west edge
    );

    // serialize header
    size_t offset = 0;
    size_t length = sizeof(quantizedMesh.header);
    std::memcpy(quantizedMeshBin.data(), &quantizedMesh.header, length);

    // serialize vertex count
    offset += length;
    uint32_t vertexCount =
        static_cast<uint32_t>(quantizedMesh.vertexData.u.size());
    length = sizeof(vertexCount);
    std::memcpy(quantizedMeshBin.data() + offset, &vertexCount, length);

    // serialize u buffer
    offset += length;
    length = quantizedMesh.vertexData.u.size() * sizeof(uint16_t);
    std::memcpy(
        quantizedMeshBin.data() + offset,
        quantizedMesh.vertexData.u.data(),
        length);

    // serialize v buffer
    offset += length;
    length = quantizedMesh.vertexData.v.size() * sizeof(uint16_t);
    std::memcpy(
        quantizedMeshBin.data() + offset,
        quantizedMesh.vertexData.v.data(),
        length);

    // serialize height buffer
    offset += length;
    length = quantizedMesh.vertexData.height.size() * sizeof(uint16_t);
    std::memcpy(
        quantizedMeshBin.data() + offset,
        quantizedMesh.vertexData.height.data(),
        length);

    // serialize triangle count
    offset += length;
    uint32_t triangleCount =
        static_cast<uint32_t>(quantizedMesh.vertexData.indices.size() / 3);
    length = sizeof(triangleCount);
    std::memcpy(quantizedMeshBin.data() + offset, &triangleCount, length);

    // serialize indices
    offset += length;
    length = quantizedMesh.vertexData.indices.size() * sizeof(uint16_t);
    std::memcpy(
        quantizedMeshBin.data() + offset,
        quantizedMesh.vertexData.indices.data(),
        length);

    // serialize west edge
    offset += length;
    uint32_t westCount =
        static_cast<uint32_t>(quantizedMesh.vertexData.westIndices.size());
    length = sizeof(westCount);
    std::memcpy(quantizedMeshBin.data() + offset, &westCount, length);

    gsl::span<const std::byte> data(
        quantizedMeshBin.data(),
        quantizedMeshBin.size());
    auto loadResult =
        QuantizedMeshLoader::load(tileID, boundingVolume, "url", data, false);

    REQUIRE(loadResult.model == std::nullopt);
  }

  SECTION("Quantized mesh with ill-formed south edge indices") {
    std::vector<std::byte> quantizedMeshBin(
        sizeof(quantizedMesh.header) +                         // header
        sizeof(uint32_t) +                                     // vertex count
        quantizedMesh.vertexData.u.size() * sizeof(uint16_t) + // u buffer
        quantizedMesh.vertexData.v.size() * sizeof(uint16_t) + // v buffer
        quantizedMesh.vertexData.height.size() *
            sizeof(uint16_t) + // height buffer
        sizeof(uint32_t) +     // triangle count
        quantizedMesh.vertexData.indices.size() * sizeof(uint16_t) +
        sizeof(uint32_t) + // west edge
        quantizedMesh.vertexData.westIndices.size() * sizeof(uint16_t) +
        sizeof(uint32_t) // south edge
    );

    // serialize header
    size_t offset = 0;
    size_t length = sizeof(quantizedMesh.header);
    std::memcpy(quantizedMeshBin.data(), &quantizedMesh.header, length);

    // serialize vertex count
    offset += length;
    uint32_t vertexCount =
        static_cast<uint32_t>(quantizedMesh.vertexData.u.size());
    length = sizeof(vertexCount);
    std::memcpy(quantizedMeshBin.data() + offset, &vertexCount, length);

    // serialize u buffer
    offset += length;
    length = quantizedMesh.vertexData.u.size() * sizeof(uint16_t);
    std::memcpy(
        quantizedMeshBin.data() + offset,
        quantizedMesh.vertexData.u.data(),
        length);

    // serialize v buffer
    offset += length;
    length = quantizedMesh.vertexData.v.size() * sizeof(uint16_t);
    std::memcpy(
        quantizedMeshBin.data() + offset,
        quantizedMesh.vertexData.v.data(),
        length);

    // serialize height buffer
    offset += length;
    length = quantizedMesh.vertexData.height.size() * sizeof(uint16_t);
    std::memcpy(
        quantizedMeshBin.data() + offset,
        quantizedMesh.vertexData.height.data(),
        length);

    // serialize triangle count
    offset += length;
    uint32_t triangleCount =
        static_cast<uint32_t>(quantizedMesh.vertexData.indices.size() / 3);
    length = sizeof(triangleCount);
    std::memcpy(quantizedMeshBin.data() + offset, &triangleCount, length);

    // serialize indices
    offset += length;
    length = quantizedMesh.vertexData.indices.size() * sizeof(uint16_t);
    std::memcpy(
        quantizedMeshBin.data() + offset,
        quantizedMesh.vertexData.indices.data(),
        length);

    // serialize west edge
    offset += length;
    uint32_t westCount =
        static_cast<uint32_t>(quantizedMesh.vertexData.westIndices.size());
    length = sizeof(westCount);
    std::memcpy(quantizedMeshBin.data() + offset, &westCount, length);

    offset += length;
    length = quantizedMesh.vertexData.westIndices.size() * sizeof(uint16_t);
    std::memcpy(
        quantizedMeshBin.data() + offset,
        quantizedMesh.vertexData.westIndices.data(),
        length);

    // serialize south edge
    offset += length;
    uint32_t southCount =
        static_cast<uint32_t>(quantizedMesh.vertexData.southIndices.size());
    length = sizeof(westCount);
    std::memcpy(quantizedMeshBin.data() + offset, &southCount, length);

    gsl::span<const std::byte> data(
        quantizedMeshBin.data(),
        quantizedMeshBin.size());
    auto loadResult =
        QuantizedMeshLoader::load(tileID, boundingVolume, "url", data, false);

    REQUIRE(loadResult.model == std::nullopt);
  }

  SECTION("Quantized mesh with ill-formed east edge indices") {
    std::vector<std::byte> quantizedMeshBin(
        sizeof(quantizedMesh.header) +                         // header
        sizeof(uint32_t) +                                     // vertex count
        quantizedMesh.vertexData.u.size() * sizeof(uint16_t) + // u buffer
        quantizedMesh.vertexData.v.size() * sizeof(uint16_t) + // v buffer
        quantizedMesh.vertexData.height.size() *
            sizeof(uint16_t) + // height buffer
        sizeof(uint32_t) +     // triangle count
        quantizedMesh.vertexData.indices.size() * sizeof(uint16_t) +
        sizeof(uint32_t) + // west edge
        quantizedMesh.vertexData.westIndices.size() * sizeof(uint16_t) +
        sizeof(uint32_t) + // south edge
        quantizedMesh.vertexData.southIndices.size() * sizeof(uint16_t) +
        sizeof(uint32_t) // east edge
    );

    // serialize header
    size_t offset = 0;
    size_t length = sizeof(quantizedMesh.header);
    std::memcpy(quantizedMeshBin.data(), &quantizedMesh.header, length);

    // serialize vertex count
    offset += length;
    uint32_t vertexCount =
        static_cast<uint32_t>(quantizedMesh.vertexData.u.size());
    length = sizeof(vertexCount);
    std::memcpy(quantizedMeshBin.data() + offset, &vertexCount, length);

    // serialize u buffer
    offset += length;
    length = quantizedMesh.vertexData.u.size() * sizeof(uint16_t);
    std::memcpy(
        quantizedMeshBin.data() + offset,
        quantizedMesh.vertexData.u.data(),
        length);

    // serialize v buffer
    offset += length;
    length = quantizedMesh.vertexData.v.size() * sizeof(uint16_t);
    std::memcpy(
        quantizedMeshBin.data() + offset,
        quantizedMesh.vertexData.v.data(),
        length);

    // serialize height buffer
    offset += length;
    length = quantizedMesh.vertexData.height.size() * sizeof(uint16_t);
    std::memcpy(
        quantizedMeshBin.data() + offset,
        quantizedMesh.vertexData.height.data(),
        length);

    // serialize triangle count
    offset += length;
    uint32_t triangleCount =
        static_cast<uint32_t>(quantizedMesh.vertexData.indices.size() / 3);
    length = sizeof(triangleCount);
    std::memcpy(quantizedMeshBin.data() + offset, &triangleCount, length);

    // serialize indices
    offset += length;
    length = quantizedMesh.vertexData.indices.size() * sizeof(uint16_t);
    std::memcpy(
        quantizedMeshBin.data() + offset,
        quantizedMesh.vertexData.indices.data(),
        length);

    // serialize west edge
    offset += length;
    uint32_t westCount =
        static_cast<uint32_t>(quantizedMesh.vertexData.westIndices.size());
    length = sizeof(westCount);
    std::memcpy(quantizedMeshBin.data() + offset, &westCount, length);

    offset += length;
    length = quantizedMesh.vertexData.westIndices.size() * sizeof(uint16_t);
    std::memcpy(
        quantizedMeshBin.data() + offset,
        quantizedMesh.vertexData.westIndices.data(),
        length);

    // serialize south edge
    offset += length;
    uint32_t southCount =
        static_cast<uint32_t>(quantizedMesh.vertexData.southIndices.size());
    length = sizeof(westCount);
    std::memcpy(quantizedMeshBin.data() + offset, &southCount, length);

    offset += length;
    length = quantizedMesh.vertexData.southIndices.size() * sizeof(uint16_t);
    std::memcpy(
        quantizedMeshBin.data() + offset,
        quantizedMesh.vertexData.southIndices.data(),
        length);

    // serialize east edge
    offset += length;
    uint32_t eastCount =
        static_cast<uint32_t>(quantizedMesh.vertexData.eastIndices.size());
    length = sizeof(eastCount);
    std::memcpy(quantizedMeshBin.data() + offset, &eastCount, length);

    gsl::span<const std::byte> data(
        quantizedMeshBin.data(),
        quantizedMeshBin.size());
    auto loadResult =
        QuantizedMeshLoader::load(tileID, boundingVolume, "url", data, false);

    REQUIRE(loadResult.model == std::nullopt);
  }

  SECTION("Quantized mesh with ill-formed north edge indices") {
    std::vector<std::byte> quantizedMeshBin(
        sizeof(quantizedMesh.header) +                         // header
        sizeof(uint32_t) +                                     // vertex count
        quantizedMesh.vertexData.u.size() * sizeof(uint16_t) + // u buffer
        quantizedMesh.vertexData.v.size() * sizeof(uint16_t) + // v buffer
        quantizedMesh.vertexData.height.size() *
            sizeof(uint16_t) + // height buffer
        sizeof(uint32_t) +     // triangle count
        quantizedMesh.vertexData.indices.size() * sizeof(uint16_t) +
        sizeof(uint32_t) + // west edge
        quantizedMesh.vertexData.westIndices.size() * sizeof(uint16_t) +
        sizeof(uint32_t) + // south edge
        quantizedMesh.vertexData.southIndices.size() * sizeof(uint16_t) +
        sizeof(uint32_t) + // east edge
        quantizedMesh.vertexData.eastIndices.size() * sizeof(uint16_t) +
        sizeof(uint32_t) // north edge
    );

    // serialize header
    size_t offset = 0;
    size_t length = sizeof(quantizedMesh.header);
    std::memcpy(quantizedMeshBin.data(), &quantizedMesh.header, length);

    // serialize vertex count
    offset += length;
    uint32_t vertexCount =
        static_cast<uint32_t>(quantizedMesh.vertexData.u.size());
    length = sizeof(vertexCount);
    std::memcpy(quantizedMeshBin.data() + offset, &vertexCount, length);

    // serialize u buffer
    offset += length;
    length = quantizedMesh.vertexData.u.size() * sizeof(uint16_t);
    std::memcpy(
        quantizedMeshBin.data() + offset,
        quantizedMesh.vertexData.u.data(),
        length);

    // serialize v buffer
    offset += length;
    length = quantizedMesh.vertexData.v.size() * sizeof(uint16_t);
    std::memcpy(
        quantizedMeshBin.data() + offset,
        quantizedMesh.vertexData.v.data(),
        length);

    // serialize height buffer
    offset += length;
    length = quantizedMesh.vertexData.height.size() * sizeof(uint16_t);
    std::memcpy(
        quantizedMeshBin.data() + offset,
        quantizedMesh.vertexData.height.data(),
        length);

    // serialize triangle count
    offset += length;
    uint32_t triangleCount =
        static_cast<uint32_t>(quantizedMesh.vertexData.indices.size() / 3);
    length = sizeof(triangleCount);
    std::memcpy(quantizedMeshBin.data() + offset, &triangleCount, length);

    // serialize indices
    offset += length;
    length = quantizedMesh.vertexData.indices.size() * sizeof(uint16_t);
    std::memcpy(
        quantizedMeshBin.data() + offset,
        quantizedMesh.vertexData.indices.data(),
        length);

    // serialize west edge
    offset += length;
    uint32_t westCount =
        static_cast<uint32_t>(quantizedMesh.vertexData.westIndices.size());
    length = sizeof(westCount);
    std::memcpy(quantizedMeshBin.data() + offset, &westCount, length);

    offset += length;
    length = quantizedMesh.vertexData.westIndices.size() * sizeof(uint16_t);
    std::memcpy(
        quantizedMeshBin.data() + offset,
        quantizedMesh.vertexData.westIndices.data(),
        length);

    // serialize south edge
    offset += length;
    uint32_t southCount =
        static_cast<uint32_t>(quantizedMesh.vertexData.southIndices.size());
    length = sizeof(westCount);
    std::memcpy(quantizedMeshBin.data() + offset, &southCount, length);

    offset += length;
    length = quantizedMesh.vertexData.southIndices.size() * sizeof(uint16_t);
    std::memcpy(
        quantizedMeshBin.data() + offset,
        quantizedMesh.vertexData.southIndices.data(),
        length);

    // serialize east edge
    offset += length;
    uint32_t eastCount =
        static_cast<uint32_t>(quantizedMesh.vertexData.eastIndices.size());
    length = sizeof(eastCount);
    std::memcpy(quantizedMeshBin.data() + offset, &eastCount, length);

    offset += length;
    length = quantizedMesh.vertexData.eastIndices.size() * sizeof(uint16_t);
    std::memcpy(
        quantizedMeshBin.data() + offset,
        quantizedMesh.vertexData.eastIndices.data(),
        length);

    // serialize north edge
    offset += length;
    uint32_t northCount =
        static_cast<uint32_t>(quantizedMesh.vertexData.northIndices.size());
    length = sizeof(northCount);
    std::memcpy(quantizedMeshBin.data() + offset, &northCount, length);

    gsl::span<const std::byte> data(
        quantizedMeshBin.data(),
        quantizedMeshBin.size());
    auto loadResult =
        QuantizedMeshLoader::load(tileID, boundingVolume, "url", data, false);

    REQUIRE(loadResult.model == std::nullopt);
  }
}
