#include "CesiumGltf/PropertyAttributeView.h"

#include <CesiumUtility/Assert.h>

#include <catch2/catch.hpp>

#include <cstddef>
#include <span>
#include <vector>

using namespace CesiumGltf;

namespace {
template <typename T, bool Normalized = false>
void addAttributeToModel(
    Model& model,
    MeshPrimitive& primitive,
    const std::string& name,
    const std::vector<T>& values) {
  Buffer& buffer = model.buffers.emplace_back();
  buffer.cesium.data.resize(values.size() * sizeof(T));
  buffer.byteLength = static_cast<int64_t>(buffer.cesium.data.size());
  std::memcpy(
      buffer.cesium.data.data(),
      values.data(),
      buffer.cesium.data.size());

  BufferView& bufferView = model.bufferViews.emplace_back();
  bufferView.buffer = static_cast<int32_t>(model.buffers.size() - 1);
  bufferView.byteOffset = 0;
  bufferView.byteLength = buffer.byteLength;

  Accessor& accessor = model.accessors.emplace_back();
  accessor.bufferView = static_cast<int32_t>(model.bufferViews.size() - 1);
  accessor.count = static_cast<int64_t>(values.size());
  accessor.byteOffset = 0;

  PropertyType type = TypeToPropertyType<T>::value;
  switch (type) {
  case PropertyType::Scalar:
    accessor.type = Accessor::Type::SCALAR;
    break;
  case PropertyType::Vec2:
    accessor.type = Accessor::Type::VEC2;
    break;
  case PropertyType::Vec3:
    accessor.type = Accessor::Type::VEC3;
    break;
  case PropertyType::Vec4:
    accessor.type = Accessor::Type::VEC4;
    break;
  case PropertyType::Mat2:
    accessor.type = Accessor::Type::MAT2;
    break;
  case PropertyType::Mat3:
    accessor.type = Accessor::Type::MAT3;
    break;
  case PropertyType::Mat4:
    accessor.type = Accessor::Type::MAT4;
    break;
  default:
    CESIUM_ASSERT(false && "Input type is not supported as an accessor type");
    break;
  }

  PropertyComponentType componentType = TypeToPropertyType<T>::component;
  switch (componentType) {
  case PropertyComponentType::Int8:
    accessor.componentType = Accessor::ComponentType::BYTE;
    break;
  case PropertyComponentType::Uint8:
    accessor.componentType = Accessor::ComponentType::UNSIGNED_BYTE;
    break;
  case PropertyComponentType::Int16:
    accessor.componentType = Accessor::ComponentType::SHORT;
    break;
  case PropertyComponentType::Uint16:
    accessor.componentType = Accessor::ComponentType::UNSIGNED_SHORT;
    break;
  case PropertyComponentType::Float32:
    accessor.componentType = Accessor::ComponentType::FLOAT;
    break;
  default:
    CESIUM_ASSERT(
        false &&
        "Input component type is not supported as an accessor component type");
    break;
  }

  accessor.normalized = Normalized;

  primitive.attributes[name] = static_cast<int32_t>(model.accessors.size() - 1);
}
} // namespace

TEST_CASE("Test PropertyAttributeView on model without EXT_structural_metadata "
          "extension") {
  Model model;

  // Create an erroneously isolated property Attribute.
  PropertyAttribute propertyAttribute;
  propertyAttribute.classProperty = "TestClass";

  PropertyAttributeProperty& propertyAttributeProperty =
      propertyAttribute.properties["TestClassProperty"];
  propertyAttributeProperty.attribute = "_ATTRIBUTE";

  PropertyAttributeView view(model, propertyAttribute);
  REQUIRE(
      view.status() ==
      PropertyAttributeViewStatus::ErrorMissingMetadataExtension);

  const ClassProperty* classProperty =
      view.getClassProperty("TestClassProperty");
  REQUIRE(!classProperty);
}

TEST_CASE("Test PropertyAttributeView on model without metadata schema") {
  Model model;

  ExtensionModelExtStructuralMetadata& metadata =
      model.addExtension<ExtensionModelExtStructuralMetadata>();

  PropertyAttribute& propertyAttribute =
      metadata.propertyAttributes.emplace_back();
  propertyAttribute.classProperty = "TestClass";

  PropertyAttributeProperty& propertyAttributeProperty =
      propertyAttribute.properties["TestClassProperty"];
  propertyAttributeProperty.attribute = "_ATTRIBUTE";

  PropertyAttributeView view(model, propertyAttribute);
  REQUIRE(view.status() == PropertyAttributeViewStatus::ErrorMissingSchema);

  const ClassProperty* classProperty =
      view.getClassProperty("TestClassProperty");
  REQUIRE(!classProperty);
}

TEST_CASE("Test property attribute with nonexistent class") {
  Model model;

  ExtensionModelExtStructuralMetadata& metadata =
      model.addExtension<ExtensionModelExtStructuralMetadata>();

  Schema& schema = metadata.schema.emplace();
  Class& testClass = schema.classes["TestClass"];
  ClassProperty& testClassProperty = testClass.properties["TestClassProperty"];
  testClassProperty.type = ClassProperty::Type::SCALAR;
  testClassProperty.componentType = ClassProperty::ComponentType::UINT8;

  PropertyAttribute& propertyAttribute =
      metadata.propertyAttributes.emplace_back();
  propertyAttribute.classProperty = "I Don't Exist";

  PropertyAttributeProperty& propertyAttributeProperty =
      propertyAttribute.properties["TestClassProperty"];
  propertyAttributeProperty.attribute = "_ATTRIBUTE";

  PropertyAttributeView view(model, propertyAttribute);
  REQUIRE(view.status() == PropertyAttributeViewStatus::ErrorClassNotFound);

  const ClassProperty* classProperty =
      view.getClassProperty("TestClassProperty");
  REQUIRE(!classProperty);
}

TEST_CASE("Test scalar PropertyAttributeProperty") {
  Model model;
  Mesh& mesh = model.meshes.emplace_back();
  MeshPrimitive& primitive = mesh.primitives.emplace_back();

  const std::string attributeName = "_ATTRIBUTE";
  std::vector<uint16_t> data = {12, 34, 30, 11};

  addAttributeToModel(model, primitive, attributeName, data);
  size_t accessorIndex = model.accessors.size() - 1;
  size_t bufferIndex = model.buffers.size() - 1;
  size_t bufferViewIndex = model.bufferViews.size() - 1;

  ExtensionModelExtStructuralMetadata& metadata =
      model.addExtension<ExtensionModelExtStructuralMetadata>();

  Schema& schema = metadata.schema.emplace();
  Class& testClass = schema.classes["TestClass"];
  ClassProperty& testClassProperty = testClass.properties["TestClassProperty"];
  testClassProperty.type = ClassProperty::Type::SCALAR;
  testClassProperty.componentType = ClassProperty::ComponentType::UINT16;

  PropertyAttribute& propertyAttribute =
      metadata.propertyAttributes.emplace_back();
  propertyAttribute.classProperty = "TestClass";

  PropertyAttributeProperty& propertyAttributeProperty =
      propertyAttribute.properties["TestClassProperty"];
  propertyAttributeProperty.attribute = attributeName;

  PropertyAttributeView view(model, propertyAttribute);
  REQUIRE(view.status() == PropertyAttributeViewStatus::Valid);

  const ClassProperty* classProperty =
      view.getClassProperty("TestClassProperty");
  REQUIRE(classProperty);
  REQUIRE(classProperty->type == ClassProperty::Type::SCALAR);
  REQUIRE(classProperty->componentType == ClassProperty::ComponentType::UINT16);
  REQUIRE(classProperty->count == std::nullopt);
  REQUIRE(!classProperty->array);
  REQUIRE(!classProperty->normalized);

  SECTION("Access correct type") {
    PropertyAttributePropertyView<uint16_t> uint16Property =
        view.getPropertyView<uint16_t>(primitive, "TestClassProperty");
    REQUIRE(
        uint16Property.status() == PropertyAttributePropertyViewStatus::Valid);
    for (size_t i = 0; i < data.size(); ++i) {
      REQUIRE(uint16Property.getRaw(static_cast<int64_t>(i)) == data[i]);
      REQUIRE(uint16Property.get(static_cast<int64_t>(i)) == data[i]);
    }
  }

  SECTION("Access wrong type") {
    PropertyAttributePropertyView<glm::u16vec2> u16vec2Invalid =
        view.getPropertyView<glm::u16vec2>(primitive, "TestClassProperty");
    REQUIRE(
        u16vec2Invalid.status() ==
        PropertyAttributePropertyViewStatus::ErrorTypeMismatch);
  }

  SECTION("Access wrong component type") {
    PropertyAttributePropertyView<uint8_t> uint8Invalid =
        view.getPropertyView<uint8_t>(primitive, "TestClassProperty");
    REQUIRE(
        uint8Invalid.status() ==
        PropertyAttributePropertyViewStatus::ErrorComponentTypeMismatch);

    PropertyAttributePropertyView<int32_t> int32Invalid =
        view.getPropertyView<int32_t>(primitive, "TestClassProperty");
    REQUIRE(
        int32Invalid.status() ==
        PropertyAttributePropertyViewStatus::ErrorComponentTypeMismatch);

    PropertyAttributePropertyView<float> uint64Invalid =
        view.getPropertyView<float>(primitive, "TestClassProperty");
    REQUIRE(
        uint64Invalid.status() ==
        PropertyAttributePropertyViewStatus::ErrorComponentTypeMismatch);
  }

  SECTION("Access incorrectly as normalized") {
    PropertyAttributePropertyView<uint16_t, true> normalizedInvalid =
        view.getPropertyView<uint16_t, true>(primitive, "TestClassProperty");
    REQUIRE(
        normalizedInvalid.status() ==
        PropertyAttributePropertyViewStatus::ErrorNormalizationMismatch);
  }

  SECTION("Buffer view points outside of the real buffer length") {
    model.buffers[bufferIndex].cesium.data.resize(4);
    PropertyAttributePropertyView<uint16_t> property =
        view.getPropertyView<uint16_t>(primitive, "TestClassProperty");
    REQUIRE(
        property.status() ==
        PropertyAttributePropertyViewStatus::ErrorBufferViewOutOfBounds);
  }

  SECTION("Wrong buffer index") {
    model.bufferViews[bufferViewIndex].buffer = 2;
    PropertyAttributePropertyView<uint16_t> property =
        view.getPropertyView<uint16_t>(primitive, "TestClassProperty");
    REQUIRE(
        property.status() ==
        PropertyAttributePropertyViewStatus::ErrorInvalidBuffer);
  }

  SECTION("Accessor view points outside of buffer viwe length") {
    model.accessors[accessorIndex].count = 10;
    PropertyAttributePropertyView<uint16_t> property =
        view.getPropertyView<uint16_t>(primitive, "TestClassProperty");
    REQUIRE(
        property.status() ==
        PropertyAttributePropertyViewStatus::ErrorAccessorOutOfBounds);
  }

  SECTION("Wrong buffer view index") {
    model.accessors[accessorIndex].bufferView = -1;
    PropertyAttributePropertyView<uint16_t> property =
        view.getPropertyView<uint16_t>(primitive, "TestClassProperty");
    REQUIRE(
        property.status() ==
        PropertyAttributePropertyViewStatus::ErrorInvalidBufferView);
  }

  SECTION("Wrong accessor normalization") {
    model.accessors[accessorIndex].normalized = true;
    PropertyAttributePropertyView<uint16_t> property =
        view.getPropertyView<uint16_t>(primitive, "TestClassProperty");
    REQUIRE(
        property.status() == PropertyAttributePropertyViewStatus::
                                 ErrorAccessorNormalizationMismatch);
  }

  SECTION("Wrong accessor component type") {
    model.accessors[accessorIndex].componentType =
        Accessor::ComponentType::SHORT;
    PropertyAttributePropertyView<uint16_t> property =
        view.getPropertyView<uint16_t>(primitive, "TestClassProperty");
    REQUIRE(
        property.status() == PropertyAttributePropertyViewStatus::
                                 ErrorAccessorComponentTypeMismatch);
  }

  SECTION("Wrong accessor type") {
    model.accessors[accessorIndex].type = Accessor::Type::VEC2;
    PropertyAttributePropertyView<uint16_t> property =
        view.getPropertyView<uint16_t>(primitive, "TestClassProperty");
    REQUIRE(
        property.status() ==
        PropertyAttributePropertyViewStatus::ErrorAccessorTypeMismatch);
  }

  SECTION("Wrong accessor index") {
    primitive.attributes[attributeName] = -1;
    PropertyAttributePropertyView<uint16_t> property =
        view.getPropertyView<uint16_t>(primitive, "TestClassProperty");
    REQUIRE(
        property.status() ==
        PropertyAttributePropertyViewStatus::ErrorInvalidAccessor);
  }

  SECTION("Missing attribute") {
    primitive.attributes.clear();
    PropertyAttributePropertyView<uint16_t> property =
        view.getPropertyView<uint16_t>(primitive, "TestClassProperty");
    REQUIRE(
        property.status() ==
        PropertyAttributePropertyViewStatus::ErrorMissingAttribute);
  }
}

