import {
  ComponentDatatype,
  DracoLoader,
  GltfJsonLoader,
  MetadataSchemaLoader,
  Resource,
  ResourceCache,
  ResourceCacheKey,
  SupportedImageFormats,
  when,
} from "../../Source/Cesium.js";
import concatTypedArrays from "../concatTypedArrays.js";
import createScene from "../createScene.js";
import generateJsonBuffer from "../generateJsonBuffer.js";
import waitForLoaderProcess from "../waitForLoaderProcess.js";

describe(
  "ResourceCache",
  function () {
    var schemaResource = new Resource({
      url: "https://example.com/schema.json",
    });
    var schemaJson = {};

    var bufferParentResource = new Resource({
      url: "https://example.com/model.glb",
    });
    var bufferResource = new Resource({
      url: "https://example.com/external.bin",
    });

    var gltfUri = "https://example.com/model.glb";

    var gltfResource = new Resource({
      url: gltfUri,
    });

    var image = new Image();
    image.src =
      "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+ip1sAAAAASUVORK5CYII=";

    var positions = new Float32Array([-1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0]); // prettier-ignore
    var normals = new Float32Array([-1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0]); // prettier-ignore
    var indices = new Uint16Array([0, 1, 2]);

    var bufferTypedArray = concatTypedArrays([positions, normals, indices]);
    var bufferArrayBuffer = bufferTypedArray.buffer;

    var dracoBufferTypedArray = new Uint8Array([1, 3, 7, 15, 31, 63, 127, 255]);
    var dracoArrayBuffer = dracoBufferTypedArray.buffer;

    var decodedPositions = new Uint16Array([0, 0, 0, 65535, 65535, 65535, 0, 65535, 0]); // prettier-ignore
    var decodedNormals = new Uint8Array([0, 255, 128, 128, 255, 0]);
    var decodedIndices = new Uint16Array([0, 1, 2]);

    var decodeDracoResults = {
      indexArray: {
        typedArray: decodedIndices,
        numberOfIndices: decodedIndices.length,
      },
      attributeData: {
        POSITION: {
          array: decodedPositions,
          data: {
            byteOffset: 0,
            byteStride: 6,
            componentDatatype: ComponentDatatype.UNSIGNED_SHORT,
            componentsPerAttribute: 3,
            normalized: false,
            quantization: {
              octEncoded: false,
              quantizationBits: 14,
              minValues: [-1.0, -1.0, -1.0],
              range: 2.0,
            },
          },
        },
        NORMAL: {
          array: decodedNormals,
          data: {
            byteOffset: 0,
            byteStride: 2,
            componentDatatype: ComponentDatatype.UNSIGNED_BYTE,
            componentsPerAttribute: 2,
            normalized: false,
            quantization: {
              octEncoded: true,
              quantizationBits: 10,
            },
          },
        },
      },
    };

    var gltfDraco = {
      buffers: [
        {
          uri: "external.bin",
          byteLength: 8,
        },
      ],
      bufferViews: [
        {
          buffer: 0,
          byteOffset: 4,
          byteLength: 4,
        },
      ],
      accessors: [
        {
          componentType: 5126,
          count: 3,
          max: [-1.0, -1.0, -1.0],
          min: [1.0, 1.0, 1.0],
          type: "VEC3",
        },
        {
          componentType: 5126,
          count: 3,
          type: "VEC3",
        },
        {
          componentType: 5123,
          count: 3,
          type: "SCALAR",
        },
      ],
      meshes: [
        {
          primitives: [
            {
              attributes: {
                POSITION: 0,
                NORMAL: 1,
              },
              indices: 2,
              extensions: {
                KHR_draco_mesh_compression: {
                  bufferView: 0,
                  attributes: {
                    POSITION: 0,
                    NORMAL: 1,
                  },
                },
              },
            },
          ],
        },
      ],
    };

    var dracoExtension =
      gltfDraco.meshes[0].primitives[0].extensions.KHR_draco_mesh_compression;

    var gltfUncompressed = {
      buffers: [
        {
          uri: "external.bin",
          byteLength: 78,
        },
      ],
      bufferViews: [
        {
          buffer: 0,
          byteOffset: 0,
          byteLength: 36,
        },
        {
          buffer: 0,
          byteOffset: 36,
          byteLength: 36,
        },
        {
          buffer: 0,
          byteOffset: 72,
          byteLength: 6,
        },
      ],
      accessors: [
        {
          componentType: 5126,
          count: 3,
          max: [-1.0, -1.0, -1.0],
          min: [1.0, 1.0, 1.0],
          type: "VEC3",
          bufferView: 0,
          byteOffset: 0,
        },
        {
          componentType: 5126,
          count: 3,
          type: "VEC3",
          bufferView: 1,
          byteOffset: 0,
        },
        {
          componentType: 5123,
          count: 3,
          type: "SCALAR",
          bufferView: 2,
          byteOffset: 0,
        },
      ],
      meshes: [
        {
          primitives: [
            {
              attributes: {
                POSITION: 0,
                NORMAL: 1,
              },
              indices: 2,
            },
          ],
        },
      ],
    };

    var gltfWithTextures = {
      images: [
        {
          uri: "image.png",
        },
      ],
      textures: [
        {
          source: 0,
        },
      ],
      materials: [
        {
          emissiveTexture: {
            index: 0,
          },
          occlusionTexture: {
            index: 1,
          },
        },
      ],
    };

    var scene;

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

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

    afterEach(function () {
      ResourceCache.clearForSpecs();
    });

    it("loads resource", function () {
      var fetchJson = spyOn(Resource.prototype, "fetchJson").and.returnValue(
        when.resolve(schemaJson)
      );

      var cacheKey = ResourceCacheKey.getSchemaCacheKey({
        resource: schemaResource,
      });
      var resourceLoader = new MetadataSchemaLoader({
        resource: schemaResource,
        cacheKey: cacheKey,
      });

      ResourceCache.load({
        resourceLoader: resourceLoader,
      });

      var cacheEntry = ResourceCache.cacheEntries[cacheKey];
      expect(cacheEntry.referenceCount).toBe(1);
      expect(cacheEntry.resourceLoader).toBe(resourceLoader);

      return resourceLoader.promise.then(function (resourceLoader) {
        expect(fetchJson).toHaveBeenCalled();

        var schema = resourceLoader.schema;
        expect(schema).toBeDefined();
      });
    });

    it("load throws if resourceLoader is undefined", function () {
      expect(function () {
        ResourceCache.load({
          resourceLoader: undefined,
        });
      }).toThrowDeveloperError();
    });

    it("load throws if resourceLoader's cacheKey is undefined", function () {
      var resourceLoader = new MetadataSchemaLoader({
        resource: schemaResource,
      });

      expect(function () {
        ResourceCache.load({
          resourceLoader: resourceLoader,
        });
      }).toThrowDeveloperError();
    });

    it("load throws if a resource with this cacheKey is already in the cache", function () {
      var cacheKey = ResourceCacheKey.getSchemaCacheKey({
        resource: schemaResource,
      });

      var resourceLoader = new MetadataSchemaLoader({
        resource: schemaResource,
        cacheKey: cacheKey,
      });

      ResourceCache.load({
        resourceLoader: resourceLoader,
      });

      expect(function () {
        ResourceCache.load({
          resourceLoader: resourceLoader,
        });
      }).toThrowDeveloperError();
    });

    it("destroys resource when reference count reaches 0", function () {
      spyOn(Resource.prototype, "fetchJson").and.returnValue(
        when.resolve(schemaJson)
      );

      var destroy = spyOn(
        MetadataSchemaLoader.prototype,
        "destroy"
      ).and.callThrough();

      var cacheKey = ResourceCacheKey.getSchemaCacheKey({
        resource: schemaResource,
      });
      var resourceLoader = new MetadataSchemaLoader({
        resource: schemaResource,
        cacheKey: cacheKey,
      });

      ResourceCache.load({
        resourceLoader: resourceLoader,
      });

      ResourceCache.get(cacheKey);

      var cacheEntry = ResourceCache.cacheEntries[cacheKey];
      expect(cacheEntry.referenceCount).toBe(2);

      ResourceCache.unload(resourceLoader);
      expect(cacheEntry.referenceCount).toBe(1);
      expect(destroy).not.toHaveBeenCalled();

      ResourceCache.unload(resourceLoader);
      expect(cacheEntry.referenceCount).toBe(0);
      expect(destroy).toHaveBeenCalled();
      expect(ResourceCache.cacheEntries[cacheKey]).toBeUndefined();
    });

    it("unload throws if resourceLoader is undefined", function () {
      expect(function () {
        ResourceCache.unload();
      }).toThrowDeveloperError();
    });

    it("unload throws if resourceLoader is not in the cache", function () {
      var cacheKey = ResourceCacheKey.getSchemaCacheKey({
        resource: schemaResource,
      });
      var resourceLoader = new MetadataSchemaLoader({
        resource: schemaResource,
        cacheKey: cacheKey,
      });

      expect(function () {
        ResourceCache.unload(resourceLoader);
      }).toThrowDeveloperError();
    });

    it("unload throws if resourceLoader has already been unloaded from the cache", function () {
      spyOn(Resource.prototype, "fetchJson").and.returnValue(
        when.resolve(schemaJson)
      );

      var cacheKey = ResourceCacheKey.getSchemaCacheKey({
        resource: schemaResource,
      });
      var resourceLoader = new MetadataSchemaLoader({
        resource: schemaResource,
        cacheKey: cacheKey,
      });

      ResourceCache.load({
        resourceLoader: resourceLoader,
      });

      ResourceCache.unload(resourceLoader);

      expect(function () {
        ResourceCache.unload(resourceLoader);
      }).toThrowDeveloperError();
    });

    it("gets resource", function () {
      spyOn(Resource.prototype, "fetchJson").and.returnValue(
        when.resolve(schemaJson)
      );

      var cacheKey = ResourceCacheKey.getSchemaCacheKey({
        resource: schemaResource,
      });
      var resourceLoader = new MetadataSchemaLoader({
        resource: schemaResource,
        cacheKey: cacheKey,
      });

      ResourceCache.load({
        resourceLoader: resourceLoader,
      });

      var cacheEntry = ResourceCache.cacheEntries[cacheKey];

      ResourceCache.get(cacheKey);
      expect(cacheEntry.referenceCount).toBe(2);

      ResourceCache.get(cacheKey);
      expect(cacheEntry.referenceCount).toBe(3);
    });

    it("get returns undefined if there is no resource with this cache key", function () {
      var cacheKey = ResourceCacheKey.getSchemaCacheKey({
        resource: schemaResource,
      });

      expect(ResourceCache.get(cacheKey)).toBeUndefined();
    });

    it("loads schema from JSON", function () {
      var expectedCacheKey = ResourceCacheKey.getSchemaCacheKey({
        schema: schemaJson,
      });
      var schemaLoader = ResourceCache.loadSchema({
        schema: schemaJson,
      });
      var cacheEntry = ResourceCache.cacheEntries[expectedCacheKey];
      expect(schemaLoader.cacheKey).toBe(expectedCacheKey);
      expect(cacheEntry.referenceCount).toBe(1);

      // The existing resource is returned if the computed cache key is the same
      expect(
        ResourceCache.loadSchema({
          schema: schemaJson,
        })
      ).toBe(schemaLoader);

      expect(cacheEntry.referenceCount).toBe(2);

      return schemaLoader.promise.then(function (schemaLoader) {
        var schema = schemaLoader.schema;
        expect(schema).toBeDefined();
      });
    });

    it("loads external schema", function () {
      spyOn(Resource.prototype, "fetchJson").and.returnValue(
        when.resolve(schemaJson)
      );

      var expectedCacheKey = ResourceCacheKey.getSchemaCacheKey({
        resource: schemaResource,
      });
      var schemaLoader = ResourceCache.loadSchema({
        resource: schemaResource,
      });
      var cacheEntry = ResourceCache.cacheEntries[expectedCacheKey];
      expect(schemaLoader.cacheKey).toBe(expectedCacheKey);
      expect(cacheEntry.referenceCount).toBe(1);

      // The existing resource is returned if the computed cache key is the same
      expect(
        ResourceCache.loadSchema({
          resource: schemaResource,
        })
      ).toBe(schemaLoader);

      expect(cacheEntry.referenceCount).toBe(2);

      return schemaLoader.promise.then(function (schemaLoader) {
        var schema = schemaLoader.schema;
        expect(schema).toBeDefined();
      });
    });

    it("loadSchema throws if neither options.schema nor options.resource are defined", function () {
      expect(function () {
        ResourceCache.loadSchema({
          schema: undefined,
          resource: undefined,
        });
      }).toThrowDeveloperError();
    });

    it("loadSchema throws if both options.schema and options.resource are defined", function () {
      expect(function () {
        ResourceCache.loadSchema({
          schema: schemaJson,
          resource: schemaResource,
        });
      }).toThrowDeveloperError();
    });

    it("loads embedded buffer", function () {
      var expectedCacheKey = ResourceCacheKey.getEmbeddedBufferCacheKey({
        parentResource: bufferParentResource,
        bufferId: 0,
      });
      var bufferLoader = ResourceCache.loadEmbeddedBuffer({
        parentResource: bufferParentResource,
        bufferId: 0,
        typedArray: bufferTypedArray,
      });
      var cacheEntry = ResourceCache.cacheEntries[expectedCacheKey];
      expect(bufferLoader.cacheKey).toBe(expectedCacheKey);
      expect(cacheEntry.referenceCount).toBe(1);

      // The existing resource is returned if the computed cache key is the same
      expect(
        ResourceCache.loadEmbeddedBuffer({
          parentResource: bufferParentResource,
          bufferId: 0,
          typedArray: bufferTypedArray,
        })
      ).toBe(bufferLoader);

      expect(cacheEntry.referenceCount).toBe(2);

      return bufferLoader.promise.then(function (bufferLoader) {
        expect(bufferLoader.typedArray).toBe(bufferTypedArray);
      });
    });

    it("loadEmbeddedBuffer throws if parentResource is undefined", function () {
      expect(function () {
        ResourceCache.loadEmbeddedBuffer({
          bufferId: 0,
          typedArray: bufferTypedArray,
        });
      }).toThrowDeveloperError();
    });

    it("loadEmbeddedBuffer throws if bufferId is undefined", function () {
      expect(function () {
        ResourceCache.loadEmbeddedBuffer({
          parentResource: bufferParentResource,
          typedArray: bufferTypedArray,
        });
      }).toThrowDeveloperError();
    });

    it("loadEmbeddedBuffer throws if typedArray is undefined", function () {
      expect(function () {
        ResourceCache.loadEmbeddedBuffer({
          parentResource: bufferParentResource,
          bufferId: 0,
        });
      }).toThrowDeveloperError();
    });

    it("loads external buffer", function () {
      spyOn(Resource.prototype, "fetchArrayBuffer").and.returnValue(
        when.resolve(bufferArrayBuffer)
      );

      var expectedCacheKey = ResourceCacheKey.getExternalBufferCacheKey({
        resource: bufferResource,
      });
      var bufferLoader = ResourceCache.loadExternalBuffer({
        resource: bufferResource,
      });
      var cacheEntry = ResourceCache.cacheEntries[expectedCacheKey];
      expect(bufferLoader.cacheKey).toBe(expectedCacheKey);
      expect(cacheEntry.referenceCount).toBe(1);

      // The existing resource is returned if the computed cache key is the same
      expect(
        ResourceCache.loadExternalBuffer({
          resource: bufferResource,
        })
      ).toBe(bufferLoader);

      expect(cacheEntry.referenceCount).toBe(2);

      return bufferLoader.promise.then(function (bufferLoader) {
        expect(bufferLoader.typedArray.buffer).toBe(bufferArrayBuffer);
      });
    });

    it("loadExternalBuffer throws if resource is undefined", function () {
      expect(function () {
        ResourceCache.loadExternalBuffer({
          resource: undefined,
        });
      }).toThrowDeveloperError();
    });

    it("loads glTF", function () {
      var gltf = {
        asset: {
          version: "2.0",
        },
      };
      var arrayBuffer = generateJsonBuffer(gltf).buffer;

      spyOn(GltfJsonLoader.prototype, "_fetchGltf").and.returnValue(
        when.resolve(arrayBuffer)
      );

      var expectedCacheKey = ResourceCacheKey.getGltfCacheKey({
        gltfResource: gltfResource,
      });
      var gltfJsonLoader = ResourceCache.loadGltfJson({
        gltfResource: gltfResource,
        baseResource: gltfResource,
      });
      var cacheEntry = ResourceCache.cacheEntries[expectedCacheKey];
      expect(gltfJsonLoader.cacheKey).toBe(expectedCacheKey);
      expect(cacheEntry.referenceCount).toBe(1);

      // The existing resource is returned if the computed cache key is the same
      expect(
        ResourceCache.loadGltfJson({
          gltfResource: gltfResource,
          baseResource: gltfResource,
        })
      ).toBe(gltfJsonLoader);

      expect(cacheEntry.referenceCount).toBe(2);

      return gltfJsonLoader.promise.then(function (gltfJsonLoader) {
        expect(gltfJsonLoader.gltf).toBeDefined();
      });
    });

    it("loadGltfJson throws if gltfResource is undefined", function () {
      expect(function () {
        ResourceCache.loadGltfJson({
          gltfResource: undefined,
          baseResource: gltfResource,
        });
      }).toThrowDeveloperError();
    });

    it("loadGltfJson throws if gltfResource is undefined", function () {
      expect(function () {
        ResourceCache.loadGltfJson({
          gltfResource: gltfResource,
          baseResource: undefined,
        });
      }).toThrowDeveloperError();
    });

    it("loads buffer view", function () {
      spyOn(Resource.prototype, "fetchArrayBuffer").and.returnValue(
        when.resolve(bufferArrayBuffer)
      );

      var expectedCacheKey = ResourceCacheKey.getBufferViewCacheKey({
        gltf: gltfUncompressed,
        bufferViewId: 0,
        gltfResource: gltfResource,
        baseResource: gltfResource,
      });
      var bufferViewLoader = ResourceCache.loadBufferView({
        gltf: gltfUncompressed,
        bufferViewId: 0,
        gltfResource: gltfResource,
        baseResource: gltfResource,
      });
      var cacheEntry = ResourceCache.cacheEntries[expectedCacheKey];
      expect(bufferViewLoader.cacheKey).toBe(expectedCacheKey);
      expect(cacheEntry.referenceCount).toBe(1);

      // The existing resource is returned if the computed cache key is the same
      expect(
        ResourceCache.loadBufferView({
          gltf: gltfUncompressed,
          bufferViewId: 0,
          gltfResource: gltfResource,
          baseResource: gltfResource,
        })
      ).toBe(bufferViewLoader);

      expect(cacheEntry.referenceCount).toBe(2);

      return bufferViewLoader.promise.then(function (bufferViewLoader) {
        expect(bufferViewLoader.typedArray).toBeDefined();
      });
    });

    it("loadBufferView throws if gltf is undefined", function () {
      expect(function () {
        ResourceCache.loadBufferView({
          gltf: undefined,
          bufferViewId: 0,
          gltfResource: gltfResource,
          baseResource: gltfResource,
        });
      }).toThrowDeveloperError();
    });

    it("loadBufferView throws if bufferViewId is undefined", function () {
      expect(function () {
        ResourceCache.loadBufferView({
          gltf: gltfUncompressed,
          bufferViewId: undefined,
          gltfResource: gltfResource,
          baseResource: gltfResource,
        });
      }).toThrowDeveloperError();
    });

    it("loadBufferView throws if gltfResource is undefined", function () {
      expect(function () {
        ResourceCache.loadBufferView({
          gltf: gltfUncompressed,
          bufferViewId: 0,
          gltfResource: undefined,
          baseResource: gltfResource,
        });
      }).toThrowDeveloperError();
    });

    it("loadBufferView throws if baseResource is undefined", function () {
      expect(function () {
        ResourceCache.loadBufferView({
          gltf: gltfUncompressed,
          bufferViewId: 0,
          gltfResource: gltfResource,
          baseResource: undefined,
        });
      }).toThrowDeveloperError();
    });

    it("loads draco", function () {
      spyOn(Resource.prototype, "fetchArrayBuffer").and.returnValue(
        when.resolve(dracoArrayBuffer)
      );

      spyOn(DracoLoader, "decodeBufferView").and.returnValue(
        when.resolve(decodeDracoResults)
      );

      var expectedCacheKey = ResourceCacheKey.getDracoCacheKey({
        gltf: gltfDraco,
        draco: dracoExtension,
        gltfResource: gltfResource,
        baseResource: gltfResource,
      });
      var dracoLoader = ResourceCache.loadDraco({
        gltf: gltfDraco,
        draco: dracoExtension,
        gltfResource: gltfResource,
        baseResource: gltfResource,
      });

      var cacheEntry = ResourceCache.cacheEntries[expectedCacheKey];
      expect(dracoLoader.cacheKey).toBe(expectedCacheKey);
      expect(cacheEntry.referenceCount).toBe(1);

      // The existing resource is returned if the computed cache key is the same
      expect(
        ResourceCache.loadDraco({
          gltf: gltfDraco,
          draco: dracoExtension,
          gltfResource: gltfResource,
          baseResource: gltfResource,
        })
      ).toBe(dracoLoader);

      expect(cacheEntry.referenceCount).toBe(2);

      return waitForLoaderProcess(dracoLoader, scene).then(function (
        dracoLoader
      ) {
        expect(dracoLoader.decodedData).toBeDefined();
      });
    });

    it("loadDraco throws if gltf is undefined", function () {
      expect(function () {
        ResourceCache.loadBufferView({
          gltf: undefined,
          draco: dracoExtension,
          gltfResource: gltfResource,
          baseResource: gltfResource,
        });
      }).toThrowDeveloperError();
    });

    it("loadDraco throws if draco is undefined", function () {
      expect(function () {
        ResourceCache.loadBufferView({
          gltf: gltfDraco,
          draco: undefined,
          gltfResource: gltfResource,
          baseResource: gltfResource,
        });
      }).toThrowDeveloperError();
    });

    it("loadDraco throws if gltfResource is undefined", function () {
      expect(function () {
        ResourceCache.loadBufferView({
          gltf: gltfDraco,
          draco: dracoExtension,
          gltfResource: undefined,
          baseResource: gltfResource,
        });
      }).toThrowDeveloperError();
    });

    it("loadDraco throws if baseResource is undefined", function () {
      expect(function () {
        ResourceCache.loadBufferView({
          gltf: gltfDraco,
          draco: dracoExtension,
          gltfResource: gltfResource,
          baseResource: undefined,
        });
      }).toThrowDeveloperError();
    });

    it("loads vertex buffer from buffer view", function () {
      spyOn(Resource.prototype, "fetchArrayBuffer").and.returnValue(
        when.resolve(bufferArrayBuffer)
      );

      var expectedCacheKey = ResourceCacheKey.getVertexBufferCacheKey({
        gltf: gltfUncompressed,
        gltfResource: gltfResource,
        baseResource: gltfResource,
        bufferViewId: 0,
      });
      var vertexBufferLoader = ResourceCache.loadVertexBuffer({
        gltf: gltfUncompressed,
        gltfResource: gltfResource,
        baseResource: gltfResource,
        bufferViewId: 0,
        accessorId: 0,
      });

      var cacheEntry = ResourceCache.cacheEntries[expectedCacheKey];
      expect(vertexBufferLoader.cacheKey).toBe(expectedCacheKey);
      expect(cacheEntry.referenceCount).toBe(1);

      // The existing resource is returned if the computed cache key is the same
      expect(
        ResourceCache.loadVertexBuffer({
          gltf: gltfUncompressed,
          gltfResource: gltfResource,
          baseResource: gltfResource,
          bufferViewId: 0,
        })
      ).toBe(vertexBufferLoader);

      expect(cacheEntry.referenceCount).toBe(2);

      return waitForLoaderProcess(vertexBufferLoader, scene).then(function (
        vertexBufferLoader
      ) {
        expect(vertexBufferLoader.vertexBuffer).toBeDefined();
      });
    });

    it("loads vertex buffer from draco", function () {
      spyOn(Resource.prototype, "fetchArrayBuffer").and.returnValue(
        when.resolve(dracoArrayBuffer)
      );

      spyOn(DracoLoader, "decodeBufferView").and.returnValue(
        when.resolve(decodeDracoResults)
      );

      var expectedCacheKey = ResourceCacheKey.getVertexBufferCacheKey({
        gltf: gltfDraco,
        gltfResource: gltfResource,
        baseResource: gltfResource,
        draco: dracoExtension,
        attributeSemantic: "POSITION",
      });
      var vertexBufferLoader = ResourceCache.loadVertexBuffer({
        gltf: gltfDraco,
        gltfResource: gltfResource,
        baseResource: gltfResource,
        draco: dracoExtension,
        attributeSemantic: "POSITION",
        accessorId: 0,
      });

      var cacheEntry = ResourceCache.cacheEntries[expectedCacheKey];
      expect(vertexBufferLoader.cacheKey).toBe(expectedCacheKey);
      expect(cacheEntry.referenceCount).toBe(1);

      // The existing resource is returned if the computed cache key is the same
      expect(
        ResourceCache.loadVertexBuffer({
          gltf: gltfDraco,
          gltfResource: gltfResource,
          baseResource: gltfResource,
          draco: dracoExtension,
          attributeSemantic: "POSITION",
          accessorId: 0,
        })
      ).toBe(vertexBufferLoader);

      expect(cacheEntry.referenceCount).toBe(2);

      return waitForLoaderProcess(vertexBufferLoader, scene).then(function (
        vertexBufferLoader
      ) {
        expect(vertexBufferLoader.vertexBuffer).toBeDefined();
      });
    });

    it("loadVertexBuffer throws if gltf is undefined", function () {
      expect(function () {
        ResourceCache.loadVertexBuffer({
          gltf: undefined,
          gltfResource: gltfResource,
          baseResource: gltfResource,
          bufferViewId: 0,
        });
      }).toThrowDeveloperError();
    });

    it("loadVertexBuffer throws if gltfResource is undefined", function () {
      expect(function () {
        ResourceCache.loadVertexBuffer({
          gltf: gltfUncompressed,
          gltfResource: undefined,
          baseResource: gltfResource,
          bufferViewId: 0,
        });
      }).toThrowDeveloperError();
    });

    it("loadVertexBuffer throws if baseResource is undefined", function () {
      expect(function () {
        ResourceCache.loadVertexBuffer({
          gltf: gltfUncompressed,
          gltfResource: gltfResource,
          baseResource: undefined,
          bufferViewId: 0,
        });
      }).toThrowDeveloperError();
    });

    it("loadVertexBuffer throws if bufferViewId and draco are both defined", function () {
      expect(function () {
        ResourceCache.loadVertexBuffer({
          gltf: gltfDraco,
          gltfResource: gltfResource,
          baseResource: gltfResource,
          bufferViewId: 0,
          draco: dracoExtension,
          attributeSemantic: "POSITION",
          accessorId: 0,
        });
      }).toThrowDeveloperError();
    });

    it("loadVertexBuffer throws if bufferViewId and draco are both undefined", function () {
      expect(function () {
        ResourceCache.loadVertexBuffer({
          gltf: gltfDraco,
          gltfResource: gltfResource,
          baseResource: gltfResource,
        });
      }).toThrowDeveloperError();
    });

    it("loadVertexBuffer throws if draco is defined and attributeSemantic is not defined", function () {
      expect(function () {
        ResourceCache.loadVertexBuffer({
          gltf: gltfDraco,
          gltfResource: gltfResource,
          baseResource: gltfResource,
          draco: dracoExtension,
          attributeSemantic: undefined,
          accessorId: 0,
        });
      }).toThrowDeveloperError();
    });

    it("loadVertexBuffer throws if draco is defined and accessorId is not defined", function () {
      expect(function () {
        ResourceCache.loadVertexBuffer({
          gltf: gltfDraco,
          gltfResource: gltfResource,
          baseResource: gltfResource,
          draco: dracoExtension,
          attributeSemantic: "POSITION",
          accessorId: undefined,
        });
      }).toThrowDeveloperError();
    });

    it("loads index buffer from accessor", function () {
      spyOn(Resource.prototype, "fetchArrayBuffer").and.returnValue(
        when.resolve(bufferArrayBuffer)
      );

      var expectedCacheKey = ResourceCacheKey.getIndexBufferCacheKey({
        gltf: gltfUncompressed,
        accessorId: 2,
        gltfResource: gltfResource,
        baseResource: gltfResource,
      });
      var indexBufferLoader = ResourceCache.loadIndexBuffer({
        gltf: gltfUncompressed,
        accessorId: 2,
        gltfResource: gltfResource,
        baseResource: gltfResource,
      });

      var cacheEntry = ResourceCache.cacheEntries[expectedCacheKey];
      expect(indexBufferLoader.cacheKey).toBe(expectedCacheKey);
      expect(cacheEntry.referenceCount).toBe(1);

      // The existing resource is returned if the computed cache key is the same
      expect(
        ResourceCache.loadIndexBuffer({
          gltf: gltfUncompressed,
          accessorId: 2,
          gltfResource: gltfResource,
          baseResource: gltfResource,
        })
      ).toBe(indexBufferLoader);

      expect(cacheEntry.referenceCount).toBe(2);

      return waitForLoaderProcess(indexBufferLoader, scene).then(function (
        indexBufferLoader
      ) {
        expect(indexBufferLoader.indexBuffer).toBeDefined();
      });
    });

    it("loads index buffer from draco", function () {
      spyOn(Resource.prototype, "fetchArrayBuffer").and.returnValue(
        when.resolve(dracoArrayBuffer)
      );

      spyOn(DracoLoader, "decodeBufferView").and.returnValue(
        when.resolve(decodeDracoResults)
      );

      var expectedCacheKey = ResourceCacheKey.getIndexBufferCacheKey({
        gltf: gltfDraco,
        accessorId: 2,
        gltfResource: gltfResource,
        baseResource: gltfResource,
        draco: dracoExtension,
      });
      var indexBufferLoader = ResourceCache.loadIndexBuffer({
        gltf: gltfDraco,
        accessorId: 2,
        gltfResource: gltfResource,
        baseResource: gltfResource,
        draco: dracoExtension,
      });

      var cacheEntry = ResourceCache.cacheEntries[expectedCacheKey];
      expect(indexBufferLoader.cacheKey).toBe(expectedCacheKey);
      expect(cacheEntry.referenceCount).toBe(1);

      // The existing resource is returned if the computed cache key is the same
      expect(
        ResourceCache.loadIndexBuffer({
          gltf: gltfDraco,
          accessorId: 2,
          gltfResource: gltfResource,
          baseResource: gltfResource,
          draco: dracoExtension,
        })
      ).toBe(indexBufferLoader);

      expect(cacheEntry.referenceCount).toBe(2);

      return waitForLoaderProcess(indexBufferLoader, scene).then(function (
        indexBufferLoader
      ) {
        expect(indexBufferLoader.indexBuffer).toBeDefined();
      });
    });

    it("loadIndexBuffer throws if gltf is undefined", function () {
      expect(function () {
        ResourceCache.loadIndexBuffer({
          gltf: undefined,
          accessorId: 2,
          gltfResource: gltfResource,
          baseResource: gltfResource,
        });
      }).toThrowDeveloperError();
    });

    it("loadIndexBuffer throws if accessorId is undefined", function () {
      expect(function () {
        ResourceCache.loadIndexBuffer({
          gltf: gltfUncompressed,
          accessorId: undefined,
          gltfResource: gltfResource,
          baseResource: gltfResource,
        });
      }).toThrowDeveloperError();
    });

    it("loadIndexBuffer throws if gltfResource is undefined", function () {
      expect(function () {
        ResourceCache.loadIndexBuffer({
          gltf: gltfUncompressed,
          accessorId: 2,
          gltfResource: undefined,
          baseResource: gltfResource,
        });
      }).toThrowDeveloperError();
    });

    it("loadIndexBuffer throws if baseResource is undefined", function () {
      expect(function () {
        ResourceCache.loadIndexBuffer({
          gltf: gltfUncompressed,
          accessorId: 2,
          gltfResource: gltfResource,
          baseResource: undefined,
        });
      }).toThrowDeveloperError();
    });

    it("loads image", function () {
      spyOn(Resource.prototype, "fetchImage").and.returnValue(
        when.resolve(image)
      );

      var expectedCacheKey = ResourceCacheKey.getImageCacheKey({
        gltf: gltfWithTextures,
        imageId: 0,
        gltfResource: gltfResource,
        baseResource: gltfResource,
      });
      var imageLoader = ResourceCache.loadImage({
        gltf: gltfWithTextures,
        imageId: 0,
        gltfResource: gltfResource,
        baseResource: gltfResource,
      });
      var cacheEntry = ResourceCache.cacheEntries[expectedCacheKey];
      expect(imageLoader.cacheKey).toBe(expectedCacheKey);
      expect(cacheEntry.referenceCount).toBe(1);

      // The existing resource is returned if the computed cache key is the same
      expect(
        ResourceCache.loadImage({
          gltf: gltfWithTextures,
          imageId: 0,
          gltfResource: gltfResource,
          baseResource: gltfResource,
        })
      ).toBe(imageLoader);

      expect(cacheEntry.referenceCount).toBe(2);

      return imageLoader.promise.then(function (imageLoader) {
        expect(imageLoader.image).toBeDefined();
      });
    });

    it("loadImage throws if gltf is undefined", function () {
      expect(function () {
        ResourceCache.loadImage({
          gltf: undefined,
          imageId: 0,
          gltfResource: gltfResource,
          baseResource: gltfResource,
        });
      }).toThrowDeveloperError();
    });

    it("loadImage throws if imageId is undefined", function () {
      expect(function () {
        ResourceCache.loadImage({
          gltf: gltfWithTextures,
          imageId: undefined,
          gltfResource: gltfResource,
          baseResource: gltfResource,
        });
      }).toThrowDeveloperError();
    });

    it("loadImage throws if gltfResource is undefined", function () {
      expect(function () {
        ResourceCache.loadImage({
          gltf: gltfWithTextures,
          imageId: 0,
          gltfResource: undefined,
          baseResource: gltfResource,
        });
      }).toThrowDeveloperError();
    });

    it("loadImage throws if baseResource is undefined", function () {
      expect(function () {
        ResourceCache.loadImage({
          gltf: gltfWithTextures,
          imageId: 0,
          gltfResource: gltfResource,
          baseResource: undefined,
        });
      }).toThrowDeveloperError();
    });

    it("loads texture", function () {
      spyOn(Resource.prototype, "fetchImage").and.returnValue(
        when.resolve(image)
      );

      var expectedCacheKey = ResourceCacheKey.getTextureCacheKey({
        gltf: gltfWithTextures,
        textureInfo: gltfWithTextures.materials[0].emissiveTexture,
        gltfResource: gltfResource,
        baseResource: gltfResource,
        supportedImageFormats: new SupportedImageFormats(),
      });
      var textureLoader = ResourceCache.loadTexture({
        gltf: gltfWithTextures,
        textureInfo: gltfWithTextures.materials[0].emissiveTexture,
        gltfResource: gltfResource,
        baseResource: gltfResource,
        supportedImageFormats: new SupportedImageFormats(),
      });
      var cacheEntry = ResourceCache.cacheEntries[expectedCacheKey];
      expect(textureLoader.cacheKey).toBe(expectedCacheKey);
      expect(cacheEntry.referenceCount).toBe(1);

      // The existing resource is returned if the computed cache key is the same
      expect(
        ResourceCache.loadTexture({
          gltf: gltfWithTextures,
          textureInfo: gltfWithTextures.materials[0].emissiveTexture,
          gltfResource: gltfResource,
          baseResource: gltfResource,
          supportedImageFormats: new SupportedImageFormats(),
        })
      ).toBe(textureLoader);

      expect(cacheEntry.referenceCount).toBe(2);

      return waitForLoaderProcess(textureLoader, scene).then(function (
        textureLoader
      ) {
        expect(textureLoader.texture).toBeDefined();
      });
    });

    it("loadTexture throws if gltf is undefined", function () {
      expect(function () {
        ResourceCache.loadTexture({
          gltf: undefined,
          textureInfo: gltfWithTextures.materials[0].emissiveTexture,
          gltfResource: gltfResource,
          baseResource: gltfResource,
          supportedImageFormats: new SupportedImageFormats(),
        });
      }).toThrowDeveloperError();
    });

    it("loadTexture throws if textureInfo is undefined", function () {
      expect(function () {
        ResourceCache.loadTexture({
          gltf: gltfWithTextures,
          textureInfo: undefined,
          gltfResource: gltfResource,
          baseResource: gltfResource,
          supportedImageFormats: new SupportedImageFormats(),
        });
      }).toThrowDeveloperError();
    });

    it("loadTexture throws if gltfResource is undefined", function () {
      expect(function () {
        ResourceCache.loadTexture({
          gltf: gltfWithTextures,
          textureInfo: gltfWithTextures.materials[0].emissiveTexture,
          gltfResource: undefined,
          baseResource: gltfResource,
          supportedImageFormats: new SupportedImageFormats(),
        });
      }).toThrowDeveloperError();
    });

    it("loadTexture throws if baseResource is undefined", function () {
      expect(function () {
        ResourceCache.loadTexture({
          gltf: gltfWithTextures,
          textureInfo: gltfWithTextures.materials[0].emissiveTexture,
          gltfResource: gltfResource,
          baseResource: undefined,
          supportedImageFormats: new SupportedImageFormats(),
        });
      }).toThrowDeveloperError();
    });

    it("loadTexture throws if supportedImageFormats is undefined", function () {
      expect(function () {
        ResourceCache.loadTexture({
          gltf: gltfWithTextures,
          textureInfo: gltfWithTextures.materials[0].emissiveTexture,
          gltfResource: gltfResource,
          baseResource: gltfResource,
          supportedImageFormats: undefined,
        });
      }).toThrowDeveloperError();
    });
  },
  "WebGL"
);