/* @license
 * Copyright 2019 Google LLC. All Rights Reserved.
 * Licensed under the Apache License, Version 2.0 (the 'License');
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an 'AS IS' BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import {$defaultPosterElement, $posterContainerElement} from '../../features/loading.js';
import {$scene, $userInputElement} from '../../model-viewer-base.js';
import {ModelViewerElement} from '../../model-viewer.js';
import {CachingGLTFLoader} from '../../three-components/CachingGLTFLoader.js';
import {timePasses, waitForEvent} from '../../utilities.js';
import {assetPath, pickShadowDescendant, rafPasses, until} from '../helpers.js';

const expect = chai.expect;
const CUBE_GLB_PATH = assetPath('models/cube.gltf');
const HORSE_GLB_PATH = assetPath('models/Horse.glb');

suite('Loading', () => {
  let element: ModelViewerElement;

  setup(async () => {
    element = new ModelViewerElement();
    document.body.insertBefore(element, document.body.firstChild);
    element.poster = assetPath('../screenshot.png');

    // Wait at least a microtask for size calculations
    await timePasses();
  });

  teardown(() => {
    CachingGLTFLoader.clearCache();

    if (element.parentNode != null) {
      element.parentNode.removeChild(element);
    }
  });

  test('creates a poster element that captures interactions', async () => {
    const picked = pickShadowDescendant(element);
    expect(picked).to.be.ok;
    // TODO(cdata): Leaky internal details here:
    expect(picked!.id).to.be.equal('default-poster');
  });

  test('does not load when hidden from render tree', async () => {
    let loadDispatched = false;
    const loadHandler = () => {
      loadDispatched = true;
    };

    element.addEventListener('load', loadHandler);

    element.style.display = 'none';

    // Give IntersectionObserver a chance to notify. In Chrome, this takes
    // two rAFs (empirically observed). Await extra time just in case:
    await timePasses(100);

    element.src = CUBE_GLB_PATH;

    await timePasses(500);  // Arbitrary time to allow model to load

    element.removeEventListener('load', loadHandler);

    expect(loadDispatched).to.be.false;
  });

  suite('load', () => {
    suite('when a model src changes after loading', () => {
      setup(async () => {
        // The shadow is here to expose an earlier bug on unloading models.
        element.shadowIntensity = 1;
        element.src = CUBE_GLB_PATH;
        await waitForEvent(element, 'poster-dismissed');
      });

      test('only dispatches load once per src change', async () => {
        let loadCount = 0;
        const onLoad = () => {
          loadCount++;
        };

        element.addEventListener('load', onLoad);

        try {
          element.src = HORSE_GLB_PATH;

          await waitForEvent(element, 'load');

          element.src = CUBE_GLB_PATH;

          await waitForEvent(element, 'load');

          // Give any late-dispatching events a chance to dispatch
          await timePasses(300);

          expect(loadCount).to.be.equal(2);
        } finally {
          element.removeEventListener('load', onLoad);
        }
      });

      test('getDimensions() returns correct size', () => {
        const size = element.getDimensions();
        expect(size.x).to.be.eq(1);
        expect(size.y).to.be.eq(1);
        expect(size.z).to.be.eq(1);
      });

      test('models are unloaded after src updates', async () => {
        element.src = HORSE_GLB_PATH;
        await waitForEvent(element, 'load');

        const {shadow, model, target} = element[$scene];
        const {children} = target;
        expect(children.length).to.be.eq(2, 'horse');
        expect(children).to.contain(shadow, 'horse shadow');
        expect(children).to.contain(model, 'horse model');

        element.src = CUBE_GLB_PATH;
        await waitForEvent(element, 'load');
        const {children: children2} = target;
        expect(children2.length).to.be.eq(2, 'cube');
        expect(children2).to.contain(shadow, 'cube shadow');
        expect(children2).to.contain(element[$scene].model, 'cube model');
      });

      test('generates 3DModel schema', async () => {
        element.generateSchema = true;
        await element.updateComplete;
        const {schemaElement} = element[$scene];
        expect(schemaElement.type).to.be.eq('application/ld+json');
        expect(schemaElement.parentElement).to.be.eq(document.head);
        const json = JSON.parse(schemaElement.textContent!);
        const encoding = json.encoding[0];

        expect(encoding.contentUrl).to.be.eq(CUBE_GLB_PATH);
        expect(encoding.encodingFormat).to.be.eq('model/gltf+json');

        element.generateSchema = false;
        await element.updateComplete;
        expect(schemaElement.parentElement).to.be.not.ok;
      });
    });
  });

  suite('loading', () => {
    suite('src changes quickly', () => {
      test('eventually notifies that current src is loaded', async () => {
        element.loading = 'eager';
        element.src = CUBE_GLB_PATH;

        const loadCubeEvent =
            waitForEvent(element, 'load') as Promise<CustomEvent>;

        await timePasses();

        element.src = HORSE_GLB_PATH;

        const loadCube = await loadCubeEvent;
        const loadHorse = await waitForEvent(element, 'load') as CustomEvent;

        expect(loadCube.detail.url).to.be.eq(CUBE_GLB_PATH);
        expect(loadHorse.detail.url).to.be.eq(HORSE_GLB_PATH);
      });
    });


    suite('reveal', () => {
      suite('auto', () => {
        test('hides poster when element loads', async () => {
          element.src = CUBE_GLB_PATH;
          const input = element[$userInputElement];

          expect(pickShadowDescendant(element))
              .to.be.not.equal(
                  input, 'the poster should be shown until the model loads');

          await waitForEvent(
              element,
              'model-visibility',
              (event: any) => event.detail.visible);

          await rafPasses();

          expect(pickShadowDescendant(element)).to.be.equal(input);

          element.reveal = 'manual';
          await element.updateComplete;
          await rafPasses();

          expect(pickShadowDescendant(element))
              .to.be.equal(input, 'changing reveal should not show the poster');
        });
      });

      suite('manual', () => {
        test('does not hide poster until dismissed', async () => {
          element.loading = 'eager';
          element.reveal = 'manual';
          element.src = CUBE_GLB_PATH;

          const posterElement = (element as any)[$defaultPosterElement];
          const input = element[$userInputElement];

          await waitForEvent(element, 'load');

          posterElement.focus();

          expect(element.shadowRoot!.activeElement).to.be.equal(posterElement);

          element.dismissPoster();

          await until(() => {
            return element.shadowRoot!.activeElement === input;
          });
        });
      });
    });
  });

  suite('configuring poster via attribute', () => {
    suite('removing the attribute', () => {
      test('sets poster to null', async () => {
        // NOTE(cdata): This is less important after we resolve
        // https://github.com/PolymerLabs/model-viewer/issues/76
        element.setAttribute('poster', CUBE_GLB_PATH);
        await timePasses();
        element.removeAttribute('poster');
        await timePasses();
        expect(element.poster).to.be.equal(null);
      });
    });
  });

  suite('with loaded model src', () => {
    setup(() => {
      element.src = CUBE_GLB_PATH;
    });

    test('can be hidden imperatively', async () => {
      const ostensiblyThePoster = pickShadowDescendant(element);

      element.dismissPoster();

      await waitForEvent<CustomEvent>(
          element, 'model-visibility', event => event.detail.visible === true);

      await rafPasses();

      const ostensiblyNotThePoster = pickShadowDescendant(element);

      expect(ostensiblyThePoster).to.not.be.equal(ostensiblyNotThePoster);
    });

    suite('when poster is hidden', () => {
      setup(async () => {
        element.dismissPoster();
        await waitForEvent<CustomEvent>(
            element,
            'model-visibility',
            event => event.detail.visible === true);
        await rafPasses();
      });

      test('allows the input to be interactive', async () => {
        const input = element[$userInputElement];
        const picked = pickShadowDescendant(element);

        expect(picked).to.be.equal(input);
      });

      test('when src is reset, poster is dismissible', async () => {
        const posterElement = (element as any)[$defaultPosterElement];
        const posterContainer = (element as any)[$posterContainerElement];
        const inputElement = element[$userInputElement];

        element.reveal = 'manual';
        element.src = null;
        element.showPoster();

        await timePasses();

        element.src = CUBE_GLB_PATH;

        await timePasses();

        expect(posterContainer.classList.contains('show')).to.be.true;

        posterElement.focus();

        expect(element.shadowRoot!.activeElement).to.be.equal(posterElement);

        element.dismissPoster();

        await until(() => {
          return element.shadowRoot!.activeElement === inputElement;
        });
      });
    });
  });
});