TEST_CASE("Test scalar PropertyAttributeProperty (normalized)") {
  Model model;
  Mesh& mesh = model.meshes.emplace_back();
  MeshPrimitive& primitive = mesh.primitives.emplace_back();

  const std::string attributeName = "_ATTRIBUTE";
  std::vector<uint8_t> data = {12, 34, 30, 11};

  addAttributeToModel<uint8_t, true>(model, primitive, attributeName, data);
  size_t accessorIndex = model.accessors.size() - 1;

  ExtensionModelExtStructuralMetadata& metadata =
      model.addExtension<ExtensionModelExtStructuralMetadata>();

  Schema& schema = metadata.schema.emplace();
  Class& testClass = schema.classes["TestClass"];
  ClassProperty& testClassProperty = testClass.properties["TestClassProperty"];
  testClassProperty.type = ClassProperty::Type::SCALAR;
  testClassProperty.componentType = ClassProperty::ComponentType::UINT8;
  testClassProperty.normalized = true;

  PropertyAttribute& propertyAttribute =
      metadata.propertyAttributes.emplace_back();
  propertyAttribute.classProperty = "TestClass";

  PropertyAttributeProperty& propertyAttributeProperty =
      propertyAttribute.properties["TestClassProperty"];
  propertyAttributeProperty.attribute = attributeName;

  PropertyAttributeView view(model, propertyAttribute);
  REQUIRE(view.status() == PropertyAttributeViewStatus::Valid);

  const ClassProperty* classProperty =
      view.getClassProperty("TestClassProperty");
  REQUIRE(classProperty);
  REQUIRE(classProperty->type == ClassProperty::Type::SCALAR);
  REQUIRE(classProperty->componentType == ClassProperty::ComponentType::UINT8);
  REQUIRE(classProperty->count == std::nullopt);
  REQUIRE(!classProperty->array);
  REQUIRE(classProperty->normalized);

  SECTION("Access correct type") {
    PropertyAttributePropertyView<uint8_t, true> uint8Property =
        view.getPropertyView<uint8_t, true>(primitive, "TestClassProperty");
    REQUIRE(
        uint8Property.status() == PropertyAttributePropertyViewStatus::Valid);

    for (size_t i = 0; i < data.size(); ++i) {
      REQUIRE(uint8Property.getRaw(static_cast<int64_t>(i)) == data[i]);
      REQUIRE(uint8Property.get(static_cast<int64_t>(i)) == normalize(data[i]));
    }
  }

  SECTION("Access wrong type") {
    PropertyAttributePropertyView<glm::u8vec2, true> u8vec2Invalid =
        view.getPropertyView<glm::u8vec2, true>(primitive, "TestClassProperty");
    REQUIRE(
        u8vec2Invalid.status() ==
        PropertyAttributePropertyViewStatus::ErrorTypeMismatch);
  }

  SECTION("Access wrong component type") {
    PropertyAttributePropertyView<uint16_t, true> uint16Invalid =
        view.getPropertyView<uint16_t, true>(primitive, "TestClassProperty");
    REQUIRE(
        uint16Invalid.status() ==
        PropertyAttributePropertyViewStatus::ErrorComponentTypeMismatch);

    PropertyAttributePropertyView<int32_t> int32Invalid =
        view.getPropertyView<int32_t>(primitive, "TestClassProperty");
    REQUIRE(
        int32Invalid.status() ==
        PropertyAttributePropertyViewStatus::ErrorComponentTypeMismatch);
  }

  SECTION("Access incorrectly as non-normalized") {
    PropertyAttributePropertyView<uint8_t> normalizedInvalid =
        view.getPropertyView<uint8_t>(primitive, "TestClassProperty");
    REQUIRE(
        normalizedInvalid.status() ==
        PropertyAttributePropertyViewStatus::ErrorNormalizationMismatch);
  }

  SECTION("Access incorrectly as double") {
    PropertyAttributePropertyView<double> doubleInvalid =
        view.getPropertyView<double>(primitive, "TestClassProperty");
    REQUIRE(
        doubleInvalid.status() ==
        PropertyAttributePropertyViewStatus::ErrorComponentTypeMismatch);
  }

  SECTION("Wrong accessor normalization") {
    model.accessors[accessorIndex].normalized = false;
    PropertyAttributePropertyView<uint8_t, true> property =
        view.getPropertyView<uint8_t, true>(primitive, "TestClassProperty");
    REQUIRE(
        property.status() == PropertyAttributePropertyViewStatus::
                                 ErrorAccessorNormalizationMismatch);
  }
}

TEST_CASE("Test vecN PropertyAttributeProperty") {
  Model model;
  Mesh& mesh = model.meshes.emplace_back();
  MeshPrimitive& primitive = mesh.primitives.emplace_back();

  const std::string attributeName = "_ATTRIBUTE";
  std::vector<glm::u8vec2> data = {
      glm::u8vec2(12, 34),
      glm::u8vec2(10, 3),
      glm::u8vec2(40, 0),
      glm::u8vec2(30, 11)};

  addAttributeToModel(model, primitive, attributeName, data);
  size_t accessorIndex = model.accessors.size() - 1;
  size_t bufferIndex = model.buffers.size() - 1;
  size_t bufferViewIndex = model.bufferViews.size() - 1;

  ExtensionModelExtStructuralMetadata& metadata =
      model.addExtension<ExtensionModelExtStructuralMetadata>();

  Schema& schema = metadata.schema.emplace();
  Class& testClass = schema.classes["TestClass"];
  ClassProperty& testClassProperty = testClass.properties["TestClassProperty"];
  testClassProperty.type = ClassProperty::Type::VEC2;
  testClassProperty.componentType = ClassProperty::ComponentType::UINT8;

  PropertyAttribute& propertyAttribute =
      metadata.propertyAttributes.emplace_back();
  propertyAttribute.classProperty = "TestClass";

  PropertyAttributeProperty& propertyAttributeProperty =
      propertyAttribute.properties["TestClassProperty"];
  propertyAttributeProperty.attribute = attributeName;

  PropertyAttributeView view(model, propertyAttribute);
  REQUIRE(view.status() == PropertyAttributeViewStatus::Valid);

  const ClassProperty* classProperty =
      view.getClassProperty("TestClassProperty");
  REQUIRE(classProperty);
  REQUIRE(classProperty->type == ClassProperty::Type::VEC2);
  REQUIRE(classProperty->componentType == ClassProperty::ComponentType::UINT8);
  REQUIRE(classProperty->count == std::nullopt);
  REQUIRE(!classProperty->array);
  REQUIRE(!classProperty->normalized);

  SECTION("Access correct type") {
    PropertyAttributePropertyView<glm::u8vec2> u8vec2Property =
        view.getPropertyView<glm::u8vec2>(primitive, "TestClassProperty");
    REQUIRE(
        u8vec2Property.status() == PropertyAttributePropertyViewStatus::Valid);
    for (size_t i = 0; i < data.size(); ++i) {
      REQUIRE(u8vec2Property.getRaw(static_cast<int64_t>(i)) == data[i]);
      REQUIRE(u8vec2Property.get(static_cast<int64_t>(i)) == data[i]);
    }
  }

  SECTION("Access wrong type") {
    PropertyAttributePropertyView<uint8_t> uint8Invalid =
        view.getPropertyView<uint8_t>(primitive, "TestClassProperty");
    REQUIRE(
        uint8Invalid.status() ==
        PropertyAttributePropertyViewStatus::ErrorTypeMismatch);

    PropertyAttributePropertyView<glm::u8vec3> u8vec3Invalid =
        view.getPropertyView<glm::u8vec3>(primitive, "TestClassProperty");
    REQUIRE(
        u8vec3Invalid.status() ==
        PropertyAttributePropertyViewStatus::ErrorTypeMismatch);
  }

  SECTION("Access wrong component type") {
    PropertyAttributePropertyView<glm::vec2> vec2Invalid =
        view.getPropertyView<glm::vec2>(primitive, "TestClassProperty");
    REQUIRE(
        vec2Invalid.status() ==
        PropertyAttributePropertyViewStatus::ErrorComponentTypeMismatch);
  }

  SECTION("Access incorrectly as normalized") {
    PropertyAttributePropertyView<glm::u8vec2, true> normalizedInvalid =
        view.getPropertyView<glm::u8vec2, true>(primitive, "TestClassProperty");
    REQUIRE(
        normalizedInvalid.status() ==
        PropertyAttributePropertyViewStatus::ErrorNormalizationMismatch);
  }

  SECTION("Buffer view points outside of the real buffer length") {
    model.buffers[bufferIndex].cesium.data.resize(4);
    PropertyAttributePropertyView<glm::u8vec2> property =
        view.getPropertyView<glm::u8vec2>(primitive, "TestClassProperty");
    REQUIRE(
        property.status() ==
        PropertyAttributePropertyViewStatus::ErrorBufferViewOutOfBounds);
  }

  SECTION("Wrong buffer index") {
    model.bufferViews[bufferViewIndex].buffer = 2;
    PropertyAttributePropertyView<glm::u8vec2> property =
        view.getPropertyView<glm::u8vec2>(primitive, "TestClassProperty");
    REQUIRE(
        property.status() ==
        PropertyAttributePropertyViewStatus::ErrorInvalidBuffer);
  }

  SECTION("Accessor view points outside of buffer viwe length") {
    model.accessors[accessorIndex].count = 10;
    PropertyAttributePropertyView<glm::u8vec2> property =
        view.getPropertyView<glm::u8vec2>(primitive, "TestClassProperty");
    REQUIRE(
        property.status() ==
        PropertyAttributePropertyViewStatus::ErrorAccessorOutOfBounds);
  }

  SECTION("Wrong buffer view index") {
    model.accessors[accessorIndex].bufferView = -1;
    PropertyAttributePropertyView<glm::u8vec2> property =
        view.getPropertyView<glm::u8vec2>(primitive, "TestClassProperty");
    REQUIRE(
        property.status() ==
        PropertyAttributePropertyViewStatus::ErrorInvalidBufferView);
  }

  SECTION("Wrong accessor normalization") {
    model.accessors[accessorIndex].normalized = true;
    PropertyAttributePropertyView<glm::u8vec2> property =
        view.getPropertyView<glm::u8vec2>(primitive, "TestClassProperty");
    REQUIRE(
        property.status() == PropertyAttributePropertyViewStatus::
                                 ErrorAccessorNormalizationMismatch);
  }

  SECTION("Wrong accessor component type") {
    model.accessors[accessorIndex].componentType =
        Accessor::ComponentType::BYTE;
    PropertyAttributePropertyView<glm::u8vec2> property =
        view.getPropertyView<glm::u8vec2>(primitive, "TestClassProperty");
    REQUIRE(
        property.status() == PropertyAttributePropertyViewStatus::
                                 ErrorAccessorComponentTypeMismatch);
  }

  SECTION("Wrong accessor type") {
    model.accessors[accessorIndex].type = Accessor::Type::SCALAR;
    PropertyAttributePropertyView<glm::u8vec2> property =
        view.getPropertyView<glm::u8vec2>(primitive, "TestClassProperty");
    REQUIRE(
        property.status() ==
        PropertyAttributePropertyViewStatus::ErrorAccessorTypeMismatch);
  }

  SECTION("Wrong accessor index") {
    primitive.attributes[attributeName] = -1;
    PropertyAttributePropertyView<glm::u8vec2> property =
        view.getPropertyView<glm::u8vec2>(primitive, "TestClassProperty");
    REQUIRE(
        property.status() ==
        PropertyAttributePropertyViewStatus::ErrorInvalidAccessor);
  }

  SECTION("Missing attribute") {
    primitive.attributes.clear();
    PropertyAttributePropertyView<glm::u8vec2> property =
        view.getPropertyView<glm::u8vec2>(primitive, "TestClassProperty");
    REQUIRE(
        property.status() ==
        PropertyAttributePropertyViewStatus::ErrorMissingAttribute);
  }
}

