describe('Skottie behavior', () => {
    let container;

    beforeEach(async () => {
        await EverythingLoaded;
        container = document.createElement('div');
        container.innerHTML = `
            <canvas width=600 height=600 id=test></canvas>
            <canvas width=600 height=600 id=report></canvas>`;
        document.body.appendChild(container);
    });

    afterEach(() => {
        document.body.removeChild(container);
    });

    const expectArrayCloseTo = (a, b, precision) => {
        precision = precision || 14; // digits of precision in base 10
        expect(a.length).toEqual(b.length);
        for (let i=0; i<a.length; i++) {
          expect(a[i]).toBeCloseTo(b[i], precision);
        }
    };

    const imgPromise = fetch('/assets/flightAnim.gif')
        .then((response) => response.arrayBuffer());
    const jsonPromise = fetch('/assets/animated_gif.json')
        .then((response) => response.text());
    const washPromise = fetch('/assets/map-shield.json')
        .then((response) => response.text());
    const slotPromise = fetch('/assets/skottie_basic_slots.json')
        .then((response) => response.text());
    const editPromise = fetch('/assets/text_edit.json')
        .then((response) => response.text());
    const notoSerifPromise = fetch('/assets/NotoSerif-Regular.ttf').then(
        (response) => response.arrayBuffer());

    gm('skottie_animgif', (canvas, promises) => {
        if (!CanvasKit.skottie || !CanvasKit.managed_skottie) {
            console.warn('Skipping test because not compiled with skottie');
            return;
        }
        expect(promises[1]).not.toBe('NOT FOUND');
        const animation = CanvasKit.MakeManagedAnimation(promises[1], {
            'flightAnim.gif': promises[0],
        });
        expect(animation).toBeTruthy();
        const bounds = CanvasKit.LTRBRect(0, 0, 500, 500);

        const size = animation.size();
        expectArrayCloseTo(size, Float32Array.of(800, 600), 4);

        animation.render(canvas, bounds);

        // We intentionally make the length of this array 5 and add a sentinel value
        // of 999 so we can make sure the bounds are copied into this rect and a new
        // one is not allocated.
        const damageRect = Float32Array.of(0, 0, 0, 0, 999);

        // There was a bug, fixed in https://skia-review.googlesource.com/c/skia/+/241757
        // that seeking again and drawing again revealed.
        animation.seek(0.5, damageRect);
        expectArrayCloseTo(damageRect, Float32Array.of(0, 0, 800, 600, 999), 4);

        canvas.clear(CanvasKit.WHITE);
        animation.render(canvas, bounds);
        animation.delete();
    }, imgPromise, jsonPromise);

    gm('skottie_setcolor', (canvas, promises) => {
        if (!CanvasKit.skottie || !CanvasKit.managed_skottie) {
            console.warn('Skipping test because not compiled with skottie');
            return;
        }
        expect(promises[0]).not.toBe('NOT FOUND');
        const bounds = CanvasKit.LTRBRect(0, 0, 500, 500);

        const animation = CanvasKit.MakeManagedAnimation(promises[0]);
        expect(animation).toBeTruthy();
        animation.setColor('$Icon Fill', CanvasKit.RED);
        animation.seek(0.5);
        animation.render(canvas, bounds);
        animation.delete();
    }, washPromise);

    gm('skottie_slots', (canvas, promises) => {
        if (!CanvasKit.skottie || !CanvasKit.managed_skottie) {
            console.warn('Skipping test because not compiled with skottie');
            return;
        }
        expect(promises[0]).not.toBe('NOT FOUND');
        const bounds = CanvasKit.LTRBRect(0, 0, 500, 500);

        const animation = CanvasKit.MakeManagedAnimation(promises[0], {
            'flightAnim.gif': promises[1],
            'NotoSerif': promises[2],
        });
        expect(animation).toBeTruthy();

        const slotInfo = animation.getSlotInfo();
        expect(slotInfo.colorSlotIDs).toEqual(['FillsGroup', 'StrokeGroup']);
        expect(slotInfo.scalarSlotIDs).toEqual(['Opacity']);
        expect(slotInfo.vec2SlotIDs).toEqual(['ScaleGroup']);
        expect(slotInfo.imageSlotIDs).toEqual(['ImageSource']);
        expect(slotInfo.textSlotIDs).toEqual(['TextSource']);

        expect(animation.getScalarSlot('Opacity')).toBe(100);
        const textProp = animation.getTextSlot('TextSource');
        expect(textProp.text).toBe('text slots');

        textProp.text = 'new text';
        textProp.fillColor = CanvasKit.CYAN;
        textProp.strokeColor = CanvasKit.MAGENTA;

        expect(animation.setColorSlot('FillsGroup', CanvasKit.RED)).toBeTruthy();
        expect(animation.setScalarSlot('Opacity', 25)).toBeTruthy();
        expect(animation.setVec2Slot('ScaleGroup', [25, 50])).toBeTruthy();
        expect(animation.setImageSlot('ImageSource', 'flighAnim.gif')).toBeTruthy();
        expect(animation.setTextSlot('TextSource', textProp)).toBeTruthy();

        expectArrayCloseTo(animation.getColorSlot('FillsGroup'), CanvasKit.RED, 4);
        expect(animation.getScalarSlot('Opacity')).toBe(25);
        expectArrayCloseTo(animation.getVec2Slot('ScaleGroup'), [25, 50], 4);

        const newTextSlot = animation.getTextSlot('TextSource');
        expect(newTextSlot.text).toBe('new text');
        expectArrayCloseTo(newTextSlot.fillColor, CanvasKit.CYAN, 4);
        expectArrayCloseTo(newTextSlot.strokeColor, CanvasKit.MAGENTA, 4);

        expect(animation.getColorSlot('Bad ID')).toBeFalsy();
        expect(animation.getScalarSlot('Bad ID')).toBeFalsy();
        expect(animation.getVec2Slot('Bad ID')).toBeFalsy();
        expect(animation.getTextSlot('Bad ID')).toBeFalsy();

        animation.seek(0.5);
        animation.render(canvas, bounds);
        animation.delete();
    }, slotPromise, imgPromise, notoSerifPromise);

    gm('skottie_textedit', (canvas, promises) => {
        if (!CanvasKit.skottie || !CanvasKit.managed_skottie) {
            console.warn('Skipping test because not compiled with skottie');
            return;
        }
        expect(promises[0]).not.toBe('NOT FOUND');
        const bounds = CanvasKit.LTRBRect(0, 0, 600, 600);

        const animation = CanvasKit.MakeManagedAnimation(promises[0], {
            // The animation is looking for a font called ArialMT, but we just
            // provide it the data for an arbitrary typeface.
            "ArialMT": promises[1],
        });
        expect(animation).toBeTruthy();

        // The animation contains two text layers grouped under the "text_layer" ID, and one
        // descriptive text layer.
        {
            const texts = animation.getTextProps();
            expect(texts.length).toEqual(2);
            expect(texts[0].key).toEqual('text_layer');
            expect(texts[0].value.text).toEqual('foo');
        }

        expect(animation.attachEditor('txt_layer', 0)).toBeFalse();  // nonexistent layer
        expect(animation.attachEditor('text_layer', 2)).toBeFalse(); // nonexistent index
        expect(animation.attachEditor('text_layer', 0)).toBeTrue();
        expect(animation.attachEditor('text_layer', 1)).toBeTrue();

        {
            // no effect, editor inactive
            expect(animation.dispatchEditorKey('Backspace')).toBeFalse();

            const texts = animation.getTextProps();
            expect(texts.length).toEqual(2);
            expect(texts[0].key).toEqual('text_layer');
            expect(texts[0].value.text).toEqual('foo');
        }

        animation.enableEditor(true);
        animation.setEditorCursorWeight(1.5);

        // To be fully functional, the editor requires glyph data issued during rendering callbacks.
        animation.seek(0);
        animation.render(canvas, bounds);

        {
            expect(animation.dispatchEditorKey('Backspace')).toBeTrue();
            expect(animation.dispatchEditorKey('Backspace')).toBeTrue();
            expect(animation.dispatchEditorKey('Backspace')).toBeTrue();
            expect(animation.dispatchEditorKey('b')).toBeTrue();
            expect(animation.dispatchEditorKey('a')).toBeTrue();
            expect(animation.dispatchEditorKey('r')).toBeTrue();

            const texts = animation.getTextProps();
            expect(texts.length).toEqual(2);
            expect(texts[0].key).toEqual('text_layer');
            expect(texts[0].value.text).toEqual('bar');
        }

        // Final render, after edits.
        animation.seek(0);
        animation.render(canvas, bounds);
        animation.delete();
    }, editPromise, notoSerifPromise);

    it('can load audio assets', (done) => {
        if (!CanvasKit.skottie || !CanvasKit.managed_skottie) {
            console.warn('Skipping test because not compiled with skottie');
            return;
        }
        const mockSoundMap = {
            map : new Map(),
            getPlayer : function(name) {return this.map.get(name)},
            setPlayer : function(name, player) {this.map.set(name, player)},
        };
        function mockPlayer(name) {
            this.name = name;
            this.wasPlayed = false,
            this.seek = function(t) {
                this.wasPlayed = true;
            }
        }
        for (let i = 0; i < 20; i++) {
            var name = 'audio_' + i;
            mockSoundMap.setPlayer(name, new mockPlayer(name));
        }
        fetch('/assets/audio_external.json')
        .then((response) => response.text())
        .then((lottie) => {
            const animation = CanvasKit.MakeManagedAnimation(lottie, null, null, mockSoundMap);
            expect(animation).toBeTruthy();
            // 190 frames in sample lottie
            for (let t = 0; t < 190; t++) {
                animation.seekFrame(t);
            }
            animation.delete();
            for(const player of mockSoundMap.map.values()) {
                expect(player.wasPlayed).toBeTrue(player.name + " was not played");
            }
            done();
        });
    });

    it('can get logs', (done) => {
        if (!CanvasKit.skottie || !CanvasKit.managed_skottie) {
            console.warn('Skipping test because not compiled with skottie');
            return;
        }

        const logger = {
           errors:   [],
           warnings: [],

           reset: function() { this.errors = []; this.warnings = []; },

           // Logger API
           onError:   function(err) { this.errors.push(err)   },
           onWarning: function(wrn) { this.warnings.push(wrn) }
        };

        {
            const json = `{
                "v": "5.2.1",
                "w": 100,
                "h": 100,
                "fr": 10,
                "ip": 0,
                "op": 100,
                "layers": [{
                    "ty": 3,
                    "nm": "null",
                    "ind": 0,
                    "ip": 0
                }]
            }`;
            const animation = CanvasKit.MakeManagedAnimation(json, null, null, null, logger);
            expect(animation).toBeTruthy();
            expect(logger.errors.length).toEqual(0);
            expect(logger.warnings.length).toEqual(0);
        }

        {
            const json = `{
                "v": "5.2.1",
                "w": 100,
                "h": 100,
                "fr": 10,
                "ip": 0,
                "op": 100,
                "layers": [{
                    "ty": 2,
                    "nm": "image",
                    "ind": 0,
                    "ip": 0
                }]
            }`;
            const animation = CanvasKit.MakeManagedAnimation(json, null, null, null, logger);
            expect(animation).toBeTruthy();
            expect(logger.errors.length).toEqual(1);
            expect(logger.warnings.length).toEqual(0);

            // Image layer missing refID
            expect(logger.errors[0].includes('missing ref'));
            logger.reset();
        }

        {
            const json = `{
                "v": "5.2.1",
                "w": 100,
                "h": 100,
                "fr": 10,
                "ip": 0,
                "op": 100,
                "layers": [{
                    "ty": 1,
                    "nm": "solid",
                    "sw": 100,
                    "sh": 100,
                    "sc": "#aabbcc",
                    "ind": 0,
                    "ip": 0,
                    "ef": [{
                      "mn": "FOO"
                    }]
                }]
            }`;
            const animation = CanvasKit.MakeManagedAnimation(json, null, null, null, logger);
            expect(animation).toBeTruthy();
            expect(logger.errors.length).toEqual(0);
            expect(logger.warnings.length).toEqual(1);

            // Unsupported effect FOO
            expect(logger.warnings[0].includes('FOO'));
            logger.reset();
        }

        done();
    });

    it('can access dynamic props', () => {
        if (!CanvasKit.skottie || !CanvasKit.managed_skottie) {
            console.warn('Skipping test because not compiled with skottie');
            return;
        }

        const json = `{
            "v": "5.2.1",
            "w": 100,
            "h": 100,
            "fr": 10,
            "ip": 0,
            "op": 100,
            "fonts": {
              "list": [{
                "fName": "test_font",
                "fFamily": "test-family",
                "fStyle": "TestFontStyle"
              }]
            },
            "layers": [
              {
                "ty": 4,
                "nm": "__shape_layer",
                "ind": 0,
                "ip": 0,
                "shapes": [
                  {
                    "ty": "el",
                    "p": { "a": 0, "k": [ 50, 50 ] },
                    "s": { "a": 0, "k": [ 50, 50 ] }
                  },{
                    "ty": "fl",
                    "nm": "__shape_fill",
                    "c": { "a": 0, "k": [ 1, 0, 0] }
                  },{
                    "ty": "tr",
                    "nm": "__shape_opacity",
                    "o": { "a": 0, "k": 50 }
                  }
                ]
              },{
                "ty": 5,
                "nm": "__text_layer",
                "ip": 0,
                "t": {
                  "d": {
                    "k": [{
                      "t": 0,
                      "s": {
                        "f": "test_font",
                        "s": 100,
                        "t": "Foo Bar Baz",
                        "lh": 120,
                        "ls": 12
                      }
                    }]
                  }
                }
              }
            ]
        }`;

        const animation = CanvasKit.MakeManagedAnimation(json, null, '__');
        expect(animation).toBeTruthy();

        {
            const colors = animation.getColorProps();
            expect(colors.length).toEqual(1);
            expect(colors[0].key).toEqual('__shape_fill');
            expect(colors[0].value).toEqual(CanvasKit.ColorAsInt(255,0,0,255));

            const opacities = animation.getOpacityProps();
            expect(opacities.length).toEqual(1);
            expect(opacities[0].key).toEqual('__shape_opacity');
            expect(opacities[0].value).toEqual(50);

            const texts = animation.getTextProps();
            expect(texts.length).toEqual(1);
            expect(texts[0].key).toEqual('__text_layer');
            expect(texts[0].value.text).toEqual('Foo Bar Baz');
            expect(texts[0].value.size).toEqual(100);
        }

        expect(animation.setColor('__shape_fill', [0,1,0,1])).toEqual(true);
        expect(animation.setOpacity('__shape_opacity', 100)).toEqual(true);
        expect(animation.setText('__text_layer', 'baz bar foo', 10)).toEqual(true);

        {
            const colors = animation.getColorProps();
            expect(colors.length).toEqual(1);
            expect(colors[0].key).toEqual('__shape_fill');
            expect(colors[0].value).toEqual(CanvasKit.ColorAsInt(0,255,0,255));

            const opacities = animation.getOpacityProps();
            expect(opacities.length).toEqual(1);
            expect(opacities[0].key).toEqual('__shape_opacity');
            expect(opacities[0].value).toEqual(100);

            const texts = animation.getTextProps();
            expect(texts.length).toEqual(1);
            expect(texts[0].key).toEqual('__text_layer');
            expect(texts[0].value.text).toEqual('baz bar foo');
            expect(texts[0].value.size).toEqual(10);
        }

        expect(animation.setColor('INVALID_KEY', [0,1,0,1])).toEqual(false);
        expect(animation.setOpacity('INVALID_KEY', 100)).toEqual(false);
        expect(animation.setText('INVALID KEY', '', 10)).toEqual(false);
    });
});
