/*global defineSuite*/
defineSuite([
        'Scene/BillboardCollection',
        'Core/BoundingRectangle',
        'Core/BoundingSphere',
        'Core/Cartesian2',
        'Core/Cartesian3',
        'Core/Color',
        'Core/defined',
        'Core/Ellipsoid',
        'Core/loadImage',
        'Core/Math',
        'Core/NearFarScalar',
        'Renderer/ContextLimits',
        'Scene/HeightReference',
        'Scene/HorizontalOrigin',
        'Scene/OrthographicFrustum',
        'Scene/TextureAtlas',
        'Scene/VerticalOrigin',
        'Specs/createScene',
        'Specs/pollToPromise',
        'ThirdParty/when'
    ], function(
        BillboardCollection,
        BoundingRectangle,
        BoundingSphere,
        Cartesian2,
        Cartesian3,
        Color,
        defined,
        Ellipsoid,
        loadImage,
        CesiumMath,
        NearFarScalar,
        ContextLimits,
        HeightReference,
        HorizontalOrigin,
        OrthographicFrustum,
        TextureAtlas,
        VerticalOrigin,
        createScene,
        pollToPromise,
        when) {
    "use strict";

    var scene;
    var context;
    var camera;
    var billboards;
    var billboardsWithHeight;

    var greenImage;
    var blueImage;
    var whiteImage;
    var largeBlueImage;

    beforeAll(function() {
        scene = createScene();
        context = scene.context;
        camera = scene.camera;

        return when.join(
            loadImage('./Data/Images/Green.png').then(function(result) {
                greenImage = result;
            }),
            loadImage('./Data/Images/Blue.png').then(function(result) {
                blueImage = result;
            }),
            loadImage('./Data/Images/White.png').then(function(result) {
                whiteImage = result;
            }),
            loadImage('./Data/Images/Blue10x10.png').then(function(result) {
                largeBlueImage = result;
            }));
    });

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

    beforeEach(function() {
        scene.morphTo3D(0);

        camera.position = new Cartesian3(10.0, 0.0, 0.0);
        camera.direction = Cartesian3.negate(Cartesian3.UNIT_X, new Cartesian3());
        camera.up = Cartesian3.clone(Cartesian3.UNIT_Z);

        billboards = new BillboardCollection();
        scene.primitives.add(billboards);

        billboardsWithHeight = new BillboardCollection({
            scene : scene
        });
        scene.primitives.add(billboardsWithHeight);
    });

    afterEach(function() {
        // billboards are destroyed by removeAll().
        scene.primitives.removeAll();
    });

    it('constructs a default billboard', function() {
        var b = billboards.add();
        expect(b.show).toEqual(true);
        expect(b.position).toEqual(Cartesian3.ZERO);
        expect(b.pixelOffset).toEqual(Cartesian2.ZERO);
        expect(b.eyeOffset).toEqual(Cartesian3.ZERO);
        expect(b.horizontalOrigin).toEqual(HorizontalOrigin.CENTER);
        expect(b.verticalOrigin).toEqual(VerticalOrigin.CENTER);
        expect(b.scale).toEqual(1.0);
        expect(b.image).not.toBeDefined();
        expect(b.color.red).toEqual(1.0);
        expect(b.color.green).toEqual(1.0);
        expect(b.color.blue).toEqual(1.0);
        expect(b.color.alpha).toEqual(1.0);
        expect(b.rotation).toEqual(0.0);
        expect(b.alignedAxis).toEqual(Cartesian3.ZERO);
        expect(b.scaleByDistance).not.toBeDefined();
        expect(b.translucencyByDistance).not.toBeDefined();
        expect(b.pixelOffsetScaleByDistance).not.toBeDefined();
        expect(b.width).not.toBeDefined();
        expect(b.height).not.toBeDefined();
        expect(b.id).not.toBeDefined();
        expect(b.heightReference).toEqual(HeightReference.NONE);
        expect(b.sizeInMeters).toEqual(false);
    });

    it('can add and remove before first update.', function() {
        var b = billboards.add();
        billboards.remove(b);
        expect(scene.renderForSpecs()).toBeDefined();
    });

    it('explicitly constructs a billboard', function() {
        var b = billboards.add({
            show : false,
            position : new Cartesian3(1.0, 2.0, 3.0),
            pixelOffset : new Cartesian2(1.0, 2.0),
            eyeOffset : new Cartesian3(1.0, 2.0, 3.0),
            horizontalOrigin : HorizontalOrigin.LEFT,
            verticalOrigin : VerticalOrigin.BOTTOM,
            scale : 2.0,
            image : greenImage,
            color : {
                red : 1.0,
                green : 2.0,
                blue : 3.0,
                alpha : 4.0
            },
            rotation : 1.0,
            alignedAxis : new Cartesian3(1.0, 2.0, 3.0),
            scaleByDistance : new NearFarScalar(1.0, 3.0, 1.0e6, 0.0),
            translucencyByDistance : new NearFarScalar(1.0, 1.0, 1.0e6, 0.0),
            pixelOffsetScaleByDistance : new NearFarScalar(1.0, 1.0, 1.0e6, 0.0),
            width : 300.0,
            height : 200.0,
            sizeInMeters : true,
            id : 'id'
        });

        expect(b.show).toEqual(false);
        expect(b.position).toEqual(new Cartesian3(1.0, 2.0, 3.0));
        expect(b.pixelOffset).toEqual(new Cartesian2(1.0, 2.0));
        expect(b.eyeOffset).toEqual(new Cartesian3(1.0, 2.0, 3.0));
        expect(b.horizontalOrigin).toEqual(HorizontalOrigin.LEFT);
        expect(b.verticalOrigin).toEqual(VerticalOrigin.BOTTOM);
        expect(b.scale).toEqual(2.0);
        expect(b.image).toEqual(greenImage.src);
        expect(b.color.red).toEqual(1.0);
        expect(b.color.green).toEqual(2.0);
        expect(b.color.blue).toEqual(3.0);
        expect(b.color.alpha).toEqual(4.0);
        expect(b.rotation).toEqual(1.0);
        expect(b.alignedAxis).toEqual(new Cartesian3(1.0, 2.0, 3.0));
        expect(b.scaleByDistance).toEqual(new NearFarScalar(1.0, 3.0, 1.0e6, 0.0));
        expect(b.translucencyByDistance).toEqual(new NearFarScalar(1.0, 1.0, 1.0e6, 0.0));
        expect(b.pixelOffsetScaleByDistance).toEqual(new NearFarScalar(1.0, 1.0, 1.0e6, 0.0));
        expect(b.width).toEqual(300.0);
        expect(b.height).toEqual(200.0);
        expect(b.sizeInMeters).toEqual(true);
        expect(b.id).toEqual('id');
    });

    it('sets billboard properties', function() {
        var b = billboards.add();
        b.show = false;
        b.position = new Cartesian3(1.0, 2.0, 3.0);
        b.pixelOffset = new Cartesian2(1.0, 2.0);
        b.eyeOffset = new Cartesian3(1.0, 2.0, 3.0);
        b.horizontalOrigin = HorizontalOrigin.LEFT;
        b.verticalOrigin = VerticalOrigin.BOTTOM;
        b.scale = 2.0;
        b.image = greenImage;
        b.color = new Color(1.0, 2.0, 3.0, 4.0);
        b.rotation = 1.0;
        b.alignedAxis = new Cartesian3(1.0, 2.0, 3.0);
        b.width = 300.0;
        b.height = 200.0;
        b.scaleByDistance = new NearFarScalar(1.0e6, 3.0, 1.0e8, 0.0);
        b.translucencyByDistance = new NearFarScalar(1.0e6, 1.0, 1.0e8, 0.0);
        b.pixelOffsetScaleByDistance = new NearFarScalar(1.0e6, 3.0, 1.0e8, 0.0);
        b.sizeInMeters = true;

        expect(b.show).toEqual(false);
        expect(b.position).toEqual(new Cartesian3(1.0, 2.0, 3.0));
        expect(b.pixelOffset).toEqual(new Cartesian2(1.0, 2.0));
        expect(b.eyeOffset).toEqual(new Cartesian3(1.0, 2.0, 3.0));
        expect(b.horizontalOrigin).toEqual(HorizontalOrigin.LEFT);
        expect(b.verticalOrigin).toEqual(VerticalOrigin.BOTTOM);
        expect(b.scale).toEqual(2.0);
        expect(b.image).toEqual(greenImage.src);
        expect(b.color.red).toEqual(1.0);
        expect(b.color.green).toEqual(2.0);
        expect(b.color.blue).toEqual(3.0);
        expect(b.color.alpha).toEqual(4.0);
        expect(b.rotation).toEqual(1.0);
        expect(b.alignedAxis).toEqual(new Cartesian3(1.0, 2.0, 3.0));
        expect(b.scaleByDistance).toEqual(new NearFarScalar(1.0e6, 3.0, 1.0e8, 0.0));
        expect(b.translucencyByDistance).toEqual(new NearFarScalar(1.0e6, 1.0, 1.0e8, 0.0));
        expect(b.pixelOffsetScaleByDistance).toEqual(new NearFarScalar(1.0e6, 3.0, 1.0e8, 0.0));
        expect(b.width).toEqual(300.0);
        expect(b.height).toEqual(200.0);
        expect(b.sizeInMeters).toEqual(true);
    });

    it('is not destroyed', function() {
        expect(billboards.isDestroyed()).toEqual(false);
    });

    it('renders billboard with sizeInMeters', function() {
        billboards.add({
            position : Cartesian3.ZERO,
            image : greenImage,
            width : 2.0,
            height : 2.0,
            sizeInMeters : true
        });

        camera.position = new Cartesian3(2.0, 0.0, 0.0);
        expect(scene.renderForSpecs()).toEqual([0, 255, 0, 255]);

        camera.position = new Cartesian3(1e6, 0.0, 0.0);
        expect(scene.renderForSpecs()).toEqual([0, 0, 0, 255]);
    });

    it('disables billboard scaleByDistance', function() {
        var b = billboards.add({
            scaleByDistance : new NearFarScalar(1.0, 3.0, 1.0e6, 0.0)
        });
        b.scaleByDistance = undefined;
        expect(b.scaleByDistance).not.toBeDefined();
    });

    it('disables billboard translucencyByDistance', function() {
        var b = billboards.add({
            translucencyByDistance : new NearFarScalar(1.0, 1.0, 1.0e6, 0.0)
        });
        b.translucencyByDistance = undefined;
        expect(b.translucencyByDistance).not.toBeDefined();
    });

    it('disables billboard pixelOffsetScaleByDistance', function() {
        var b = billboards.add({
            pixelOffsetScaleByDistance : new NearFarScalar(1.0, 1.0, 1.0e6, 0.0)
        });
        b.pixelOffsetScaleByDistance = undefined;
        expect(b.pixelOffsetScaleByDistance).not.toBeDefined();
    });

    it('renders billboard with scaleByDistance', function() {
        billboards.add({
            position : Cartesian3.ZERO,
            scaleByDistance: new NearFarScalar(2.0, 1.0, 4.0, 0.0),
            image : greenImage
        });

        camera.position = new Cartesian3(2.0, 0.0, 0.0);
        expect(scene.renderForSpecs()).toEqual([0, 255, 0, 255]);

        camera.position = new Cartesian3(4.0, 0.0, 0.0);
        expect(scene.renderForSpecs()).toEqual([0, 0, 0, 255]);
    });

    it('renders billboard with translucencyByDistance', function() {
        billboards.add({
            position : Cartesian3.ZERO,
            translucencyByDistance: new NearFarScalar(2.0, 1.0, 4.0, 0.0),
            image : greenImage
        });

        camera.position = new Cartesian3(2.0, 0.0, 0.0);
        expect(scene.renderForSpecs()).toEqual([0, 255, 0, 255]);

        camera.position = new Cartesian3(4.0, 0.0, 0.0);
        expect(scene.renderForSpecs()).toEqual([0, 0, 0, 255]);
    });

    it('renders billboard with pixelOffsetScaleByDistance', function() {
        billboards.add({
            position : Cartesian3.ZERO,
            pixelOffset : new Cartesian2(1.0, 0.0),
            pixelOffsetScaleByDistance: new NearFarScalar(2.0, 0.0, 4.0, 1000.0),
            image : greenImage
        });

        camera.position = new Cartesian3(2.0, 0.0, 0.0);
        expect(scene.renderForSpecs()).toEqual([0, 255, 0, 255]);

        camera.position = new Cartesian3(4.0, 0.0, 0.0);
        expect(scene.renderForSpecs()).toEqual([0, 0, 0, 255]);
    });

    it('throws scaleByDistance with nearDistance === farDistance', function() {
        var b = billboards.add();
        var scale = new NearFarScalar(2.0e5, 1.0, 2.0e5, 0.0);
        expect(function() {
            b.scaleByDistance = scale;
        }).toThrowDeveloperError();
    });

    it('throws new billboard with invalid scaleByDistance (nearDistance === farDistance)', function() {
        var scale = new NearFarScalar(2.0e5, 1.0, 2.0e5, 0.0);
        expect(function() {
            billboards.add({
                scaleByDistance : scale
            });
        }).toThrowDeveloperError();
    });

    it('throws scaleByDistance with nearDistance > farDistance', function() {
        var b = billboards.add();
        var scale = new NearFarScalar(1.0e9, 1.0, 1.0e5, 1.0);
        expect(function() {
            b.scaleByDistance = scale;
        }).toThrowDeveloperError();
    });

    it('throws pixelOffsetScaleByDistance with nearDistance === farDistance', function() {
        var b = billboards.add();
        var scale = new NearFarScalar(2.0e5, 1.0, 2.0e5, 0.0);
        expect(function() {
            b.pixelOffsetScaleByDistance = scale;
        }).toThrowDeveloperError();
    });

    it('throws new billboard with invalid pixelOffsetScaleByDistance (nearDistance === farDistance)', function() {
        var scale = new NearFarScalar(2.0e5, 1.0, 2.0e5, 0.0);
        expect(function() {
            billboards.add({
                pixelOffsetScaleByDistance : scale
            });
        }).toThrowDeveloperError();
    });

    it('throws pixelOffsetScaleByDistance with nearDistance > farDistance', function() {
        var b = billboards.add();
        var scale = new NearFarScalar(1.0e9, 1.0, 1.0e5, 1.0);
        expect(function() {
            b.pixelOffsetScaleByDistance = scale;
        }).toThrowDeveloperError();
    });

    it('throws translucencyByDistance with nearDistance === farDistance', function() {
        var b = billboards.add();
        var translucency = new NearFarScalar(2.0e5, 1.0, 2.0e5, 0.0);
        expect(function() {
            b.translucencyByDistance = translucency;
        }).toThrowDeveloperError();
    });

    it('throws new billboard with invalid translucencyByDistance (nearDistance === farDistance)', function() {
        var translucency = new NearFarScalar(2.0e5, 1.0, 2.0e5, 0.0);
        expect(function() {
            billboards.add({
                translucencyByDistance : translucency
            });
        }).toThrowDeveloperError();
    });

    it('throws translucencyByDistance with nearDistance > farDistance', function() {
        var b = billboards.add();
        var translucency = new NearFarScalar(1.0e9, 1.0, 1.0e5, 1.0);
        expect(function() {
            b.translucencyByDistance = translucency;
        }).toThrowDeveloperError();
    });

    it('sets a removed billboard property', function() {
        var b = billboards.add();
        billboards.remove(b);
        b.show = false;
        expect(b.show).toEqual(false);
    });

    it('has zero billboards when constructed', function() {
        expect(billboards.length).toEqual(0);
    });

    it('adds a billboard', function() {
        var b = billboards.add({
            position : new Cartesian3(1.0, 2.0, 3.0)
        });

        expect(billboards.length).toEqual(1);
        expect(billboards.get(0)).toEqual(b);
    });

    it('removes the first billboard', function() {
        var one = billboards.add({
            position : new Cartesian3(1.0, 2.0, 3.0)
        });
        var two = billboards.add({
            position : new Cartesian3(4.0, 5.0, 6.0)
        });

        expect(billboards.length).toEqual(2);

        expect(billboards.remove(one)).toEqual(true);

        expect(billboards.length).toEqual(1);
        expect(billboards.get(0)).toEqual(two);
    });

    it('removes the last billboard', function() {
        var one = billboards.add({
            position : new Cartesian3(1.0, 2.0, 3.0)
        });
        var two = billboards.add({
            position : new Cartesian3(4.0, 5.0, 6.0)
        });

        expect(billboards.length).toEqual(2);

        expect(billboards.remove(two)).toEqual(true);

        expect(billboards.length).toEqual(1);
        expect(billboards.get(0)).toEqual(one);
    });

    it('removes the same billboard twice', function() {
        var b = billboards.add({
            position : new Cartesian3(1.0, 2.0, 3.0)
        });
        expect(billboards.length).toEqual(1);

        expect(billboards.remove(b)).toEqual(true);
        expect(billboards.length).toEqual(0);

        expect(billboards.remove(b)).toEqual(false);
        expect(billboards.length).toEqual(0);
    });

    it('returns false when removing undefined', function() {
        billboards.add({
            position : new Cartesian3(1.0, 2.0, 3.0)
        });
        expect(billboards.length).toEqual(1);

        expect(billboards.remove(undefined)).toEqual(false);
        expect(billboards.length).toEqual(1);
    });

    it('adds and removes billboards', function() {
        var one = billboards.add({
            position : new Cartesian3(1.0, 2.0, 3.0)
        });
        var two = billboards.add({
            position : new Cartesian3(4.0, 5.0, 6.0)
        });
        expect(billboards.length).toEqual(2);
        expect(billboards.get(0)).toEqual(one);
        expect(billboards.get(1)).toEqual(two);

        expect(billboards.remove(two)).toEqual(true);
        var three = billboards.add({
            position : new Cartesian3(7.0, 8.0, 9.0)
        });
        expect(billboards.length).toEqual(2);
        expect(billboards.get(0)).toEqual(one);
        expect(billboards.get(1)).toEqual(three);
    });

    it('removes all billboards', function() {
        billboards.add({
            position : new Cartesian3(1.0, 2.0, 3.0)
        });
        billboards.add({
            position : new Cartesian3(4.0, 5.0, 6.0)
        });
        expect(billboards.length).toEqual(2);

        billboards.removeAll();
        expect(billboards.length).toEqual(0);
    });

    it('can check if it contains a billboard', function() {
        var billboard = billboards.add();

        expect(billboards.contains(billboard)).toEqual(true);
    });

    it('returns false when checking if it contains a billboard it does not contain', function() {
        var billboard = billboards.add();
        billboards.remove(billboard);

        expect(billboards.contains(billboard)).toEqual(false);
    });

    it('does not contain undefined', function() {
        expect(billboards.contains(undefined)).toEqual(false);
    });

    it('does not contain random other objects', function() {
        expect(billboards.contains({})).toEqual(false);
        expect(billboards.contains(new Cartesian2())).toEqual(false);
    });

    it('sets and gets a texture atlas', function() {
        expect(billboards.textureAtlas).not.toBeDefined();

        var atlas = new TextureAtlas({ context : scene.context });
        billboards.textureAtlas = atlas;
        expect(billboards.textureAtlas).toEqual(atlas);
    });

    it('destroys a texture atlas', function() {
        var b = new BillboardCollection();
        expect(b.destroyTextureAtlas).toEqual(true);

        var atlas = new TextureAtlas({ context : scene.context });
        b.textureAtlas = atlas;
        b = b.destroy();

        expect(atlas.isDestroyed()).toEqual(true);
    });

    it('does not destroy a texture atlas', function() {
        var b = new BillboardCollection();
        b.destroyTextureAtlas = false;

        var atlas = new TextureAtlas({ context : scene.context });
        b.rextureAtlas = atlas;
        b = b.destroy();

        expect(atlas.isDestroyed()).toEqual(false);
    });

    it('does not render when constructed', function() {
        expect(scene.renderForSpecs()).toEqual([0, 0, 0, 255]);
    });

    it('modifies and removes a billboard, then renders', function() {
        var b1 = billboards.add({
            position : Cartesian3.ZERO,
            image : greenImage
        });
        billboards.add({
            position : new Cartesian3(-1.0, 0.0, 0.0),
            image : blueImage
        });

        expect(scene.renderForSpecs()).toEqual([0, 255, 0, 255]);

        b1.scale = 2.0;
        billboards.remove(b1);
        expect(scene.renderForSpecs()).toEqual([0, 0, 255, 255]);
    });

    it('renders a green billboard', function() {
        billboards.add({
            position : Cartesian3.ZERO,
            image : greenImage
        });

        expect(scene.renderForSpecs()).toEqual([0, 255, 0, 255]);
    });

    it('adds and renders a billboard', function() {
        billboards.add({
            position : Cartesian3.ZERO,
            image : greenImage
        });

        expect(scene.renderForSpecs()).toEqual([0, 255, 0, 255]);

        billboards.add({
            position : new Cartesian3(1.0, 0.0, 0.0), // Closer to camera
            image : blueImage
        });

        expect(scene.renderForSpecs()).toEqual([0, 0, 255, 255]);
    });

    it('removes and renders a billboard', function() {
        billboards.add({
            position : Cartesian3.ZERO,
            image : greenImage
        });
        var blueBillboard = billboards.add({
            position : new Cartesian3(1.0, 0.0, 0.0), // Closer to camera
            image : blueImage
        });

        expect(scene.renderForSpecs()).toEqual([0, 0, 255, 255]);

        billboards.remove(blueBillboard);
        expect(scene.renderForSpecs()).toEqual([0, 255, 0, 255]);
    });

    it('removes all billboards and renders', function() {
        billboards.add({
            position : Cartesian3.ZERO,
            image : greenImage
        });

        expect(scene.renderForSpecs()).toEqual([0, 255, 0, 255]);

        billboards.removeAll();
        expect(scene.renderForSpecs()).toEqual([0, 0, 0, 255]);
    });

    it('removes all billboards, adds a billboard, and renders', function() {
        billboards.add({
            position : Cartesian3.ZERO,
            image : greenImage
        });

        expect(scene.renderForSpecs()).toEqual([0, 255, 0, 255]);

        billboards.removeAll();
        billboards.add({
            position : Cartesian3.ZERO,
            image : blueImage
        });

        expect(scene.renderForSpecs()).toEqual([0, 0, 255, 255]);
    });

    it('renders with a different texture atlas', function() {
        var b = billboards.add({
            position : Cartesian3.ZERO,
            image : greenImage
        });

        expect(scene.renderForSpecs()).toEqual([0, 255, 0, 255]);

        billboards.textureAtlas = new TextureAtlas({ context : scene.context });
        b.image = blueImage;
        expect(scene.renderForSpecs()).toEqual([0, 0, 255, 255]);
    });

    it('renders using billboard show property', function() {
        var greenBillboard = billboards.add({
            position : Cartesian3.ZERO,
            image : greenImage
        });
        var blueBillboard = billboards.add({
            show : false,
            position : Cartesian3.ZERO,
            image : blueImage
        });

        expect(scene.renderForSpecs()).toEqual([0, 255, 0, 255]);

        greenBillboard.show = false;
        blueBillboard.show = true;

        expect(scene.renderForSpecs()).toEqual([0, 0, 255, 255]);
    });

    it('renders using billboard position property', function() {
        var b = billboards.add({
            position : Cartesian3.ZERO,
            image : greenImage
        });

        expect(scene.renderForSpecs()).toEqual([0, 255, 0, 255]);

        b.position = new Cartesian3(20.0, 0.0, 0.0); // Behind camera
        expect(scene.renderForSpecs()).toEqual([0, 0, 0, 255]);

        b.position = new Cartesian3(1.0, 0.0, 0.0);  // Back in front of camera
        expect(scene.renderForSpecs()).toEqual([0, 255, 0, 255]);
    });

    it('renders using billboard scale property', function() {
        var b = billboards.add({
            position : Cartesian3.ZERO,
            image : greenImage
        });

        expect(scene.renderForSpecs()).toEqual([0, 255, 0, 255]);

        b.scale = 0.0;
        expect(scene.renderForSpecs()).toEqual([0, 0, 0, 255]);

        b.scale = 2.0;
        expect(scene.renderForSpecs()).toEqual([0, 255, 0, 255]);
    });

    it('renders using billboard image property', function() {
        var b = billboards.add({
            position : Cartesian3.ZERO,
            image : greenImage
        });

        expect(scene.renderForSpecs()).toEqual([0, 255, 0, 255]);

        b.image = blueImage;
        expect(scene.renderForSpecs()).toEqual([0, 0, 255, 255]);
    });

    it('renders using billboard setImage function', function() {
        var b = billboards.add({
            position : Cartesian3.ZERO,
            image : greenImage
        });

        expect(scene.renderForSpecs()).toEqual([0, 255, 0, 255]);

        b.setImage(blueImage.src, blueImage);
        expect(scene.renderForSpecs()).toEqual([0, 0, 255, 255]);
    });

    it('renders using billboard setImageSubRegion function', function() {
        var b = billboards.add({
            position : Cartesian3.ZERO,
            image : greenImage
        });

        expect(scene.renderForSpecs()).toEqual([0, 255, 0, 255]);

        billboards.textureAtlas.addImage(largeBlueImage.src, largeBlueImage);
        b.setImageSubRegion(largeBlueImage.src, new BoundingRectangle(5.0, 5.0, 1.0, 1.0));
        expect(scene.renderForSpecs()).toEqual([0, 0, 255, 255]);
    });

    it('renders using billboard color property', function() {
        var b = billboards.add({
            position : Cartesian3.ZERO,
            image : whiteImage
        });

        expect(scene.renderForSpecs()).toEqual([255, 255, 255, 255]);

        b.color = new Color(1.0, 0.0, 1.0, 1.0);
        expect(scene.renderForSpecs()).toEqual([255, 0, 255, 255]);

        // Update a second time since it goes through a different vertex array update path
        b.color = new Color(0.0, 1.0, 0.0, 1.0);
        expect(scene.renderForSpecs()).toEqual([0, 255, 0, 255]);
    });

    it('renders using billboard rotation property', function() {
        var b = billboards.add({
            position : Cartesian3.ZERO,
            image : greenImage
        });

        b.rotation = CesiumMath.PI_OVER_TWO;
        expect(scene.renderForSpecs()).toEqual([0, 255, 0, 255]);
    });

    it('renders using billboard aligned axis property', function() {
        var b = billboards.add({
            position : Cartesian3.ZERO,
            image : greenImage
        });

        b.alignedAxis = Cartesian3.UNIT_X;
        expect(scene.renderForSpecs()).toEqual([0, 255, 0, 255]);
    });

    it('renders using billboard custom width property', function() {
        var b = billboards.add({
            position : Cartesian3.ZERO,
            image : greenImage
        });

        b.width = 300.0;
        expect(scene.renderForSpecs()).toEqual([0, 255, 0, 255]);
    });

    it('renders using billboard custom height property', function() {
        var b = billboards.add({
            position : Cartesian3.ZERO,
            image : greenImage
        });

        b.height = 300.0;
        expect(scene.renderForSpecs()).toEqual([0, 255, 0, 255]);
    });

    it('renders bounding volume with debugShowBoundingVolume', function() {
        billboards.add({
            position : Cartesian3.ZERO,
            image : greenImage,
            scale : 0.5 // bring bounding volume in view
        });
        billboards.debugShowBoundingVolume = true;

        expect(scene.renderForSpecs()).not.toEqual([0, 0, 0, 255]);
    });

    it('renders billboards when instancing is disabled', function() {
        // disable extension
        var instancedArrays = context._instancedArrays;
        context._instancedArrays = undefined;

        expect(scene.renderForSpecs()).toEqual([0, 0, 0, 255]);

        var b1 = billboards.add({
            position : Cartesian3.ZERO,
            image : greenImage
        });
        expect(scene.renderForSpecs()).toEqual([0, 255, 0, 255]);

        var b2 = billboards.add({
            position : new Cartesian3(1.0, 0.0, 0.0), // Closer to camera
            image : blueImage
        });
        expect(scene.renderForSpecs()).toEqual([0, 0, 255, 255]);

        billboards.remove(b2);
        expect(scene.renderForSpecs()).toEqual([0, 255, 0, 255]);

        billboards.remove(b1);
        expect(scene.renderForSpecs()).toEqual([0, 0, 0, 255]);

        context._instancedArrays = instancedArrays;
    });

    it('updates 10% of billboards', function() {
        for ( var i = 0; i < 10; ++i) {
            billboards.add({
                position : Cartesian3.ZERO,
                image : whiteImage,
                show : (i === 3)
            });
        }

        // First render - default billboard color is white.
        expect(scene.renderForSpecs()).toEqual([255, 255, 255, 255]);

        billboards.get(3).color = new Color(0.0, 1.0, 0.0, 1.0);

        // Second render - billboard is green
        expect(scene.renderForSpecs()).toEqual([0, 255, 0, 255]);

        billboards.get(3).color = new Color(1.0, 0.0, 0.0, 1.0);

        // Third render - update goes through a different vertex array update path
        expect(scene.renderForSpecs()).toEqual([255, 0, 0, 255]);
    });

    it('renders more than 16K billboards', function() {
        for ( var i = 0; i < 16 * 1024; ++i) {
            billboards.add({
                position : Cartesian3.ZERO,
                image : whiteImage,
                color : {
                    alpha : 0.0
                }
            });
        }

        billboards.add({
            position : Cartesian3.ZERO,
            image : whiteImage
        });

        expect(scene.renderForSpecs()).toEqual([255, 255, 255, 255]);
    });

    it('is picked', function() {
        var b = billboards.add({
            position : Cartesian3.ZERO,
            image : whiteImage,
            id : 'id'
        });

        var pick = scene.pick(new Cartesian2(0, 0));
        expect(pick.primitive).toEqual(b);
        expect(pick.id).toEqual('id');
    });

    it('can change pick id', function() {
        var b = billboards.add({
            position : Cartesian3.ZERO,
            image : whiteImage,
            id : 'id'
        });

        var pick = scene.pick(new Cartesian2(0, 0));
        expect(pick.primitive).toEqual(b);
        expect(pick.id).toEqual('id');

        b.id = 'id2';

        pick = scene.pick(new Cartesian2(0, 0));
        expect(pick.primitive).toEqual(b);
        expect(pick.id).toEqual('id2');
    });

    it('is not picked', function() {
        billboards.add({
            show : false,
            position : Cartesian3.ZERO,
            image : whiteImage
        });

        var pick = scene.pick(new Cartesian2(0, 0));
        expect(pick).not.toBeDefined();
    });

    it('picks a billboard using scaleByDistance', function() {
        var b = billboards.add({
            position : Cartesian3.ZERO,
            image : whiteImage
        });

        var scaleByDistance = new NearFarScalar(1.0, 4.0, 3.0e9, 2.0);
        b.scaleByDistance = scaleByDistance;

        var pick = scene.pick(new Cartesian2(0, 0));
        expect(pick.primitive).toEqual(b);

        scaleByDistance.nearValue = 0.0;
        scaleByDistance.farValue = 0.0;
        b.scaleByDistance = scaleByDistance;

        pick = scene.pick(new Cartesian2(0, 0));
        expect(pick).not.toBeDefined();
    });

    it('picks a billboard using translucencyByDistance', function() {
        var b = billboards.add({
            position : Cartesian3.ZERO,
            image : whiteImage
        });

        var translucency = new NearFarScalar(1.0, 0.9, 3.0e9, 0.8);
        b.translucencyByDistance = translucency;

        var pick = scene.pick(new Cartesian2(0, 0));
        expect(pick.primitive).toEqual(b);

        translucency.nearValue = 0.0;
        translucency.farValue = 0.0;
        b.translucencyByDistance = translucency;

        pick = scene.pick(new Cartesian2(0, 0));
        expect(pick).not.toBeDefined();
    });

    it('picks a billboard using pixelOffsetScaleByDistance', function() {
        var b = billboards.add({
            position : Cartesian3.ZERO,
            pixelOffset : new Cartesian2(0.0, 100.0),
            image : whiteImage
        });

        var pixelOffsetScale = new NearFarScalar(1.0, 0.0, 3.0e9, 0.0);
        b.pixelOffsetScaleByDistance = pixelOffsetScale;

        var pick = scene.pick(new Cartesian2(0, 0));
        expect(pick.primitive).toEqual(b);

        pixelOffsetScale.nearValue = 10.0;
        pixelOffsetScale.farValue = 10.0;
        b.pixelOffsetScaleByDistance = pixelOffsetScale;

        pick = scene.pick(new Cartesian2(0, 0));
        expect(pick).not.toBeDefined();
    });

    it('can pick a billboard using the rotation property', function() {
        var b = billboards.add({
            position : Cartesian3.ZERO,
            image : greenImage
        });

        b.rotation = CesiumMath.PI_OVER_TWO;
        var pick = scene.pick(new Cartesian2(0, 0));
        expect(pick.primitive).toEqual(b);
    });

    it('can pick a billboard using the aligned axis property', function() {
        var b = billboards.add({
            position : Cartesian3.ZERO,
            image : greenImage
        });

        b.alignedAxis = Cartesian3.UNIT_X;
        var pick = scene.pick(new Cartesian2(0, 0));
        expect(pick.primitive).toEqual(b);
    });

    it('computes screen space position', function() {
        var b = billboards.add({
            position : Cartesian3.ZERO
        });
        scene.renderForSpecs();
        expect(b.computeScreenSpacePosition(scene)).toEqualEpsilon(new Cartesian2(0.5, 0.5), CesiumMath.EPSILON1);
    });

    it('stores screen space position in a result', function() {
        var b = billboards.add({
            position : Cartesian3.ZERO
        });
        var result = new Cartesian2();
        scene.renderForSpecs();
        var actual = b.computeScreenSpacePosition(scene, result);
        expect(actual).toEqual(result);
        expect(result).toEqualEpsilon(new Cartesian2(0.5, 0.5), CesiumMath.EPSILON1);
    });

    it('computes screen space position with pixelOffset', function() {
        var b = billboards.add({
            position : Cartesian3.ZERO,
            pixelOffset : new Cartesian2(1.0, 2.0)
        });
        scene.renderForSpecs();
        expect(b.computeScreenSpacePosition(scene)).toEqualEpsilon(new Cartesian2(1.5, 2.5), CesiumMath.EPSILON1);
    });

    it('computes screen space position with eyeOffset', function() {
        var b = billboards.add({
            position : Cartesian3.ZERO,
            eyeOffset : new Cartesian3(1.0, 1.0, 0.0)
        });
        scene.renderForSpecs();
        expect(b.computeScreenSpacePosition(scene)).toEqualEpsilon(new Cartesian2(0.5, 0.5), CesiumMath.EPSILON1);
    });

    it('throws when computing screen space position when not in a collection', function() {
        var b = billboards.add({
            position : Cartesian3.ZERO
        });
        billboards.remove(b);
        expect(function() {
            b.computeScreenSpacePosition(scene);
        }).toThrowDeveloperError();
    });

    it('throws when computing screen space position without scene', function() {
        var b = billboards.add();

        expect(function() {
            b.computeScreenSpacePosition();
        }).toThrowDeveloperError();
    });

    it('equals another billboard', function() {
        var b = billboards.add({
            position : new Cartesian3(1.0, 2.0, 3.0),
            color : {
                red : 1.0,
                green : 0.0,
                blue : 0.0,
                alpha : 1.0
            }
        });
        var b2 = billboards.add({
            position : new Cartesian3(1.0, 2.0, 3.0),
            color : {
                red : 1.0,
                green : 0.0,
                blue : 0.0,
                alpha : 1.0
            }
        });

        // This tests the `BillboardCollection.equals` function itself, not simple equality.
        expect(b.equals(b2)).toEqual(true);
    });

    it('does not equal another billboard', function() {
        var b = billboards.add({
            position : new Cartesian3(1.0, 2.0, 3.0)
        });
        var b2 = billboards.add({
            position : new Cartesian3(4.0, 5.0, 6.0)
        });

        // This tests the `BillboardCollection.equals` function itself, not simple equality.
        expect(b.equals(b2)).toEqual(false);
    });

    it('does not equal undefined', function() {
        // This tests the `BillboardCollection.equals` function itself, not simple equality.
        var billboard = billboards.add();
        expect(billboard.equals(undefined)).toEqual(false);
    });

    it('throws when accessing without an index', function() {
        expect(function() {
            billboards.get();
        }).toThrowDeveloperError();
    });

    it('setImage throws without an id', function() {
        var b = billboards.add();
        expect(function() {
            b.setImage(undefined, {});
        }).toThrowDeveloperError();
    });

    it('setImage throws without an inmage', function() {
        var b = billboards.add();
        expect(function() {
            b.setImage('', undefined);
        }).toThrowDeveloperError();
    });

    it('setImageSubRegion throws without an id', function() {
        var b = billboards.add();
        expect(function() {
            b.setImage(undefined, {});
        }).toThrowDeveloperError();
    });

    it('setImageSubRegion throws without a sub-region', function() {
        var b = billboards.add();
        expect(function() {
            b.setImage('', undefined);
        }).toThrowDeveloperError();
    });

    it('computes bounding sphere in 3D', function() {
        var one = billboards.add({
            image : greenImage,
            position : Cartesian3.fromDegrees(-50.0, -50.0)
        });
        var two = billboards.add({
            image : greenImage,
            position : Cartesian3.fromDegrees(-50.0, 50.0)
        });

        scene.renderForSpecs();
        var actual = scene.frameState.commandList[0].boundingVolume;

        var positions = [one.position, two.position];
        var expected = BoundingSphere.fromPoints(positions);
        expect(actual.center).toEqual(expected.center);
        expect(actual.radius).toEqual(expected.radius);
    });

    it('computes bounding sphere in Columbus view', function() {
        var projection = scene.mapProjection;
        var ellipsoid = projection.ellipsoid;

        var one = billboards.add({
            image : greenImage,
            position : Cartesian3.fromDegrees(-50.0, -50.0)
        });
        var two = billboards.add({
            image : greenImage,
            position : Cartesian3.fromDegrees(-50.0, 50.0)
        });

        // Update scene state
        scene.morphToColumbusView(0);
        scene.renderForSpecs();
        var actual = scene.frameState.commandList[0].boundingVolume;

        var projectedPositions = [
            projection.project(ellipsoid.cartesianToCartographic(one.position)),
            projection.project(ellipsoid.cartesianToCartographic(two.position))
        ];
        var expected = BoundingSphere.fromPoints(projectedPositions);
        expected.center = new Cartesian3(0.0, expected.center.x, expected.center.y);
        expect(actual.center).toEqualEpsilon(expected.center, CesiumMath.EPSILON8);
        expect(actual.radius).toBeGreaterThan(expected.radius);
    });

    it('computes bounding sphere in 2D', function() {
        var projection = scene.mapProjection;
        var ellipsoid = projection.ellipsoid;

        var one = billboards.add({
            image : greenImage,
            position : Cartesian3.fromDegrees(-50.0, -50.0)
        });
        var two = billboards.add({
            image : greenImage,
            position : Cartesian3.fromDegrees(-50.0, 50.0)
        });

        var maxRadii = ellipsoid.maximumRadius;
        var orthoFrustum = new OrthographicFrustum();
        orthoFrustum.right = maxRadii * Math.PI;
        orthoFrustum.left = -orthoFrustum.right;
        orthoFrustum.top = orthoFrustum.right;
        orthoFrustum.bottom = -orthoFrustum.top;
        orthoFrustum.near = 0.01 * maxRadii;
        orthoFrustum.far = 60.0 * maxRadii;

        // Update scene state
        scene.morphTo2D(0);
        scene.renderForSpecs();

        camera.frustum = orthoFrustum;

        scene.renderForSpecs();
        var actual = scene.frameState.commandList[0].boundingVolume;

        var projectedPositions = [
            projection.project(ellipsoid.cartesianToCartographic(one.position)),
            projection.project(ellipsoid.cartesianToCartographic(two.position))
        ];
        var expected = BoundingSphere.fromPoints(projectedPositions);
        expected.center = new Cartesian3(0.0, expected.center.x, expected.center.y);
        expect(actual.center).toEqualEpsilon(expected.center, CesiumMath.EPSILON8);
        expect(actual.radius).toBeGreaterThan(expected.radius);
    });

    it('computes bounding sphere with pixel offset', function() {
        var one = billboards.add({
            image : greenImage,
            position : Cartesian3.fromDegrees(-50.0, -50.0),
            pixelOffset : new Cartesian2(0.0, 200.0)
        });
        var two = billboards.add({
            image : greenImage,
            position : Cartesian3.fromDegrees(-50.0, 50.0),
            pixelOffset : new Cartesian2(0.0, 200.0)
        });

        scene.renderForSpecs();
        var actual = scene.frameState.commandList[0].boundingVolume;

        var positions = [one.position, two.position];
        var bs = BoundingSphere.fromPoints(positions);

        var dimensions = new Cartesian2(1.0, 1.0);
        var diff = Cartesian3.subtract(actual.center, camera.position, new Cartesian3());
        var vectorProjection = Cartesian3.multiplyByScalar(camera.direction, Cartesian3.dot(diff, camera.direction), new Cartesian3());
        var distance = Math.max(0.0, Cartesian3.magnitude(vectorProjection) - bs.radius);

        var pixelSize = camera.frustum.getPixelDimensions(dimensions.x, dimensions.y, distance, new Cartesian2());
        bs.radius += pixelSize.y * 0.25 * Math.max(greenImage.width, greenImage.height) + pixelSize.y * one.pixelOffset.y;

        expect(actual.center).toEqual(bs.center);
        expect(actual.radius).toEqual(bs.radius);
    });

    it('computes bounding sphere with non-centered origin', function() {
        billboards.add({
            image : greenImage,
            position : Cartesian3.fromDegrees(-50.0, -50.0)
        });
        scene.renderForSpecs();
        var centeredRadius = scene.frameState.commandList[0].boundingVolume.radius;
        billboards.removeAll();

        billboards.add({
            image : greenImage,
            position : Cartesian3.fromDegrees(-50.0, -50.0),
            verticalOrigin: VerticalOrigin.TOP
        });
        scene.renderForSpecs();
        var verticalRadius = scene.frameState.commandList[0].boundingVolume.radius;
        billboards.removeAll();

        billboards.add({
            image : greenImage,
            position : Cartesian3.fromDegrees(-50.0, -50.0),
            horizontalOrigin: HorizontalOrigin.LEFT
        });
        scene.renderForSpecs();
        var horizontalRadius = scene.frameState.commandList[0].boundingVolume.radius;

        expect(verticalRadius).toEqual(2*centeredRadius);
        expect(horizontalRadius).toEqual(2*centeredRadius);
    });

    it('can create a billboard using a URL', function() {
        scene.renderForSpecs();

        var one = billboards.add({
            image : './Data/Images/Green.png'
        });

        expect(one.ready).toEqual(false);
        expect(one.image).toEqual('./Data/Images/Green.png');

        return pollToPromise(function() {
            return one.ready;
        }).then(function() {
            expect(scene.renderForSpecs()).toEqual([0, 255, 0, 255]);
        });
    });

    it('sets billboard width and height based on loaded image width and height', function() {
        scene.renderForSpecs();

        var one = billboards.add({
            image : './Data/Images/Green1x4.png'
        });

        expect(one.width).toBeUndefined();
        expect(one.height).toBeUndefined();

        return pollToPromise(function() {
            return one.ready;
        }).then(function() {
            expect(one.width).toEqual(1);
            expect(one.height).toEqual(4);

            one.image = './Data/Images/Blue10x10.png';

            return pollToPromise(function() {
                return one.ready;
            }).then(function() {
                expect(one.width).toEqual(10);
                expect(one.height).toEqual(10);
            });
        });
    });

    it('does not cancel image load when a billboard is set to the same URL repeatedly', function() {
        scene.renderForSpecs();

        var one = billboards.add({
            image : './Data/Images/Green.png'
        });

        expect(one.ready).toEqual(false);
        expect(one.image).toEqual('./Data/Images/Green.png');

        one.image = './Data/Images/Green.png';
        one.image = './Data/Images/Green.png';
        one.image = './Data/Images/Green.png';

        return pollToPromise(function() {
            return one.ready;
        });
    });

    it('ignores calls to set image equal to the current value after load', function() {
        scene.renderForSpecs();

        var one = billboards.add({
            image : './Data/Images/Green.png'
        });

        expect(one.ready).toEqual(false);
        expect(one.image).toEqual('./Data/Images/Green.png');

        return pollToPromise(function() {
            return one.ready;
        }).then(function() {
            expect(scene.renderForSpecs()).toEqual([0, 255, 0, 255]);

            one.image = './Data/Images/Green.png';

            expect(one.ready).toEqual(true);
            expect(scene.renderForSpecs()).toEqual([0, 255, 0, 255]);
        });
    });

    it('can create a billboard using a function', function() {
        var one = billboards.add({
            image : function() {
                return greenImage;
            }
        });

        // the image property will be an autogenerated id if not provided
        expect(one.image).toBeDefined();
        expect(scene.renderForSpecs()).toEqual([0, 255, 0, 255]);
    });

    it('can create a billboard using a function and id', function() {
        var one = billboards.add({
            imageId : 'Foo',
            image : function() {
                return greenImage;
            }
        });

        // the image property will be an autogenerated id if not provided
        expect(one.image).toEqual('Foo');
        expect(scene.renderForSpecs()).toEqual([0, 255, 0, 255]);
    });

    it('can create a billboard using another billboard image', function() {
        var createImage = jasmine.createSpy('createImage').and.returnValue(greenImage);

        var one = billboards.add({
            image : createImage
        });

        scene.renderForSpecs();

        expect(createImage.calls.count()).toEqual(1);

        var two = billboards.add({
            image : one.image
        });

        scene.renderForSpecs();

        expect(two.image).toEqual(one.image);
        expect(createImage.calls.count()).toEqual(1);
        expect(scene.renderForSpecs()).toEqual([0, 255, 0, 255]);
    });

    it('can create a billboard using a subregion of an image', function() {
        scene.renderForSpecs();

        var one = billboards.add({
            image : './Data/Images/Red16x16.png',
            imageSubRegion : new BoundingRectangle(0.0, 0.0, 1.0, 2.0)
        });

        expect(one.ready).toEqual(false);

        return pollToPromise(function() {
            return one.ready;
        }).then(function() {
            expect(scene.renderForSpecs()).toEqual([255, 0, 0, 255]);
        });
    });

    it('sets billboard width and height based on subregion width and height', function() {
        scene.renderForSpecs();

        var one = billboards.add({
            image : './Data/Images/Red16x16.png',
            imageSubRegion : new BoundingRectangle(0.0, 0.0, 1.0, 2.0)
        });

        expect(one.width).toBeUndefined();
        expect(one.height).toBeUndefined();

        return pollToPromise(function() {
            return one.ready;
        }).then(function() {
            expect(one.width).toEqual(1);
            expect(one.height).toEqual(2);
        });
    });

    it('can change image while an image is loading', function() {
        scene.renderForSpecs();

        var one = billboards.add({
            image : './Data/Images/Green.png'
        });

        expect(one.ready).toEqual(false);
        expect(one.image).toEqual('./Data/Images/Green.png');

        // switch to blue while green is in-flight

        one.image = './Data/Images/Blue.png';

        expect(one.ready).toEqual(false);
        expect(one.image).toEqual('./Data/Images/Blue.png');

        return pollToPromise(function() {
            return one.ready;
        }).then(function() {
            var deferred = when.defer();

            // render and yield control several times to make sure the
            // green image doesn't clobber the blue
            var iterations = 10;

            function renderAndCheck() {
                expect(scene.renderForSpecs()).toEqual([0, 0, 255, 255]);

                if (iterations > 0) {
                    --iterations;
                    setTimeout(renderAndCheck, 1);
                } else {
                    deferred.resolve();
                }
            }

            renderAndCheck();

            return deferred.promise;
        });
    });

    it('can set image to undefined while an image is loading', function() {
        scene.renderForSpecs();

        var one = billboards.add({
            image : './Data/Images/Green.png'
        });

        expect(one.ready).toEqual(false);
        expect(one.image).toEqual('./Data/Images/Green.png');

        // switch to undefined while green is in-flight

        one.image = undefined;

        expect(one.ready).toEqual(false);
        expect(one.image).toBeUndefined();

        var deferred = when.defer();

        // render and yield control several times to make sure the
        // green image never loads
        var iterations = 10;

        function renderAndCheck() {
            expect(scene.renderForSpecs()).toEqual([0, 0, 0, 255]);

            if (iterations > 0) {
                --iterations;
                setTimeout(renderAndCheck, 1);
            } else {
                deferred.resolve();
            }
        }

        renderAndCheck();

        return deferred.promise;
    });

    it('does not crash when removing a billboard that is loading', function() {
        scene.renderForSpecs();

        var one = billboards.add({
            image : './Data/Images/Green.png'
        });

        billboards.remove(one);

        var deferred = when.defer();

        // render and yield control several times to make sure the
        // green image doesn't crash when it loads
        var iterations = 10;

        function renderAndCheck() {
            expect(scene.renderForSpecs()).toEqual([0, 0, 0, 255]);

            if (iterations > 0) {
                --iterations;
                setTimeout(renderAndCheck, 1);
            } else {
                deferred.resolve();
            }
        }

        renderAndCheck();

        return deferred.promise;
    });

    describe('height referenced billboards', function() {
        function createMockGlobe() {
            var globe = {
                callback : undefined,
                removedCallback : false,
                ellipsoid : Ellipsoid.WGS84,
                update : function() {},
                getHeight : function() {
                    return 0.0;
                },
                _surface : {},
                destroy : function() {}
            };

            globe._surface.updateHeight = function(position, callback) {
                globe.callback = callback;
                return function() {
                    globe.removedCallback = true;
                    globe.callback = undefined;
                };
            };

            return globe;
        }

        it('explicitly constructs a billboard with height reference', function() {
            scene.globe = createMockGlobe();
            var b = billboardsWithHeight.add({
                heightReference : HeightReference.CLAMP_TO_GROUND
            });

            expect(b.heightReference).toEqual(HeightReference.CLAMP_TO_GROUND);
        });

        it('set billboard height reference property', function() {
            scene.globe = createMockGlobe();
            var b = billboardsWithHeight.add();
            b.heightReference = HeightReference.CLAMP_TO_GROUND;

            expect(b.heightReference).toEqual(HeightReference.CLAMP_TO_GROUND);
        });

        it('creating with a height reference creates a height update callback', function() {
            scene.globe = createMockGlobe();
            billboardsWithHeight.add({
                heightReference : HeightReference.CLAMP_TO_GROUND,
                position : Cartesian3.fromDegrees(-72.0, 40.0)
            });
            expect(scene.globe.callback).toBeDefined();
        });

        it('set height reference property creates a height update callback', function() {
            scene.globe = createMockGlobe();
            var b = billboardsWithHeight.add({
                position : Cartesian3.fromDegrees(-72.0, 40.0)
            });
            b.heightReference = HeightReference.CLAMP_TO_GROUND;
            expect(scene.globe.callback).toBeDefined();
        });

        it('updates the callback when the height reference changes', function() {
            scene.globe = createMockGlobe();
            var b = billboardsWithHeight.add({
                heightReference : HeightReference.CLAMP_TO_GROUND,
                position : Cartesian3.fromDegrees(-72.0, 40.0)
            });
            expect(scene.globe.callback).toBeDefined();

            b.heightReference = HeightReference.RELATIVE_TO_GROUND;
            expect(scene.globe.removedCallback).toEqual(true);
            expect(scene.globe.callback).toBeDefined();

            scene.globe.removedCallback = false;
            b.heightReference = HeightReference.NONE;
            expect(scene.globe.removedCallback).toEqual(true);
            expect(scene.globe.callback).not.toBeDefined();
        });

        it('changing the position updates the callback', function() {
            scene.globe = createMockGlobe();
            var b = billboardsWithHeight.add({
                heightReference : HeightReference.CLAMP_TO_GROUND,
                position : Cartesian3.fromDegrees(-72.0, 40.0)
            });
            expect(scene.globe.callback).toBeDefined();

            b.position = Cartesian3.fromDegrees(-73.0, 40.0);
            expect(scene.globe.removedCallback).toEqual(true);
            expect(scene.globe.callback).toBeDefined();
        });

        it('callback updates the position', function() {
            scene.globe = createMockGlobe();
            var b = billboardsWithHeight.add({
                heightReference : HeightReference.CLAMP_TO_GROUND,
                position : Cartesian3.fromDegrees(-72.0, 40.0)
            });
            expect(scene.globe.callback).toBeDefined();

            var cartographic = scene.globe.ellipsoid.cartesianToCartographic(b._clampedPosition);
            expect(cartographic.height).toEqual(0.0);

            scene.globe.callback(Cartesian3.fromDegrees(-72.0, 40.0, 100.0));
            cartographic = scene.globe.ellipsoid.cartesianToCartographic(b._clampedPosition);
            expect(cartographic.height).toEqualEpsilon(100.0, CesiumMath.EPSILON9);
        });
    });
}, 'WebGL');