TEST_CASE("Test vecN PropertyAttributeProperty (normalized)") {
  Model model;
  Mesh& mesh = model.meshes.emplace_back();
  MeshPrimitive& primitive = mesh.primitives.emplace_back();

  const std::string attributeName = "_ATTRIBUTE";
  std::vector<glm::u8vec2> data = {
      glm::u8vec2(12, 34),
      glm::u8vec2(10, 3),
      glm::u8vec2(40, 0),
      glm::u8vec2(30, 11)};

  addAttributeToModel<glm::u8vec2, true>(model, primitive, attributeName, data);
  size_t accessorIndex = model.accessors.size() - 1;

  ExtensionModelExtStructuralMetadata& metadata =
      model.addExtension<ExtensionModelExtStructuralMetadata>();

  Schema& schema = metadata.schema.emplace();
  Class& testClass = schema.classes["TestClass"];
  ClassProperty& testClassProperty = testClass.properties["TestClassProperty"];
  testClassProperty.type = ClassProperty::Type::VEC2;
  testClassProperty.componentType = ClassProperty::ComponentType::UINT8;
  testClassProperty.normalized = true;

  PropertyAttribute& propertyAttribute =
      metadata.propertyAttributes.emplace_back();
  propertyAttribute.classProperty = "TestClass";

  PropertyAttributeProperty& propertyAttributeProperty =
      propertyAttribute.properties["TestClassProperty"];
  propertyAttributeProperty.attribute = attributeName;

  PropertyAttributeView view(model, propertyAttribute);
  REQUIRE(view.status() == PropertyAttributeViewStatus::Valid);

  const ClassProperty* classProperty =
      view.getClassProperty("TestClassProperty");
  REQUIRE(classProperty);
  REQUIRE(classProperty->type == ClassProperty::Type::VEC2);
  REQUIRE(classProperty->componentType == ClassProperty::ComponentType::UINT8);
  REQUIRE(classProperty->count == std::nullopt);
  REQUIRE(!classProperty->array);
  REQUIRE(classProperty->normalized);

  SECTION("Access correct type") {
    PropertyAttributePropertyView<glm::u8vec2, true> u8vec2Property =
        view.getPropertyView<glm::u8vec2, true>(primitive, "TestClassProperty");
    REQUIRE(
        u8vec2Property.status() == PropertyAttributePropertyViewStatus::Valid);
    for (size_t i = 0; i < data.size(); ++i) {
      REQUIRE(u8vec2Property.getRaw(static_cast<int64_t>(i)) == data[i]);
      REQUIRE(
          u8vec2Property.get(static_cast<int64_t>(i)) == normalize(data[i]));
    }
  }

  SECTION("Access wrong type") {
    PropertyAttributePropertyView<uint8_t, true> uint8Invalid =
        view.getPropertyView<uint8_t, true>(primitive, "TestClassProperty");
    REQUIRE(
        uint8Invalid.status() ==
        PropertyAttributePropertyViewStatus::ErrorTypeMismatch);

    PropertyAttributePropertyView<glm::u8vec3, true> u8vec3Invalid =
        view.getPropertyView<glm::u8vec3, true>(primitive, "TestClassProperty");
    REQUIRE(
        u8vec3Invalid.status() ==
        PropertyAttributePropertyViewStatus::ErrorTypeMismatch);
  }

  SECTION("Access wrong component type") {
    PropertyAttributePropertyView<glm::u16vec2, true> u16vec2Invalid =
        view.getPropertyView<glm::u16vec2, true>(
            primitive,
            "TestClassProperty");
    REQUIRE(
        u16vec2Invalid.status() ==
        PropertyAttributePropertyViewStatus::ErrorComponentTypeMismatch);

    PropertyAttributePropertyView<glm::i8vec2, true> i8vec2Invalid =
        view.getPropertyView<glm::i8vec2, true>(primitive, "TestClassProperty");
    REQUIRE(
        i8vec2Invalid.status() ==
        PropertyAttributePropertyViewStatus::ErrorComponentTypeMismatch);
  }

  SECTION("Access incorrectly as non-normalized") {
    PropertyAttributePropertyView<glm::u8vec2> normalizedInvalid =
        view.getPropertyView<glm::u8vec2>(primitive, "TestClassProperty");
    REQUIRE(
        normalizedInvalid.status() ==
        PropertyAttributePropertyViewStatus::ErrorNormalizationMismatch);
  }

  SECTION("Access incorrectly as dvec2") {
    PropertyAttributePropertyView<glm::dvec2> dvec2Invalid =
        view.getPropertyView<glm::dvec2>(primitive, "TestClassProperty");
    REQUIRE(
        dvec2Invalid.status() ==
        PropertyAttributePropertyViewStatus::ErrorComponentTypeMismatch);
  }

  SECTION("Wrong accessor normalization") {
    model.accessors[accessorIndex].normalized = false;
    PropertyAttributePropertyView<glm::u8vec2, true> property =
        view.getPropertyView<glm::u8vec2, true>(primitive, "TestClassProperty");
    REQUIRE(
        property.status() == PropertyAttributePropertyViewStatus::
                                 ErrorAccessorNormalizationMismatch);
  }
}

TEST_CASE("Test matN PropertyAttributeProperty") {
  Model model;
  Mesh& mesh = model.meshes.emplace_back();
  MeshPrimitive& primitive = mesh.primitives.emplace_back();

  const std::string attributeName = "_ATTRIBUTE";
  // clang-format off
  std::vector<glm::u16mat2x2> data = {
      glm::u16mat2x2(
        12, 34,
        30, 1),
      glm::u16mat2x2(
        11, 8,
        73, 102),
      glm::u16mat2x2(
        1, 0,
        63, 2),
      glm::u16mat2x2(
        4, 8,
        3, 23)};
  // clang-format on

  addAttributeToModel(model, primitive, attributeName, data);
  size_t accessorIndex = model.accessors.size() - 1;
  size_t bufferIndex = model.buffers.size() - 1;
  size_t bufferViewIndex = model.bufferViews.size() - 1;

  ExtensionModelExtStructuralMetadata& metadata =
      model.addExtension<ExtensionModelExtStructuralMetadata>();

  Schema& schema = metadata.schema.emplace();
  Class& testClass = schema.classes["TestClass"];
  ClassProperty& testClassProperty = testClass.properties["TestClassProperty"];
  testClassProperty.type = ClassProperty::Type::MAT2;
  testClassProperty.componentType = ClassProperty::ComponentType::UINT16;

  PropertyAttribute& propertyAttribute =
      metadata.propertyAttributes.emplace_back();
  propertyAttribute.classProperty = "TestClass";

  PropertyAttributeProperty& propertyAttributeProperty =
      propertyAttribute.properties["TestClassProperty"];
  propertyAttributeProperty.attribute = attributeName;

  PropertyAttributeView view(model, propertyAttribute);
  REQUIRE(view.status() == PropertyAttributeViewStatus::Valid);

  const ClassProperty* classProperty =
      view.getClassProperty("TestClassProperty");
  REQUIRE(classProperty);
  REQUIRE(classProperty->type == ClassProperty::Type::MAT2);
  REQUIRE(classProperty->componentType == ClassProperty::ComponentType::UINT16);
  REQUIRE(classProperty->count == std::nullopt);
  REQUIRE(!classProperty->array);
  REQUIRE(!classProperty->normalized);

  SECTION("Access correct type") {
    PropertyAttributePropertyView<glm::u16mat2x2> u16mat2x2Property =
        view.getPropertyView<glm::u16mat2x2>(primitive, "TestClassProperty");
    REQUIRE(
        u16mat2x2Property.status() ==
        PropertyAttributePropertyViewStatus::Valid);
    for (size_t i = 0; i < data.size(); ++i) {
      REQUIRE(u16mat2x2Property.getRaw(static_cast<int64_t>(i)) == data[i]);
      REQUIRE(u16mat2x2Property.get(static_cast<int64_t>(i)) == data[i]);
    }
  }

  SECTION("Access wrong type") {
    PropertyAttributePropertyView<uint16_t> uint16Invalid =
        view.getPropertyView<uint16_t>(primitive, "TestClassProperty");
    REQUIRE(
        uint16Invalid.status() ==
        PropertyAttributePropertyViewStatus::ErrorTypeMismatch);

    PropertyAttributePropertyView<glm::u16vec2> u16vec2Invalid =
        view.getPropertyView<glm::u16vec2>(primitive, "TestClassProperty");
    REQUIRE(
        u16vec2Invalid.status() ==
        PropertyAttributePropertyViewStatus::ErrorTypeMismatch);

    PropertyAttributePropertyView<glm::u16mat4x4> u16mat4x4Invalid =
        view.getPropertyView<glm::u16mat4x4>(primitive, "TestClassProperty");
    REQUIRE(
        u16mat4x4Invalid.status() ==
        PropertyAttributePropertyViewStatus::ErrorTypeMismatch);
  }

  SECTION("Access wrong component type") {
    PropertyAttributePropertyView<glm::mat2> mat2Invalid =
        view.getPropertyView<glm::mat2>(primitive, "TestClassProperty");
    REQUIRE(
        mat2Invalid.status() ==
        PropertyAttributePropertyViewStatus::ErrorComponentTypeMismatch);
  }

  SECTION("Access incorrectly as normalized") {
    PropertyAttributePropertyView<glm::u16mat2x2, true> normalizedInvalid =
        view.getPropertyView<glm::u16mat2x2, true>(
            primitive,
            "TestClassProperty");
    REQUIRE(
        normalizedInvalid.status() ==
        PropertyAttributePropertyViewStatus::ErrorNormalizationMismatch);
  }

  SECTION("Buffer view points outside of the real buffer length") {
    model.buffers[bufferIndex].cesium.data.resize(4);
    PropertyAttributePropertyView<glm::u16mat2x2> property =
        view.getPropertyView<glm::u16mat2x2>(primitive, "TestClassProperty");
    REQUIRE(
        property.status() ==
        PropertyAttributePropertyViewStatus::ErrorBufferViewOutOfBounds);
  }

  SECTION("Wrong buffer index") {
    model.bufferViews[bufferViewIndex].buffer = 2;
    PropertyAttributePropertyView<glm::u16mat2x2> property =
        view.getPropertyView<glm::u16mat2x2>(primitive, "TestClassProperty");
    REQUIRE(
        property.status() ==
        PropertyAttributePropertyViewStatus::ErrorInvalidBuffer);
  }

  SECTION("Accessor view points outside of buffer viwe length") {
    model.accessors[accessorIndex].count = 10;
    PropertyAttributePropertyView<glm::u16mat2x2> property =
        view.getPropertyView<glm::u16mat2x2>(primitive, "TestClassProperty");
    REQUIRE(
        property.status() ==
        PropertyAttributePropertyViewStatus::ErrorAccessorOutOfBounds);
  }

  SECTION("Wrong buffer view index") {
    model.accessors[accessorIndex].bufferView = -1;
    PropertyAttributePropertyView<glm::u16mat2x2> property =
        view.getPropertyView<glm::u16mat2x2>(primitive, "TestClassProperty");
    REQUIRE(
        property.status() ==
        PropertyAttributePropertyViewStatus::ErrorInvalidBufferView);
  }

  SECTION("Wrong accessor normalization") {
    model.accessors[accessorIndex].normalized = true;
    PropertyAttributePropertyView<glm::u16mat2x2> property =
        view.getPropertyView<glm::u16mat2x2>(primitive, "TestClassProperty");
    REQUIRE(
        property.status() == PropertyAttributePropertyViewStatus::
                                 ErrorAccessorNormalizationMismatch);
  }

  SECTION("Wrong accessor component type") {
    model.accessors[accessorIndex].componentType =
        Accessor::ComponentType::BYTE;
    PropertyAttributePropertyView<glm::u16mat2x2> property =
        view.getPropertyView<glm::u16mat2x2>(primitive, "TestClassProperty");
    REQUIRE(
        property.status() == PropertyAttributePropertyViewStatus::
                                 ErrorAccessorComponentTypeMismatch);
  }

  SECTION("Wrong accessor type") {
    model.accessors[accessorIndex].type = Accessor::Type::SCALAR;
    PropertyAttributePropertyView<glm::u16mat2x2> property =
        view.getPropertyView<glm::u16mat2x2>(primitive, "TestClassProperty");
    REQUIRE(
        property.status() ==
        PropertyAttributePropertyViewStatus::ErrorAccessorTypeMismatch);
  }

  SECTION("Wrong accessor index") {
    primitive.attributes[attributeName] = -1;
    PropertyAttributePropertyView<glm::u16mat2x2> property =
        view.getPropertyView<glm::u16mat2x2>(primitive, "TestClassProperty");
    REQUIRE(
        property.status() ==
        PropertyAttributePropertyViewStatus::ErrorInvalidAccessor);
  }

  SECTION("Missing attribute") {
    primitive.attributes.clear();
    PropertyAttributePropertyView<glm::u16mat2x2> property =
        view.getPropertyView<glm::u16mat2x2>(primitive, "TestClassProperty");
    REQUIRE(
        property.status() ==
        PropertyAttributePropertyViewStatus::ErrorMissingAttribute);
  }
}

TEST_CASE("Test matN PropertyAttributeProperty (normalized)") {
  Model model;
  Mesh& mesh = model.meshes.emplace_back();
  MeshPrimitive& primitive = mesh.primitives.emplace_back();

  const std::string attributeName = "_ATTRIBUTE";
  // clang-format off
  std::vector<glm::u16mat2x2> data = {
      glm::u16mat2x2(
        12, 34,
        30, 1),
      glm::u16mat2x2(
        11, 8,
        73, 102),
      glm::u16mat2x2(
        1, 0,
        63, 2),
      glm::u16mat2x2(
        4, 8,
        3, 23)};
  // clang-format on

  addAttributeToModel<glm::u16mat2x2, true>(
      model,
      primitive,
      attributeName,
      data);
  size_t accessorIndex = model.accessors.size() - 1;

  ExtensionModelExtStructuralMetadata& metadata =
      model.addExtension<ExtensionModelExtStructuralMetadata>();

  Schema& schema = metadata.schema.emplace();
  Class& testClass = schema.classes["TestClass"];
  ClassProperty& testClassProperty = testClass.properties["TestClassProperty"];
  testClassProperty.type = ClassProperty::Type::MAT2;
  testClassProperty.componentType = ClassProperty::ComponentType::UINT16;
  testClassProperty.normalized = true;

  PropertyAttribute& propertyAttribute =
      metadata.propertyAttributes.emplace_back();
  propertyAttribute.classProperty = "TestClass";

  PropertyAttributeProperty& propertyAttributeProperty =
      propertyAttribute.properties["TestClassProperty"];
  propertyAttributeProperty.attribute = attributeName;

  PropertyAttributeView view(model, propertyAttribute);
  REQUIRE(view.status() == PropertyAttributeViewStatus::Valid);

  const ClassProperty* classProperty =
      view.getClassProperty("TestClassProperty");
  REQUIRE(classProperty);
  REQUIRE(classProperty->type == ClassProperty::Type::MAT2);
  REQUIRE(classProperty->componentType == ClassProperty::ComponentType::UINT16);
  REQUIRE(classProperty->count == std::nullopt);
  REQUIRE(!classProperty->array);
  REQUIRE(classProperty->normalized);

  SECTION("Access correct type") {
    PropertyAttributePropertyView<glm::u16mat2x2, true> u16mat2x2Property =
        view.getPropertyView<glm::u16mat2x2, true>(
            primitive,
            "TestClassProperty");
    REQUIRE(
        u16mat2x2Property.status() ==
        PropertyAttributePropertyViewStatus::Valid);
    for (size_t i = 0; i < data.size(); ++i) {
      REQUIRE(u16mat2x2Property.getRaw(static_cast<int64_t>(i)) == data[i]);
      REQUIRE(
          u16mat2x2Property.get(static_cast<int64_t>(i)) == normalize(data[i]));
    }
  }

  SECTION("Access wrong type") {
    PropertyAttributePropertyView<uint16_t, true> uint16Invalid =
        view.getPropertyView<uint16_t, true>(primitive, "TestClassProperty");
    REQUIRE(
        uint16Invalid.status() ==
        PropertyAttributePropertyViewStatus::ErrorTypeMismatch);

    PropertyAttributePropertyView<glm::u16vec2, true> u16vec2Invalid =
        view.getPropertyView<glm::u16vec2, true>(
            primitive,
            "TestClassProperty");
    REQUIRE(
        u16vec2Invalid.status() ==
        PropertyAttributePropertyViewStatus::ErrorTypeMismatch);

    PropertyAttributePropertyView<glm::u16mat4x4, true> u16mat4x4Invalid =
        view.getPropertyView<glm::u16mat4x4, true>(
            primitive,
            "TestClassProperty");
    REQUIRE(
        u16mat4x4Invalid.status() ==
        PropertyAttributePropertyViewStatus::ErrorTypeMismatch);
  }

  SECTION("Access wrong component type") {
    PropertyAttributePropertyView<glm::imat2x2, true> imat2Invalid =
        view.getPropertyView<glm::imat2x2, true>(
            primitive,
            "TestClassProperty");
    REQUIRE(
        imat2Invalid.status() ==
        PropertyAttributePropertyViewStatus::ErrorComponentTypeMismatch);
  }

  SECTION("Access incorrectly as non-normalized") {
    PropertyAttributePropertyView<glm::u16mat2x2> nonNormalizedInvalid =
        view.getPropertyView<glm::u16mat2x2>(primitive, "TestClassProperty");
    REQUIRE(
        nonNormalizedInvalid.status() ==
        PropertyAttributePropertyViewStatus::ErrorNormalizationMismatch);
  }

  SECTION("Access incorrectly as dmat2") {
    PropertyAttributePropertyView<glm::dmat2> dmat2Invalid =
        view.getPropertyView<glm::dmat2>(primitive, "TestClassProperty");
    REQUIRE(
        dmat2Invalid.status() ==
        PropertyAttributePropertyViewStatus::ErrorComponentTypeMismatch);
  }

  SECTION("Wrong accessor normalization") {
    model.accessors[accessorIndex].normalized = false;
    PropertyAttributePropertyView<glm::u16mat2x2, true> property =
        view.getPropertyView<glm::u16mat2x2, true>(
            primitive,
            "TestClassProperty");
    REQUIRE(
        property.status() == PropertyAttributePropertyViewStatus::
                                 ErrorAccessorNormalizationMismatch);
  }
}

TEST_CASE("Test with PropertyAttributeProperty offset, scale, min, max") {
  Model model;
  Mesh& mesh = model.meshes.emplace_back();
  MeshPrimitive& primitive = mesh.primitives.emplace_back();

  std::vector<float> data{1.0f, 2.0f, 3.0f, 4.0f};

  const float offset = 1.0f;
  const float scale = 2.0f;
  const float min = 3.0f;
  const float max = 9.0f;

  const std::string attributeName = "_ATTRIBUTE";
  addAttributeToModel(model, primitive, attributeName, data);

  ExtensionModelExtStructuralMetadata& metadata =
      model.addExtension<ExtensionModelExtStructuralMetadata>();

  Schema& schema = metadata.schema.emplace();
  Class& testClass = schema.classes["TestClass"];
  ClassProperty& testClassProperty = testClass.properties["TestClassProperty"];
  testClassProperty.type = ClassProperty::Type::SCALAR;
  testClassProperty.componentType = ClassProperty::ComponentType::FLOAT32;
  testClassProperty.offset = offset;
  testClassProperty.scale = scale;
  testClassProperty.min = min;
  testClassProperty.max = max;

  PropertyAttribute& propertyAttribute =
      metadata.propertyAttributes.emplace_back();
  propertyAttribute.classProperty = "TestClass";

  PropertyAttributeProperty& propertyAttributeProperty =
      propertyAttribute.properties["TestClassProperty"];
  propertyAttributeProperty.attribute = attributeName;

  PropertyAttributeView view(model, propertyAttribute);
  REQUIRE(view.status() == PropertyAttributeViewStatus::Valid);

  const ClassProperty* classProperty =
      view.getClassProperty("TestClassProperty");
  REQUIRE(classProperty);
  REQUIRE(classProperty->type == ClassProperty::Type::SCALAR);
  REQUIRE(
      classProperty->componentType == ClassProperty::ComponentType::FLOAT32);
  REQUIRE(classProperty->count == std::nullopt);
  REQUIRE(!classProperty->array);
  REQUIRE(!classProperty->normalized);
  REQUIRE(classProperty->offset);
  REQUIRE(classProperty->scale);
  REQUIRE(classProperty->min);
  REQUIRE(classProperty->max);

  SECTION("Use class property values") {
    PropertyAttributePropertyView<float> property =
        view.getPropertyView<float>(primitive, "TestClassProperty");
    REQUIRE(property.status() == PropertyAttributePropertyViewStatus::Valid);
    REQUIRE(property.offset() == offset);
    REQUIRE(property.scale() == scale);
    REQUIRE(property.min() == min);
    REQUIRE(property.max() == max);

    std::vector<float> expected{3.0f, 5.0f, 7.0f, 9.0f};
    for (size_t i = 0; i < data.size(); ++i) {
      REQUIRE(property.getRaw(static_cast<int64_t>(i)) == data[i]);
      REQUIRE(property.get(static_cast<int64_t>(i)) == expected[i]);
    }
  }

  SECTION("Use own property values") {
    const float newOffset = 0.5f;
    const float newScale = -1.0f;
    const float newMin = -3.5f;
    const float newMax = -0.5f;
    propertyAttributeProperty.offset = newOffset;
    propertyAttributeProperty.scale = newScale;
    propertyAttributeProperty.min = newMin;
    propertyAttributeProperty.max = newMax;

    PropertyAttributePropertyView<float> property =
        view.getPropertyView<float>(primitive, "TestClassProperty");
    REQUIRE(property.status() == PropertyAttributePropertyViewStatus::Valid);
    REQUIRE(property.offset() == newOffset);
    REQUIRE(property.scale() == newScale);
    REQUIRE(property.min() == newMin);
    REQUIRE(property.max() == newMax);

    std::vector<float> expected{-0.5f, -1.5f, -2.5f, -3.5f};
    for (size_t i = 0; i < data.size(); ++i) {
      REQUIRE(property.getRaw(static_cast<int64_t>(i)) == data[i]);
      REQUIRE(property.get(static_cast<int64_t>(i)) == expected[i]);
    }
  }
}

TEST_CASE("Test with PropertyAttributeProperty offset, scale, min, max "
          "(normalized)") {
  Model model;

  Mesh& mesh = model.meshes.emplace_back();
  MeshPrimitive& primitive = mesh.primitives.emplace_back();

  std::vector<uint8_t> data{0, 128, 255, 32};

  const double offset = 1.0;
  const double scale = 2.0;
  const double min = 1.0;
  const double max = 3.0;

  const std::string attributeName = "_ATTRIBUTE";
  addAttributeToModel<uint8_t, true>(model, primitive, attributeName, data);

  ExtensionModelExtStructuralMetadata& metadata =
      model.addExtension<ExtensionModelExtStructuralMetadata>();

  Schema& schema = metadata.schema.emplace();
  Class& testClass = schema.classes["TestClass"];
  ClassProperty& testClassProperty = testClass.properties["TestClassProperty"];
  testClassProperty.type = ClassProperty::Type::SCALAR;
  testClassProperty.componentType = ClassProperty::ComponentType::UINT8;
  testClassProperty.normalized = true;
  testClassProperty.offset = offset;
  testClassProperty.scale = scale;
  testClassProperty.min = min;
  testClassProperty.max = max;

  PropertyAttribute& propertyAttribute =
      metadata.propertyAttributes.emplace_back();
  propertyAttribute.classProperty = "TestClass";

  PropertyAttributeProperty& propertyAttributeProperty =
      propertyAttribute.properties["TestClassProperty"];
  propertyAttributeProperty.attribute = attributeName;

  PropertyAttributeView view(model, propertyAttribute);
  REQUIRE(view.status() == PropertyAttributeViewStatus::Valid);

  const ClassProperty* classProperty =
      view.getClassProperty("TestClassProperty");
  REQUIRE(classProperty);
  REQUIRE(classProperty->type == ClassProperty::Type::SCALAR);
  REQUIRE(classProperty->componentType == ClassProperty::ComponentType::UINT8);
  REQUIRE(classProperty->count == std::nullopt);
  REQUIRE(!classProperty->array);
  REQUIRE(classProperty->normalized);

  std::vector<glm::dvec2> texCoords{
      glm::dvec2(0, 0),
      glm::dvec2(0.5, 0),
      glm::dvec2(0, 0.5),
      glm::dvec2(0.5, 0.5)};

  SECTION("Use class property values") {
    PropertyAttributePropertyView<uint8_t, true> property =
        view.getPropertyView<uint8_t, true>(primitive, "TestClassProperty");
    REQUIRE(property.status() == PropertyAttributePropertyViewStatus::Valid);
    REQUIRE(property.offset() == offset);
    REQUIRE(property.scale() == scale);
    REQUIRE(property.min() == min);
    REQUIRE(property.max() == max);

    for (size_t i = 0; i < data.size(); ++i) {
      REQUIRE(property.getRaw(static_cast<int64_t>(i)) == data[i]);
      REQUIRE(
          property.get(static_cast<int64_t>(i)) ==
          normalize(data[i]) * scale + offset);
    }
  }

  SECTION("Use own property values") {
    const double newOffset = 2.0;
    const double newScale = 5.0;
    const double newMin = 10.0;
    const double newMax = 11.0;
    propertyAttributeProperty.offset = newOffset;
    propertyAttributeProperty.scale = newScale;
    propertyAttributeProperty.min = newMin;
    propertyAttributeProperty.max = newMax;

    PropertyAttributePropertyView<uint8_t, true> property =
        view.getPropertyView<uint8_t, true>(primitive, "TestClassProperty");
    REQUIRE(property.status() == PropertyAttributePropertyViewStatus::Valid);
    REQUIRE(property.offset() == newOffset);
    REQUIRE(property.scale() == newScale);
    REQUIRE(property.min() == newMin);
    REQUIRE(property.max() == newMax);

    for (size_t i = 0; i < data.size(); ++i) {
      REQUIRE(property.getRaw(static_cast<int64_t>(i)) == data[i]);
      REQUIRE(
          property.get(static_cast<int64_t>(i)) ==
          normalize(data[i]) * newScale + newOffset);
    }
  }
}

TEST_CASE("Test with PropertyAttributeProperty noData") {
  Model model;
  Mesh& mesh = model.meshes.emplace_back();
  MeshPrimitive& primitive = mesh.primitives.emplace_back();

  std::vector<uint8_t> data = {12, 34, 30, 11};
  const uint8_t noData = 34;

  const std::string attributeName = "_ATTRIBUTE";
  addAttributeToModel(model, primitive, attributeName, data);

  ExtensionModelExtStructuralMetadata& metadata =
      model.addExtension<ExtensionModelExtStructuralMetadata>();

  Schema& schema = metadata.schema.emplace();
  Class& testClass = schema.classes["TestClass"];
  ClassProperty& testClassProperty = testClass.properties["TestClassProperty"];
  testClassProperty.type = ClassProperty::Type::SCALAR;
  testClassProperty.componentType = ClassProperty::ComponentType::UINT8;
  testClassProperty.noData = noData;

  PropertyAttribute& propertyAttribute =
      metadata.propertyAttributes.emplace_back();
  propertyAttribute.classProperty = "TestClass";

  PropertyAttributeProperty& propertyAttributeProperty =
      propertyAttribute.properties["TestClassProperty"];
  propertyAttributeProperty.attribute = attributeName;

  PropertyAttributeView view(model, propertyAttribute);
  REQUIRE(view.status() == PropertyAttributeViewStatus::Valid);

  const ClassProperty* classProperty =
      view.getClassProperty("TestClassProperty");
  REQUIRE(classProperty);
  REQUIRE(classProperty->type == ClassProperty::Type::SCALAR);
  REQUIRE(classProperty->componentType == ClassProperty::ComponentType::UINT8);
  REQUIRE(classProperty->count == std::nullopt);
  REQUIRE(!classProperty->array);
  REQUIRE(!classProperty->normalized);

  SECTION("Without default value") {
    PropertyAttributePropertyView<uint8_t> property =
        view.getPropertyView<uint8_t>(primitive, "TestClassProperty");
    REQUIRE(property.status() == PropertyAttributePropertyViewStatus::Valid);

    for (size_t i = 0; i < data.size(); ++i) {
      auto value = property.getRaw(static_cast<int64_t>(i));
      REQUIRE(value == data[i]);

      auto maybeValue = property.get(static_cast<int64_t>(i));
      if (value == noData) {
        REQUIRE(!maybeValue);
      } else {
        REQUIRE(maybeValue == data[i]);
      }
    }
  }

  SECTION("With default value") {
    const uint8_t defaultValue = 255;
    testClassProperty.defaultProperty = defaultValue;

    PropertyAttributePropertyView<uint8_t> property =
        view.getPropertyView<uint8_t>(primitive, "TestClassProperty");
    REQUIRE(property.status() == PropertyAttributePropertyViewStatus::Valid);

    for (size_t i = 0; i < data.size(); ++i) {
      auto value = property.getRaw(static_cast<int64_t>(i));
      REQUIRE(value == data[i]);

      auto maybeValue = property.get(static_cast<int64_t>(i));
      if (value == noData) {
        REQUIRE(maybeValue == defaultValue);
      } else {
        REQUIRE(maybeValue == data[i]);
      }
    }
  }
}

TEST_CASE("Test with PropertyAttributeProperty noData (normalized)") {
  Model model;
  Mesh& mesh = model.meshes.emplace_back();
  MeshPrimitive& primitive = mesh.primitives.emplace_back();

  std::vector<uint8_t> data = {12, 34, 30, 11};
  const uint8_t noData = 34;

  const std::string attributeName = "_ATTRIBUTE";
  addAttributeToModel<uint8_t, true>(model, primitive, attributeName, data);

  ExtensionModelExtStructuralMetadata& metadata =
      model.addExtension<ExtensionModelExtStructuralMetadata>();

  Schema& schema = metadata.schema.emplace();
  Class& testClass = schema.classes["TestClass"];
  ClassProperty& testClassProperty = testClass.properties["TestClassProperty"];
  testClassProperty.type = ClassProperty::Type::SCALAR;
  testClassProperty.componentType = ClassProperty::ComponentType::UINT8;
  testClassProperty.normalized = true;
  testClassProperty.noData = noData;

  PropertyAttribute& propertyAttribute =
      metadata.propertyAttributes.emplace_back();
  propertyAttribute.classProperty = "TestClass";

  PropertyAttributeProperty& propertyAttributeProperty =
      propertyAttribute.properties["TestClassProperty"];
  propertyAttributeProperty.attribute = attributeName;

  PropertyAttributeView view(model, propertyAttribute);
  REQUIRE(view.status() == PropertyAttributeViewStatus::Valid);

  const ClassProperty* classProperty =
      view.getClassProperty("TestClassProperty");
  REQUIRE(classProperty);
  REQUIRE(classProperty->type == ClassProperty::Type::SCALAR);
  REQUIRE(classProperty->componentType == ClassProperty::ComponentType::UINT8);
  REQUIRE(classProperty->count == std::nullopt);
  REQUIRE(!classProperty->array);
  REQUIRE(classProperty->normalized);

  SECTION("Without default value") {
    PropertyAttributePropertyView<uint8_t, true> property =
        view.getPropertyView<uint8_t, true>(primitive, "TestClassProperty");
    REQUIRE(property.status() == PropertyAttributePropertyViewStatus::Valid);

    for (size_t i = 0; i < data.size(); ++i) {
      auto value = property.getRaw(static_cast<int64_t>(i));
      REQUIRE(value == data[i]);

      auto maybeValue = property.get(static_cast<int64_t>(i));
      if (value == noData) {
        REQUIRE(!maybeValue);
      } else {
        REQUIRE(maybeValue == normalize(data[i]));
      }
    }
  }

  SECTION("With default value") {
    const double defaultValue = -1.0;
    testClassProperty.defaultProperty = defaultValue;

    PropertyAttributePropertyView<uint8_t, true> property =
        view.getPropertyView<uint8_t, true>(primitive, "TestClassProperty");
    REQUIRE(property.status() == PropertyAttributePropertyViewStatus::Valid);

    for (size_t i = 0; i < data.size(); ++i) {
      auto value = property.getRaw(static_cast<int64_t>(i));
      REQUIRE(value == data[i]);

      auto maybeValue = property.get(static_cast<int64_t>(i));
      if (value == noData) {
        REQUIRE(maybeValue == defaultValue);
      } else {
        REQUIRE(maybeValue == normalize(data[i]));
      }
    }
  }
}

TEST_CASE(
    "Test nonexistent PropertyAttributeProperty with class property default") {
  Model model;
  Mesh& mesh = model.meshes.emplace_back();
  MeshPrimitive& primitive = mesh.primitives.emplace_back();

  // Add a position attribute so it can be used to substitute the property
  // accessor size.
  const std::string attributeName = "POSITION";
  std::vector<glm::vec3> data = {
      glm::vec3(0),
      glm::vec3(1, 2, 3),
      glm::vec3(0, 1, 0)};

  addAttributeToModel(model, primitive, attributeName, data);

  ExtensionModelExtStructuralMetadata& metadata =
      model.addExtension<ExtensionModelExtStructuralMetadata>();

  Schema& schema = metadata.schema.emplace();
  Class& testClass = schema.classes["TestClass"];
  ClassProperty& testClassProperty = testClass.properties["TestClassProperty"];
  testClassProperty.type = ClassProperty::Type::SCALAR;
  testClassProperty.componentType = ClassProperty::ComponentType::UINT16;

  const uint16_t defaultValue = 10;
  testClassProperty.defaultProperty = defaultValue;

  PropertyAttribute& propertyAttribute =
      metadata.propertyAttributes.emplace_back();
  propertyAttribute.classProperty = "TestClass";

  PropertyAttributeView view(model, propertyAttribute);
  REQUIRE(view.status() == PropertyAttributeViewStatus::Valid);

  const ClassProperty* classProperty =
      view.getClassProperty("TestClassProperty");
  REQUIRE(classProperty);
  REQUIRE(classProperty->type == ClassProperty::Type::SCALAR);
  REQUIRE(classProperty->componentType == ClassProperty::ComponentType::UINT16);
  REQUIRE(classProperty->count == std::nullopt);
  REQUIRE(!classProperty->array);
  REQUIRE(!classProperty->normalized);
  REQUIRE(classProperty->defaultProperty);

  SECTION("Access correct type") {
    PropertyAttributePropertyView<uint16_t> uint16Property =
        view.getPropertyView<uint16_t>(primitive, "TestClassProperty");
    REQUIRE(
        uint16Property.status() ==
        PropertyAttributePropertyViewStatus::EmptyPropertyWithDefault);
    REQUIRE(uint16Property.size() == static_cast<int64_t>(data.size()));
    REQUIRE(uint16Property.defaultValue() == defaultValue);

    for (size_t i = 0; i < data.size(); ++i) {
      REQUIRE(uint16Property.get(static_cast<int64_t>(i)) == defaultValue);
    }
  }

  SECTION("Access wrong type") {
    PropertyAttributePropertyView<glm::u16vec2> u16vec2Invalid =
        view.getPropertyView<glm::u16vec2>(primitive, "TestClassProperty");
    REQUIRE(
        u16vec2Invalid.status() ==
        PropertyAttributePropertyViewStatus::ErrorTypeMismatch);
  }

  SECTION("Access wrong component type") {
    PropertyAttributePropertyView<uint8_t> uint8Invalid =
        view.getPropertyView<uint8_t>(primitive, "TestClassProperty");
    REQUIRE(
        uint8Invalid.status() ==
        PropertyAttributePropertyViewStatus::ErrorComponentTypeMismatch);
  }

  SECTION("Access incorrectly as normalized") {
    PropertyAttributePropertyView<uint16_t, true> normalizedInvalid =
        view.getPropertyView<uint16_t, true>(primitive, "TestClassProperty");
    REQUIRE(
        normalizedInvalid.status() ==
        PropertyAttributePropertyViewStatus::ErrorNormalizationMismatch);
  }

  SECTION("Invalid default value") {
    testClassProperty.defaultProperty = "not a number";
    PropertyAttributePropertyView<uint16_t> uint16Property =
        view.getPropertyView<uint16_t>(primitive, "TestClassProperty");
    REQUIRE(
        uint16Property.status() ==
        PropertyAttributePropertyViewStatus::ErrorInvalidDefaultValue);
  }

  SECTION("No default value") {
    testClassProperty.defaultProperty.reset();
    PropertyAttributePropertyView<uint16_t> uint16Property =
        view.getPropertyView<uint16_t>(primitive, "TestClassProperty");
    REQUIRE(
        uint16Property.status() ==
        PropertyAttributePropertyViewStatus::ErrorNonexistentProperty);
  }
}

TEST_CASE("Test callback on invalid property Attribute view") {
  Model model;
  Mesh& mesh = model.meshes.emplace_back();
  MeshPrimitive& primitive = mesh.primitives.emplace_back();

  ExtensionModelExtStructuralMetadata& metadata =
      model.addExtension<ExtensionModelExtStructuralMetadata>();
  metadata.schema.emplace();

  // Property Attribute has a nonexistent class.
  PropertyAttribute& propertyAttribute =
      metadata.propertyAttributes.emplace_back();
  propertyAttribute.classProperty = "TestClass";

  PropertyAttributeProperty& propertyAttributeProperty =
      propertyAttribute.properties["TestClassProperty"];
  propertyAttributeProperty.attribute = "_INVALID";

  PropertyAttributeView view(model, propertyAttribute);
  REQUIRE(view.status() == PropertyAttributeViewStatus::ErrorClassNotFound);

  const ClassProperty* classProperty =
      view.getClassProperty("TestClassProperty");
  REQUIRE(!classProperty);

  uint32_t invokedCallbackCount = 0;
  view.getPropertyView(
      primitive,
      "TestClassProperty",
      [&invokedCallbackCount](
          const std::string& /*propertyId*/,
          auto propertyValue) mutable {
        invokedCallbackCount++;
        REQUIRE(
            propertyValue.status() ==
            PropertyAttributePropertyViewStatus::ErrorInvalidPropertyAttribute);
      });

  REQUIRE(invokedCallbackCount == 1);
}

TEST_CASE("Test callback on invalid PropertyAttributeProperty") {
  Model model;
  Mesh& mesh = model.meshes.emplace_back();
  MeshPrimitive& primitive = mesh.primitives.emplace_back();

  ExtensionModelExtStructuralMetadata& metadata =
      model.addExtension<ExtensionModelExtStructuralMetadata>();

  Schema& schema = metadata.schema.emplace();
  Class& testClass = schema.classes["TestClass"];
  ClassProperty& testClassProperty = testClass.properties["InvalidProperty"];
  testClassProperty.type = ClassProperty::Type::SCALAR;
  testClassProperty.componentType = ClassProperty::ComponentType::UINT8;

  PropertyAttribute& propertyAttribute =
      metadata.propertyAttributes.emplace_back();
  propertyAttribute.classProperty = "TestClass";

  PropertyAttributeProperty& propertyAttributeProperty =
      propertyAttribute.properties["InvalidProperty"];
  propertyAttributeProperty.attribute = "_INVALID";

  PropertyAttributeView view(model, propertyAttribute);
  REQUIRE(view.status() == PropertyAttributeViewStatus::Valid);

  const ClassProperty* classProperty = view.getClassProperty("InvalidProperty");
  REQUIRE(classProperty);

  classProperty = view.getClassProperty("NonexistentProperty");
  REQUIRE(!classProperty);

  uint32_t invokedCallbackCount = 0;
  auto testCallback = [&invokedCallbackCount](
                          const std::string& /*propertyId*/,
                          auto propertyValue) mutable {
    invokedCallbackCount++;
    REQUIRE(
        propertyValue.status() != PropertyAttributePropertyViewStatus::Valid);
  };

  view.getPropertyView(primitive, "InvalidProperty", testCallback);
  view.getPropertyView(primitive, "NonexistentProperty", testCallback);

  REQUIRE(invokedCallbackCount == 2);
}

TEST_CASE("Test callback on invalid normalized PropertyAttributeProperty") {
  Model model;
  Mesh& mesh = model.meshes.emplace_back();
  MeshPrimitive& primitive = mesh.primitives.emplace_back();

  ExtensionModelExtStructuralMetadata& metadata =
      model.addExtension<ExtensionModelExtStructuralMetadata>();

  Schema& schema = metadata.schema.emplace();
  Class& testClass = schema.classes["TestClass"];
  ClassProperty& testClassProperty = testClass.properties["TestClassProperty"];
  testClassProperty.type = ClassProperty::Type::SCALAR;
  testClassProperty.componentType = ClassProperty::ComponentType::FLOAT32;
  testClassProperty.normalized = true; // This is erroneous.

  PropertyAttribute& propertyAttribute =
      metadata.propertyAttributes.emplace_back();
  propertyAttribute.classProperty = "TestClass";

  PropertyAttributeProperty& propertyAttributeProperty =
      propertyAttribute.properties["TestClassProperty"];
  propertyAttributeProperty.attribute = "_INVALID";

  PropertyAttributeView view(model, propertyAttribute);
  REQUIRE(view.status() == PropertyAttributeViewStatus::Valid);

  const ClassProperty* classProperty =
      view.getClassProperty("TestClassProperty");
  REQUIRE(classProperty);

  uint32_t invokedCallbackCount = 0;
  auto testCallback = [&invokedCallbackCount](
                          const std::string& /*propertyId*/,
                          auto propertyValue) mutable {
    invokedCallbackCount++;
    REQUIRE(
        propertyValue.status() ==
        PropertyAttributePropertyViewStatus::ErrorInvalidNormalization);
  };

  view.getPropertyView(primitive, "TestClassProperty", testCallback);

  REQUIRE(invokedCallbackCount == 1);
}

TEST_CASE("Test callback for scalar PropertyAttributeProperty") {
  Model model;
  Mesh& mesh = model.meshes.emplace_back();
  MeshPrimitive& primitive = mesh.primitives.emplace_back();

  std::vector<int16_t> data{-1, 268, 542, -256};

  const std::string attributeName = "_ATTRIBUTE";
  addAttributeToModel(model, primitive, attributeName, data);

  ExtensionModelExtStructuralMetadata& metadata =
      model.addExtension<ExtensionModelExtStructuralMetadata>();

  Schema& schema = metadata.schema.emplace();
  Class& testClass = schema.classes["TestClass"];
  ClassProperty& testClassProperty = testClass.properties["TestClassProperty"];
  testClassProperty.type = ClassProperty::Type::SCALAR;
  testClassProperty.componentType = ClassProperty::ComponentType::INT16;

  PropertyAttribute& propertyAttribute =
      metadata.propertyAttributes.emplace_back();
  propertyAttribute.classProperty = "TestClass";

  PropertyAttributeProperty& propertyAttributeProperty =
      propertyAttribute.properties["TestClassProperty"];
  propertyAttributeProperty.attribute = attributeName;

  PropertyAttributeView view(model, propertyAttribute);
  REQUIRE(view.status() == PropertyAttributeViewStatus::Valid);

  const ClassProperty* classProperty =
      view.getClassProperty("TestClassProperty");
  REQUIRE(classProperty);
  REQUIRE(classProperty->type == ClassProperty::Type::SCALAR);
  REQUIRE(classProperty->componentType == ClassProperty::ComponentType::INT16);
  REQUIRE(classProperty->count == std::nullopt);
  REQUIRE(!classProperty->array);
  REQUIRE(!classProperty->normalized);

  uint32_t invokedCallbackCount = 0;
  view.getPropertyView(
      primitive,
      "TestClassProperty",
      [&data, &invokedCallbackCount](
          const std::string& /*propertyId*/,
          auto propertyValue) mutable {
        invokedCallbackCount++;
        if constexpr (std::is_same_v<
                          PropertyAttributePropertyView<int16_t>,
                          decltype(propertyValue)>) {
          REQUIRE(
              propertyValue.status() ==
              PropertyAttributePropertyViewStatus::Valid);

          for (size_t i = 0; i < data.size(); ++i) {
            REQUIRE(propertyValue.getRaw(static_cast<int64_t>(i)) == data[i]);
            REQUIRE(propertyValue.get(static_cast<int64_t>(i)) == data[i]);
          }
        } else {
          FAIL("getPropertyView returned PropertyAttributePropertyView of "
               "incorrect type for TestClassProperty.");
        }
      });

  REQUIRE(invokedCallbackCount == 1);
}

TEST_CASE("Test callback for scalar PropertyAttributeProperty (normalized)") {
  Model model;
  Mesh& mesh = model.meshes.emplace_back();
  MeshPrimitive& primitive = mesh.primitives.emplace_back();

  std::vector<int16_t> data{-1, 268, 542, -256};

  const std::string attributeName = "_ATTRIBUTE";
  addAttributeToModel<int16_t, true>(model, primitive, attributeName, data);

  ExtensionModelExtStructuralMetadata& metadata =
      model.addExtension<ExtensionModelExtStructuralMetadata>();

  Schema& schema = metadata.schema.emplace();
  Class& testClass = schema.classes["TestClass"];
  ClassProperty& testClassProperty = testClass.properties["TestClassProperty"];
  testClassProperty.type = ClassProperty::Type::SCALAR;
  testClassProperty.componentType = ClassProperty::ComponentType::INT16;
  testClassProperty.normalized = true;

  PropertyAttribute& propertyAttribute =
      metadata.propertyAttributes.emplace_back();
  propertyAttribute.classProperty = "TestClass";

  PropertyAttributeProperty& propertyAttributeProperty =
      propertyAttribute.properties["TestClassProperty"];
  propertyAttributeProperty.attribute = attributeName;

  PropertyAttributeView view(model, propertyAttribute);
  REQUIRE(view.status() == PropertyAttributeViewStatus::Valid);

  const ClassProperty* classProperty =
      view.getClassProperty("TestClassProperty");
  REQUIRE(classProperty);
  REQUIRE(classProperty->type == ClassProperty::Type::SCALAR);
  REQUIRE(classProperty->componentType == ClassProperty::ComponentType::INT16);
  REQUIRE(classProperty->count == std::nullopt);
  REQUIRE(!classProperty->array);
  REQUIRE(classProperty->normalized);

  uint32_t invokedCallbackCount = 0;
  view.getPropertyView(
      primitive,
      "TestClassProperty",
      [&data, &invokedCallbackCount](
          const std::string& /*propertyId*/,
          auto propertyValue) mutable {
        invokedCallbackCount++;
        if constexpr (std::is_same_v<
                          PropertyAttributePropertyView<int16_t, true>,
                          decltype(propertyValue)>) {
          REQUIRE(
              propertyValue.status() ==
              PropertyAttributePropertyViewStatus::Valid);

          for (size_t i = 0; i < data.size(); ++i) {
            REQUIRE(propertyValue.getRaw(static_cast<int64_t>(i)) == data[i]);
            REQUIRE(
                propertyValue.get(static_cast<int64_t>(i)) ==
                normalize(data[i]));
          }
        } else {
          FAIL("getPropertyView returned PropertyAttributePropertyView of "
               "incorrect type for TestClassProperty.");
        }
      });

  REQUIRE(invokedCallbackCount == 1);
}

TEST_CASE("Test callback for vecN PropertyAttributeProperty") {
  Model model;
  Mesh& mesh = model.meshes.emplace_back();
  MeshPrimitive& primitive = mesh.primitives.emplace_back();

  std::vector<glm::i8vec2> data{
      glm::i8vec2(-1, -1),
      glm::i8vec2(12, 1),
      glm::i8vec2(30, 2),
      glm::i8vec2(0, -1)};

  const std::string attributeName = "_ATTRIBUTE";
  addAttributeToModel(model, primitive, attributeName, data);

  ExtensionModelExtStructuralMetadata& metadata =
      model.addExtension<ExtensionModelExtStructuralMetadata>();

  Schema& schema = metadata.schema.emplace();
  Class& testClass = schema.classes["TestClass"];
  ClassProperty& testClassProperty = testClass.properties["TestClassProperty"];
  testClassProperty.type = ClassProperty::Type::VEC2;
  testClassProperty.componentType = ClassProperty::ComponentType::INT8;

  PropertyAttribute& propertyAttribute =
      metadata.propertyAttributes.emplace_back();
  propertyAttribute.classProperty = "TestClass";

  PropertyAttributeProperty& propertyAttributeProperty =
      propertyAttribute.properties["TestClassProperty"];
  propertyAttributeProperty.attribute = attributeName;

  PropertyAttributeView view(model, propertyAttribute);
  REQUIRE(view.status() == PropertyAttributeViewStatus::Valid);

  const ClassProperty* classProperty =
      view.getClassProperty("TestClassProperty");
  REQUIRE(classProperty);
  REQUIRE(classProperty->type == ClassProperty::Type::VEC2);
  REQUIRE(classProperty->componentType == ClassProperty::ComponentType::INT8);
  REQUIRE(classProperty->count == std::nullopt);
  REQUIRE(!classProperty->array);
  REQUIRE(!classProperty->normalized);

  uint32_t invokedCallbackCount = 0;
  view.getPropertyView(
      primitive,
      "TestClassProperty",
      [&data, &invokedCallbackCount](
          const std::string& /*propertyId*/,
          auto propertyValue) mutable {
        invokedCallbackCount++;
        if constexpr (std::is_same_v<
                          PropertyAttributePropertyView<glm::i8vec2>,
                          decltype(propertyValue)>) {
          REQUIRE(
              propertyValue.status() ==
              PropertyAttributePropertyViewStatus::Valid);

          for (size_t i = 0; i < data.size(); ++i) {
            REQUIRE(propertyValue.getRaw(static_cast<int64_t>(i)) == data[i]);
            REQUIRE(propertyValue.get(static_cast<int64_t>(i)) == data[i]);
          }
        } else {
          FAIL("getPropertyView returned PropertyAttributePropertyView of "
               "incorrect type for TestClassProperty.");
        }
      });

  REQUIRE(invokedCallbackCount == 1);
}

TEST_CASE("Test callback for vecN PropertyAttributeProperty (normalized)") {
  Model model;
  Mesh& mesh = model.meshes.emplace_back();
  MeshPrimitive& primitive = mesh.primitives.emplace_back();

  std::vector<glm::i8vec2> data{
      glm::i8vec2(-1, -1),
      glm::i8vec2(12, 1),
      glm::i8vec2(30, 2),
      glm::i8vec2(0, -1)};

  const std::string attributeName = "_ATTRIBUTE";
  addAttributeToModel<glm::i8vec2, true>(model, primitive, attributeName, data);

  ExtensionModelExtStructuralMetadata& metadata =
      model.addExtension<ExtensionModelExtStructuralMetadata>();

  Schema& schema = metadata.schema.emplace();
  Class& testClass = schema.classes["TestClass"];
  ClassProperty& testClassProperty = testClass.properties["TestClassProperty"];
  testClassProperty.type = ClassProperty::Type::VEC2;
  testClassProperty.componentType = ClassProperty::ComponentType::INT8;
  testClassProperty.normalized = true;

  PropertyAttribute& propertyAttribute =
      metadata.propertyAttributes.emplace_back();
  propertyAttribute.classProperty = "TestClass";

  PropertyAttributeProperty& propertyAttributeProperty =
      propertyAttribute.properties["TestClassProperty"];
  propertyAttributeProperty.attribute = attributeName;

  PropertyAttributeView view(model, propertyAttribute);
  REQUIRE(view.status() == PropertyAttributeViewStatus::Valid);

  const ClassProperty* classProperty =
      view.getClassProperty("TestClassProperty");
  REQUIRE(classProperty);
  REQUIRE(classProperty->type == ClassProperty::Type::VEC2);
  REQUIRE(classProperty->componentType == ClassProperty::ComponentType::INT8);
  REQUIRE(classProperty->count == std::nullopt);
  REQUIRE(!classProperty->array);
  REQUIRE(classProperty->normalized);

  uint32_t invokedCallbackCount = 0;
  view.getPropertyView(
      primitive,
      "TestClassProperty",
      [&data, &invokedCallbackCount](
          const std::string& /*propertyId*/,
          auto propertyValue) mutable {
        invokedCallbackCount++;
        if constexpr (std::is_same_v<
                          PropertyAttributePropertyView<glm::i8vec2, true>,
                          decltype(propertyValue)>) {
          REQUIRE(
              propertyValue.status() ==
              PropertyAttributePropertyViewStatus::Valid);

          for (size_t i = 0; i < data.size(); ++i) {
            REQUIRE(propertyValue.getRaw(static_cast<int64_t>(i)) == data[i]);
            REQUIRE(
                propertyValue.get(static_cast<int64_t>(i)) ==
                normalize(data[i]));
          }
        } else {
          FAIL("getPropertyView returned PropertyAttributePropertyView of "
               "incorrect type for TestClassProperty.");
        }
      });

  REQUIRE(invokedCallbackCount == 1);
}

TEST_CASE("Test callback for matN PropertyAttributeProperty") {
  Model model;
  Mesh& mesh = model.meshes.emplace_back();
  MeshPrimitive& primitive = mesh.primitives.emplace_back();

  // clang-format off
  std::vector<glm::u16mat2x2> data = {
      glm::u16mat2x2(
        12, 34,
        30, 1),
      glm::u16mat2x2(
        11, 8,
        73, 102),
      glm::u16mat2x2(
        1, 0,
        63, 2),
      glm::u16mat2x2(
        4, 8,
        3, 23)};
  // clang-format on

  const std::string attributeName = "_ATTRIBUTE";
  addAttributeToModel(model, primitive, attributeName, data);

  ExtensionModelExtStructuralMetadata& metadata =
      model.addExtension<ExtensionModelExtStructuralMetadata>();

  Schema& schema = metadata.schema.emplace();
  Class& testClass = schema.classes["TestClass"];
  ClassProperty& testClassProperty = testClass.properties["TestClassProperty"];
  testClassProperty.type = ClassProperty::Type::MAT2;
  testClassProperty.componentType = ClassProperty::ComponentType::UINT16;

  PropertyAttribute& propertyAttribute =
      metadata.propertyAttributes.emplace_back();
  propertyAttribute.classProperty = "TestClass";

  PropertyAttributeProperty& propertyAttributeProperty =
      propertyAttribute.properties["TestClassProperty"];
  propertyAttributeProperty.attribute = attributeName;

  PropertyAttributeView view(model, propertyAttribute);
  REQUIRE(view.status() == PropertyAttributeViewStatus::Valid);

  const ClassProperty* classProperty =
      view.getClassProperty("TestClassProperty");
  REQUIRE(classProperty);
  REQUIRE(classProperty->type == ClassProperty::Type::MAT2);
  REQUIRE(classProperty->componentType == ClassProperty::ComponentType::UINT16);
  REQUIRE(classProperty->count == std::nullopt);
  REQUIRE(!classProperty->array);
  REQUIRE(!classProperty->normalized);

  uint32_t invokedCallbackCount = 0;
  view.getPropertyView(
      primitive,
      "TestClassProperty",
      [&data, &invokedCallbackCount](
          const std::string& /*propertyId*/,
          auto propertyValue) mutable {
        invokedCallbackCount++;
        if constexpr (std::is_same_v<
                          PropertyAttributePropertyView<glm::u16mat2x2>,
                          decltype(propertyValue)>) {
          REQUIRE(
              propertyValue.status() ==
              PropertyAttributePropertyViewStatus::Valid);

          for (size_t i = 0; i < data.size(); ++i) {
            REQUIRE(propertyValue.getRaw(static_cast<int64_t>(i)) == data[i]);
            REQUIRE(propertyValue.get(static_cast<int64_t>(i)) == data[i]);
          }
        } else {
          FAIL("getPropertyView returned PropertyAttributePropertyView of "
               "incorrect type for TestClassProperty.");
        }
      });

  REQUIRE(invokedCallbackCount == 1);
}

TEST_CASE("Test callback for matN PropertyAttributeProperty (normalized)") {
  Model model;
  Mesh& mesh = model.meshes.emplace_back();
  MeshPrimitive& primitive = mesh.primitives.emplace_back();

  // clang-format off
  std::vector<glm::u16mat2x2> data = {
      glm::u16mat2x2(
        12, 34,
        30, 1),
      glm::u16mat2x2(
        11, 8,
        73, 102),
      glm::u16mat2x2(
        1, 0,
        63, 2),
      glm::u16mat2x2(
        4, 8,
        3, 23)};
  // clang-format on

  const std::string attributeName = "_ATTRIBUTE";
  addAttributeToModel<glm::u16mat2x2, true>(
      model,
      primitive,
      attributeName,
      data);

  ExtensionModelExtStructuralMetadata& metadata =
      model.addExtension<ExtensionModelExtStructuralMetadata>();

  Schema& schema = metadata.schema.emplace();
  Class& testClass = schema.classes["TestClass"];
  ClassProperty& testClassProperty = testClass.properties["TestClassProperty"];
  testClassProperty.type = ClassProperty::Type::MAT2;
  testClassProperty.componentType = ClassProperty::ComponentType::UINT16;
  testClassProperty.normalized = true;

  PropertyAttribute& propertyAttribute =
      metadata.propertyAttributes.emplace_back();
  propertyAttribute.classProperty = "TestClass";

  PropertyAttributeProperty& propertyAttributeProperty =
      propertyAttribute.properties["TestClassProperty"];
  propertyAttributeProperty.attribute = attributeName;

  PropertyAttributeView view(model, propertyAttribute);
  REQUIRE(view.status() == PropertyAttributeViewStatus::Valid);

  const ClassProperty* classProperty =
      view.getClassProperty("TestClassProperty");
  REQUIRE(classProperty);
  REQUIRE(classProperty->type == ClassProperty::Type::MAT2);
  REQUIRE(classProperty->componentType == ClassProperty::ComponentType::UINT16);
  REQUIRE(classProperty->count == std::nullopt);
  REQUIRE(!classProperty->array);
  REQUIRE(classProperty->normalized);

  uint32_t invokedCallbackCount = 0;
  view.getPropertyView(
      primitive,
      "TestClassProperty",
      [&data, &invokedCallbackCount](
          const std::string& /*propertyId*/,
          auto propertyValue) mutable {
        invokedCallbackCount++;
        if constexpr (std::is_same_v<
                          PropertyAttributePropertyView<glm::u16mat2x2, true>,
                          decltype(propertyValue)>) {
          REQUIRE(
              propertyValue.status() ==
              PropertyAttributePropertyViewStatus::Valid);

          for (size_t i = 0; i < data.size(); ++i) {
            REQUIRE(propertyValue.getRaw(static_cast<int64_t>(i)) == data[i]);
            REQUIRE(
                propertyValue.get(static_cast<int64_t>(i)) ==
                normalize(data[i]));
          }
        } else {
          FAIL("getPropertyView returned PropertyAttributePropertyView of "
               "incorrect type for TestClassProperty.");
        }
      });

  REQUIRE(invokedCallbackCount == 1);
}

TEST_CASE("Test callback on unsupported PropertyAttributeProperty") {
  Model model;
  Mesh& mesh = model.meshes.emplace_back();
  MeshPrimitive& primitive = mesh.primitives.emplace_back();

  ExtensionModelExtStructuralMetadata& metadata =
      model.addExtension<ExtensionModelExtStructuralMetadata>();

  Schema& schema = metadata.schema.emplace();
  Class& testClass = schema.classes["TestClass"];

  ClassProperty& doubleClassProperty =
      testClass.properties["DoubleClassProperty"];
  doubleClassProperty.type = ClassProperty::Type::SCALAR;
  doubleClassProperty.componentType = ClassProperty::ComponentType::FLOAT64;

  ClassProperty& arrayClassProperty =
      testClass.properties["ArrayClassProperty"];
  arrayClassProperty.type = ClassProperty::Type::SCALAR;
  arrayClassProperty.componentType = ClassProperty::ComponentType::UINT8;
  arrayClassProperty.array = true;
  arrayClassProperty.count = 2;

  PropertyAttribute& propertyAttribute =
      metadata.propertyAttributes.emplace_back();
  propertyAttribute.classProperty = "TestClass";

  PropertyAttributeProperty& doubleProperty =
      propertyAttribute.properties["DoubleClassProperty"];
  doubleProperty.attribute = "_DOUBLE";
  PropertyAttributeProperty& arrayProperty =
      propertyAttribute.properties["ArrayClassProperty"];
  arrayProperty.attribute = "_ARRAY";

  PropertyAttributeView view(model, propertyAttribute);
  REQUIRE(view.status() == PropertyAttributeViewStatus::Valid);

  const ClassProperty* classProperty =
      view.getClassProperty("DoubleClassProperty");
  REQUIRE(classProperty);
  REQUIRE(classProperty->type == ClassProperty::Type::SCALAR);
  REQUIRE(
      classProperty->componentType == ClassProperty::ComponentType::FLOAT64);
  REQUIRE(!classProperty->array);

  classProperty = view.getClassProperty("ArrayClassProperty");
  REQUIRE(classProperty);
  REQUIRE(classProperty->type == ClassProperty::Type::SCALAR);
  REQUIRE(classProperty->componentType == ClassProperty::ComponentType::UINT8);
  REQUIRE(classProperty->array);
  REQUIRE(classProperty->count == 2);

  uint32_t invokedCallbackCount = 0;
  view.getPropertyView(
      primitive,
      "DoubleClassProperty",
      [&invokedCallbackCount](
          const std::string& /*propertyId*/,
          auto propertyValue) mutable {
        invokedCallbackCount++;
        REQUIRE(
            propertyValue.status() ==
            PropertyAttributePropertyViewStatus::ErrorUnsupportedProperty);
      });
  REQUIRE(invokedCallbackCount == 1);

  view.getPropertyView(
      primitive,
      "ArrayClassProperty",
      [&invokedCallbackCount](
          const std::string& /*propertyId*/,
          auto propertyValue) mutable {
        invokedCallbackCount++;
        REQUIRE(
            propertyValue.status() ==
            PropertyAttributePropertyViewStatus::ErrorUnsupportedProperty);
      });
  REQUIRE(invokedCallbackCount == 2);
}

TEST_CASE(
    "Test callback for empty PropertyAttributeProperty with default value") {
  Model model;
  Mesh& mesh = model.meshes.emplace_back();
  MeshPrimitive& primitive = mesh.primitives.emplace_back();

  // Add a position attribute so it can be used to substitute the property
  // accessor size.
  const std::string attributeName = "POSITION";
  std::vector<glm::vec3> data = {
      glm::vec3(0),
      glm::vec3(1, 2, 3),
      glm::vec3(0, 1, 0)};
  addAttributeToModel(model, primitive, attributeName, data);

  ExtensionModelExtStructuralMetadata& metadata =
      model.addExtension<ExtensionModelExtStructuralMetadata>();

  Schema& schema = metadata.schema.emplace();
  Class& testClass = schema.classes["TestClass"];
  ClassProperty& testClassProperty = testClass.properties["TestClassProperty"];
  testClassProperty.type = ClassProperty::Type::SCALAR;
  testClassProperty.componentType = ClassProperty::ComponentType::INT16;

  const int16_t defaultValue = 10;
  testClassProperty.defaultProperty = defaultValue;

  PropertyAttribute& propertyAttribute =
      metadata.propertyAttributes.emplace_back();
  propertyAttribute.classProperty = "TestClass";

  PropertyAttributeView view(model, propertyAttribute);
  REQUIRE(view.status() == PropertyAttributeViewStatus::Valid);

  const ClassProperty* classProperty =
      view.getClassProperty("TestClassProperty");
  REQUIRE(classProperty);
  REQUIRE(classProperty->type == ClassProperty::Type::SCALAR);
  REQUIRE(classProperty->componentType == ClassProperty::ComponentType::INT16);
  REQUIRE(classProperty->count == std::nullopt);
  REQUIRE(!classProperty->array);
  REQUIRE(!classProperty->normalized);
  REQUIRE(classProperty->defaultProperty);

  uint32_t invokedCallbackCount = 0;
  view.getPropertyView(
      primitive,
      "TestClassProperty",
      [defaultValue, &data, &invokedCallbackCount](
          const std::string& /*propertyId*/,
          auto propertyValue) mutable {
        invokedCallbackCount++;
        if constexpr (std::is_same_v<
                          PropertyAttributePropertyView<int16_t>,
                          decltype(propertyValue)>) {
          REQUIRE(
              propertyValue.status() ==
              PropertyAttributePropertyViewStatus::EmptyPropertyWithDefault);
          REQUIRE(propertyValue.size() == static_cast<int64_t>(data.size()));
          REQUIRE(propertyValue.defaultValue() == defaultValue);
          for (size_t i = 0; i < data.size(); ++i) {
            REQUIRE(propertyValue.get(static_cast<int64_t>(i)) == defaultValue);
          }
        } else {
          FAIL("getPropertyView returned PropertyAttributePropertyView of "
               "incorrect type for TestClassProperty.");
        }
      });

  REQUIRE(invokedCallbackCount == 1);
}
