import {toggle} from '#core/dom/style';
import {endsWith} from '#core/types/string';

import {Services} from '#service';
import {AmpDocSingle, installDocService} from '#service/ampdoc-impl';
import {FixedLayer} from '#service/fixed-layer';
import {installHiddenObserverForDoc} from '#service/hidden-observer-impl';
import {installPlatformService} from '#service/platform-impl';
import {installTimerService} from '#service/timer-impl';
import {installViewerServiceForDoc} from '#service/viewer-impl';

import {Animation} from '#utils/animation';
import {user} from '#utils/log';

import {
  FakeMutationObserver,
  FakePerformance,
  FakeWindow,
} from '#testing/fake-dom';
import {hypenCaseToCamelCase} from '#testing/helpers';

describes.sandboxed('FixedLayer', {}, (env) => {
  let parentApi;
  let documentApi;
  let ampdoc;
  let viewer;
  let vsyncApi;
  let vsyncTasks;
  let docBody, docElem;
  let element1;
  let element2;
  let element3;
  let element4;
  let element5;
  let element6;
  let allRules;
  let timer;

  beforeEach(() => {
    parentApi = {};
    documentApi = {};
    allRules = {};

    docBody = createElement('docBody');
    docBody.id = 'doc-body-id';
    docElem = createElement('docElem');

    element1 = createElement('element1');
    element2 = createElement('element2');
    element3 = createElement('element3');
    element4 = createElement('element4');
    element5 = createElement('element5');
    element6 = createElement('element6');
    docBody.appendChild(element1);
    docBody.appendChild(element2);
    docBody.appendChild(element3);
    docBody.appendChild(element4);
    docBody.appendChild(element5);
    docBody.appendChild(element6);

    const invalidRule = createValidRule('#invalid', 'fixed', [
      element1,
      element3,
    ]);
    Object.assign(documentApi, {
      styleSheets: [
        // Will be ignored due to being a link.
        {
          ownerNode: document.createElement('link'),
          cssRules: [invalidRule],
        },
        // Will be ignored because it's disabled
        {
          ownerNode: createStyleNode(),
          disabled: true,
          cssRules: [invalidRule],
        },
        // Will be ignored because it's a boilerplate
        {
          ownerNode: createStyleNode('amp-boilerplate'),
          cssRules: [invalidRule],
        },
        // Will be ignored because it's a runtime
        {
          ownerNode: createStyleNode('amp-runtime'),
          cssRules: [invalidRule],
        },
        // Will be ignored because it's an extension
        {
          ownerNode: createStyleNode('amp-extension', 'amp-fit-text'),
          cssRules: [invalidRule],
        },
        // Valid stylesheet with amp-custom
        {
          ownerNode: createStyleNode('amp-custom'),
          cssRules: [
            createValidRule('#amp-custom-rule1', 'fixed', [element1]),
            createValidRule('#doc-body-id #amp-custom-rule1', 'fixed', [
              element1,
            ]),
            createValidRule('#amp-custom-rule2', 'fixed', [element1, element2]),
            createUnrelatedRule('#amp-custom-rule3', [element3]),
            createValidRule('#amp-custom-rule4', 'sticky', [
              element2,
              element4,
            ]),
            createValidRule('#amp-custom-rule5', '-webkit-sticky', [element5]),
            {
              type: 4,
              cssRules: [
                createValidRule('#amp-custom-media-rule1', 'fixed', [element1]),
              ],
            },
            {
              type: 12,
              cssRules: [
                createValidRule('#amp-custom-supports-rule1', 'fixed', [
                  element2,
                ]),
              ],
            },
            // Unknown rule.
            {
              type: 3,
            },
          ],
        },
        // Valid stylesheet without amp-custom
        {
          ownerNode: createStyleNode(),
          cssRules: [
            createValidRule('#other-rule1', 'fixed', [element1]),
            createValidRule('#other-rule2', 'fixed', [element2]),
            createUnrelatedRule('#other-rule3', [element1, element3]),
          ],
        },
      ],
      querySelectorAll: (selector) => {
        if (!allRules[selector]) {
          return null;
        }
        return allRules[selector].elements;
      },
      contains: (elem) => {
        return !!elem.parentElement;
      },
      defaultView: {
        setTimeout: window.setTimeout,
        clearTimeout: window.clearTimeout,
        Promise: window.Promise,
        MutationObserver: FakeMutationObserver,
        getComputedStyle: (elem) => {
          return elem.computedStyle;
        },
        navigator: window.navigator,
        location: window.location,
        cookie: '',
        performance: new FakePerformance(window),
      },
      createElement: (name) => {
        return createElement(name);
      },
      documentElement: docElem,
      body: docBody,
      parent: parentApi,
    });
    documentApi.defaultView.document = documentApi;
    installDocService(documentApi.defaultView, /* isSingleDoc */ true);
    ampdoc = Services.ampdocServiceFor(documentApi.defaultView).getSingleDoc();
    installHiddenObserverForDoc(ampdoc);
    installPlatformService(documentApi.defaultView);
    installTimerService(documentApi.defaultView);
    installViewerServiceForDoc(ampdoc);
    timer = Services.timerFor(documentApi.defaultView);
    viewer = Services.viewerForDoc(ampdoc);
    viewer.isEmbedded = () => true;

    vsyncTasks = [];
    vsyncApi = {
      runPromise: (task) => {
        vsyncTasks.push(task);
        return Promise.resolve();
      },
      mutate: (mutator) => {
        vsyncTasks.push({mutate: mutator});
      },
    };
  });

  class FakeAttributes {
    constructor() {
      const attrs = [];
      attrs.setNamedItem = function ({name, value}) {
        for (let i = 0; i < this.length; i++) {
          if (this[i].name === name) {
            this[i].value = value;
            return;
          }
        }
        this.push(new FakeAttr(name, value));
      };
      return attrs;
    }
  }
  class FakeAttr {
    constructor(name, value) {
      this.name = name;
      this.value = value;
    }

    cloneNode() {
      return new FakeAttr(this.name, this.value);
    }
  }
  function createElement(id) {
    const children = [];
    const elem = {
      id,
      nodeType: 1,
      ownerDocument: documentApi,
      autoTop: '',
      tagName: id,
      toString: () => {
        return id;
      },
      style: {
        top: '15px',
        bottom: '',
        position: '',
        opacity: '0.9',
        visibility: 'visible',
        transition: '',

        setProperty(prop, value, priority) {
          // Override if important
          if (priority === 'important') {
            elem.style[prop] = `${value} !${priority}`;
          } else if (
            elem.style[prop] ||
            !elem.computedStyle[prop]?.endsWith('!important')
          ) {
            if (
              prop === 'transition' &&
              !value &&
              endsWith(elem.style[prop] || '', '!important')
            ) {
              // Emulate a stupid Safari bug.
              // noop.
            } else {
              // If element style is already set, we can override
              // Or, if computed style is not important priority
              elem.style[hypenCaseToCamelCase(prop)] = value;
            }
          }
        },
      },
      computedStyle: {
        opacity: '0.9',
        visibility: 'visible',
        _top: '',
        get top() {
          if (
            elem.computedStyle.transition &&
            elem.style.transition !== '' &&
            elem.style.transition !== 'none !important'
          ) {
            return this._oldTop;
          }
          if (elem.style.bottom) {
            return elem.autoTop || this._top;
          }
          return this._top;
        },
        set top(val) {
          this._oldTop = this._top;
          this._top = val;
        },
        bottom: '',
        zIndex: '',
        transform: '',
        position: '',
        transition: '',
      },
      matches: () => true,
      compareDocumentPosition: (other) => {
        if (other.id > id) {
          return Node.DOCUMENT_POSITION_FOLLOWING;
        }
        return Node.DOCUMENT_POSITION_PRECEDING;
      },
      contains(elem) {
        let el = elem;
        while (el) {
          if (el === this) {
            return true;
          }
          el = el.parentElement;
        }
        return false;
      },
      hasAttribute(name) {
        for (let i = 0; i < this.attributes.length; i++) {
          if (this.attributes[i].name === name) {
            return true;
          }
        }
        return false;
      },
      getAttribute(name) {
        for (let i = 0; i < this.attributes.length; i++) {
          if (this.attributes[i].name === name) {
            return this.attributes[i].value;
          }
        }
        return null;
      },
      setAttribute(name, value) {
        if (name.includes('[')) {
          throw new Error('invalid characters');
        }
        for (let i = 0; i < this.attributes.length; i++) {
          if (this.attributes[i].name === name) {
            this.attributes[i].value = value;
            return;
          }
        }
        this.attributes.push(new FakeAttr(name, value));
      },
      removeAttribute(name) {
        for (let i = 0; i < this.attributes.length; i++) {
          if (this.attributes[i].name === name) {
            this.attributes.splice(i, 1);
            return;
          }
        }
      },
      attributes: new FakeAttributes(),
      appendChild: (child) => {
        child.parentElement = elem;
        children.push(child);
      },
      removeChild: (child) => {
        const index = children.indexOf(child);
        if (index != -1) {
          children.splice(index, 1);
        }
        child.parentElement = null;
      },
      replaceChild: (newChild, oldChild) => {
        oldChild.parentElement = null;
        if (children.indexOf(oldChild) != -1) {
          children.splice(children.indexOf(oldChild), 1);
        }
        newChild.parentElement = elem;
        children.push(newChild);
      },
      cloneNode() {
        return createElement(this.id);
      },
      get offsetTop() {
        return parseFloat(elem.computedStyle.top);
      },
      set innerHTML(html) {
        if (html === '') {
          this.firstElementChild = null;
          return;
        }
        // Updating the placeholder means we have to update the tests.
        expect(html.trim()).to.equal(
          '<i-amphtml-fpa style="display: none"></i-amphtml-fpa>'
        );
        this.firstElementChild = createElement('i-amphtml-fpa');
        toggle(this.firstElementChild, false);
      },
      getRootNode() {
        return documentApi;
      },
      dispatchEvent() {},
    };
    return elem;
  }

  function createStyleNode(attr, value) {
    const node = document.createElement('style');
    if (attr) {
      node.setAttribute(attr, value || '');
    }
    return node;
  }

  function createValidRule(selector, position, elements) {
    const rule = {
      type: 1,
      selectorText: selector,
      style: {position},
      elements,
    };
    if (allRules[selector]) {
      throw new Error('dup selector');
    }
    allRules[selector] = rule;
    return rule;
  }

  function createUnrelatedRule(selector, elements) {
    const rule = {
      type: 1,
      selectorText: selector,
      style: {},
      elements,
    };
    if (allRules[selector]) {
      throw new Error('dup selector');
    }
    allRules[selector] = rule;
    return rule;
  }

  describe('no-transfer', () => {
    let fixedLayer;

    beforeEach(() => {
      fixedLayer = new FixedLayer(
        ampdoc,
        vsyncApi,
        /* borderTop */ 0,
        /* paddingTop */ 11,
        /* transfer */ false
      );
      fixedLayer.setup();
    });

    it('should initialize fixed layer to null', () => {
      expect(fixedLayer.transferLayer_).to.be.null;
    });

    it('should discover all potentials', () => {
      function expectFe(actual, expected) {
        expect(actual.id).to.equal(expected.id, `${expected.id} wrong`);
        expect(actual.element).to.equal(
          expected.element,
          `${expected.id}: wrong element`
        );
        expect(actual.position).to.equal(
          expected.position,
          `${expected.id}: wrong position`
        );
        expect(JSON.stringify(actual.selectors)).to.equal(
          JSON.stringify(expected.selectors),
          `${expected.id}: wrong selectors`
        );
      }

      expect(fixedLayer.elements_).to.have.length(5);
      expectFe(fixedLayer.elements_[0], {
        id: 'F0',
        element: element1,
        position: 'fixed',
        selectors: [
          '#amp-custom-rule1',
          '#doc-body-id #amp-custom-rule1',
          '#amp-custom-rule2',
          '#amp-custom-media-rule1',
          '#other-rule1',
        ],
      });
      expectFe(fixedLayer.elements_[1], {
        id: 'F1',
        element: element2,
        position: 'fixed',
        selectors: [
          '#amp-custom-rule2',
          '#amp-custom-supports-rule1',
          '#other-rule2',
        ],
      });
      expectFe(fixedLayer.elements_[2], {
        id: 'F2',
        element: element2,
        position: 'sticky',
        selectors: ['#amp-custom-rule4'],
      });
      expectFe(fixedLayer.elements_[3], {
        id: 'F3',
        element: element4,
        position: 'sticky',
        selectors: ['#amp-custom-rule4'],
      });
      expectFe(fixedLayer.elements_[4], {
        id: 'F4',
        element: element5,
        position: 'sticky',
        selectors: ['#amp-custom-rule5'],
      });
      expect(fixedLayer.isDeclaredFixed(element1)).to.be.true;
      expect(fixedLayer.isDeclaredSticky(element1)).to.be.false;
      expect(fixedLayer.isDeclaredFixed(element2)).to.be.true;
      expect(fixedLayer.isDeclaredSticky(element2)).to.be.true;
      expect(fixedLayer.isDeclaredFixed(element3)).to.be.false;
      expect(fixedLayer.isDeclaredSticky(element3)).to.be.false;
      expect(fixedLayer.isDeclaredSticky(element4)).to.be.true;
      expect(fixedLayer.isDeclaredSticky(element5)).to.be.true;
    });

    it('should add and remove element directly', () => {
      const updateStub = env.sandbox.stub(fixedLayer, 'update');
      expect(fixedLayer.elements_).to.have.length(5);

      // Add.
      fixedLayer.addElement(element6);
      expect(updateStub).to.be.calledOnce;
      expect(fixedLayer.elements_).to.have.length(6);
      const fe = fixedLayer.elements_[5];
      expect(fe.id).to.equal('F5');
      expect(fe.element).to.equal(element6);
      expect(fe.selectors).to.deep.equal(['*']);
      expect(fe.forceTransfer).to.be.undefined;

      // Remove.
      fixedLayer.removeElement(element6);
      expect(fixedLayer.elements_).to.have.length(5);

      // Add with forceTransfer=true.
      fixedLayer.addElement(element6, true);
      expect(updateStub).to.have.callCount(2);
      expect(fixedLayer.elements_).to.have.length(6);
      const fe1 = fixedLayer.elements_[5];
      expect(fe1.id).to.equal('F6');
      expect(fe1.element).to.equal(element6);
      expect(fe1.selectors).to.deep.equal(['*']);
      expect(fe1.forceTransfer).to.be.true;

      // Remove.
      fixedLayer.removeElement(element6);
      expect(fixedLayer.elements_).to.have.length(5);

      // Add with forceTransfer=false.
      fixedLayer.addElement(element6, false);
      expect(updateStub).to.have.callCount(3);
      expect(fixedLayer.elements_).to.have.length(6);
      const fe2 = fixedLayer.elements_[5];
      expect(fe2.id).to.equal('F7');
      expect(fe2.element).to.equal(element6);
      expect(fe2.selectors).to.deep.equal(['*']);
      expect(fe2.forceTransfer).to.be.false;

      // Remove.
      fixedLayer.removeElement(element6);
      expect(fixedLayer.elements_).to.have.length(5);
    });

    it('should remove node when disappeared from DOM', () => {
      docBody.removeChild(element1);
      expect(fixedLayer.elements_).to.have.length(5);
      fixedLayer.update();
      expect(fixedLayer.elements_).to.have.length(4);
    });

    it('should remove all candidates', () => {
      // element2 is both fixed and sticky.
      docBody.removeChild(element2);
      expect(fixedLayer.elements_).to.have.length(5);
      fixedLayer.update();
      expect(fixedLayer.elements_).to.have.length(3);
    });

    it('should collect updates', () => {
      element1.computedStyle['position'] = 'fixed';
      element1.offsetWidth = 10;
      element1.offsetHeight = 10;
      element5.computedStyle['position'] = 'sticky';

      expect(vsyncTasks).to.have.length(1);
      const state = {};
      vsyncTasks[0].measure(state);

      // F0: element1
      expect(state['F0'].fixed).to.be.true;
      expect(state['F0'].sticky).to.be.false;
      expect(state['F0'].top).to.equal('');
      expect(state['F0'].zIndex).to.equal('');

      // F1: element2
      expect(state['F1'].fixed).to.equal(false);
      expect(state['F1'].sticky).to.equal(false);

      // F2: element3
      expect(state['F2'].fixed).to.equal(false);
      expect(state['F2'].sticky).to.equal(false);

      // F3: element4
      expect(state['F3'].fixed).to.be.false;
      expect(state['F3'].sticky).to.be.false;
      expect(state['F3'].top).to.equal('');
      expect(state['F3'].zIndex).to.equal('');

      // F4: element5
      expect(state['F4'].fixed).to.be.false;
      expect(state['F4'].sticky).to.be.true;
      expect(state['F4'].top).to.equal('');
      expect(state['F4'].zIndex).to.equal('');
    });

    it('should support vendor-based sticky', () => {
      element5.computedStyle['position'] = '-webkit-sticky';

      expect(vsyncTasks).to.have.length(1);
      const state = {};
      vsyncTasks[0].measure(state);

      expect(state['F4'].sticky).to.be.true;
    });

    it('should disregard non-fixed position', () => {
      element1.computedStyle['position'] = 'static';
      element1.offsetWidth = 10;
      element1.offsetHeight = 10;
      element5.computedStyle['position'] = 'static';

      expect(vsyncTasks).to.have.length(1);
      const state = {};
      vsyncTasks[0].measure(state);

      expect(state['F0'].fixed).to.be.false;
      expect(state['F1'].fixed).to.be.false;
      expect(state['F4'].fixed).to.be.false;
    });

    it('should disregard invisible element, but for fixed only', () => {
      element1.computedStyle['position'] = 'fixed';
      element1.offsetWidth = 0;
      element1.offsetHeight = 0;
      element5.computedStyle['position'] = 'sticky';
      element5.offsetWidth = 0;
      element5.offsetHeight = 0;

      expect(vsyncTasks).to.have.length(1);
      const state = {};
      vsyncTasks[0].measure(state);

      expect(state['F0'].fixed).to.be.false;
      expect(state['F1'].fixed).to.be.false;
      expect(state['F4'].sticky).to.be.true;
    });

    it('should disregard display:none element', () => {
      element1.computedStyle['position'] = 'fixed';
      element1.offsetWidth = 10;
      element1.offsetHeight = 10;
      element1.computedStyle['display'] = 'none';
      element5.computedStyle['position'] = 'sticky';
      element5.offsetWidth = 10;
      element5.offsetHeight = 10;
      element5.computedStyle['display'] = 'none';

      expect(vsyncTasks).to.have.length(1);
      const state = {};
      vsyncTasks[0].measure(state);

      expect(state['F0'].fixed).to.be.false;
      expect(state['F1'].fixed).to.be.false;
      expect(state['F4'].sticky).to.be.false;
    });

    it('should tolerate getComputedStyle = null', () => {
      // See #3096 and https://bugzilla.mozilla.org/show_bug.cgi?id=548397
      documentApi.defaultView.getComputedStyle = () => null;

      element1.computedStyle['position'] = 'fixed';
      element1.offsetWidth = 10;
      element1.offsetHeight = 10;

      expect(vsyncTasks).to.have.length(1);
      const state = {};
      vsyncTasks[0].measure(state);

      expect(state['F0'].fixed).to.be.false;
      expect(state['F0'].sticky).to.be.false;
      expect(state['F0'].transferrable).to.be.false;
      expect(state['F0'].top).to.equal('');
      expect(state['F0'].zIndex).to.equal('');

      expect(state['F1'].fixed).to.be.false;
      expect(state['F1'].sticky).to.be.false;
    });

    it('should collect for top != auto', () => {
      element1.computedStyle['position'] = 'fixed';
      element1.computedStyle['top'] = '11px';
      element1.offsetWidth = 10;
      element1.offsetHeight = 10;
      element5.computedStyle['position'] = 'sticky';
      element5.computedStyle['top'] = '11px';

      expect(vsyncTasks).to.have.length(1);
      const state = {};
      vsyncTasks[0].measure(state);

      expect(state['F0'].fixed).to.be.true;
      expect(state['F0'].sticky).to.be.false;
      expect(state['F0'].top).to.equal('11px');

      expect(state['F4'].fixed).to.be.false;
      expect(state['F4'].sticky).to.be.true;
      expect(state['F4'].top).to.equal('11px');
    });

    it('should collect for top = auto, but not update top', () => {
      element1.computedStyle['position'] = 'fixed';
      element1.computedStyle['top'] = 'auto';
      element1.offsetWidth = 10;
      element1.offsetHeight = 10;
      element5.computedStyle['position'] = 'sticky';
      element5.computedStyle['top'] = 'auto';

      expect(vsyncTasks).to.have.length(1);
      const state = {};
      vsyncTasks[0].measure(state);

      expect(state['F0'].fixed).to.be.true;
      expect(state['F0'].sticky).to.be.false;
      expect(state['F0'].top).to.equal('');

      expect(state['F4'].fixed).to.be.false;
      expect(state['F4'].sticky).to.be.true;
      expect(state['F4'].top).to.equal('');
    });

    it('should work around top=0 for sticky', () => {
      // See http://crbug.com/703816.
      element5.computedStyle['position'] = 'sticky';
      element5.computedStyle['top'] = '0px';
      element5.autoTop = '12px';

      expect(vsyncTasks).to.have.length(1);
      const state = {};
      vsyncTasks[0].measure(state);

      expect(state['F4'].sticky).to.be.true;
      expect(state['F4'].top).to.equal('');
    });

    it('should work around top=0 for sticky when offset = 0', () => {
      // See http://crbug.com/703816.
      element5.computedStyle['position'] = 'sticky';
      element5.computedStyle['top'] = '0px';
      element5.autoTop = '0px';

      expect(vsyncTasks).to.have.length(1);
      const state = {};
      vsyncTasks[0].measure(state);

      expect(state['F4'].sticky).to.be.true;
      expect(state['F4'].top).to.equal('0px');
    });

    it('should NOT work around top=0 for sticky for non-implicit top', () => {
      // See http://crbug.com/703816.
      element5.computedStyle['position'] = 'sticky';
      element5.computedStyle['top'] = '0px';
      element5.autoTop = '12px';

      expect(vsyncTasks).to.have.length(1);
      const state = {};
      vsyncTasks[0].measure(state);

      expect(state['F4'].sticky).to.be.true;
      expect(state['F4'].top).to.equal('');
    });

    it('should collect for implicit top = auto, but not update top', () => {
      element1.computedStyle['position'] = 'fixed';
      element1.computedStyle['top'] = '0px';
      element1.autoTop = '12px';
      element1.offsetWidth = 10;
      element1.offsetHeight = 10;
      element5.computedStyle['position'] = 'sticky';
      element5.autoTop = '12px';

      expect(vsyncTasks).to.have.length(1);
      const state = {};
      vsyncTasks[0].measure(state);

      expect(state['F0'].fixed).to.be.true;
      expect(state['F0'].top).to.equal('');

      expect(state['F4'].sticky).to.be.true;
      expect(state['F4'].top).to.equal('');
    });

    it('should override implicit top = auto to 0 when equals padding', () => {
      element1.computedStyle['position'] = 'fixed';
      element1.computedStyle['top'] = '11px';
      element1.autoTop = '0px';
      element1.offsetWidth = 10;
      element1.offsetHeight = 10;
      element5.computedStyle['position'] = 'sticky';
      element5.computedStyle['top'] = '11px';
      element5.autoTop = '11px';

      expect(vsyncTasks).to.have.length(1);
      const state = {};
      vsyncTasks[0].measure(state);

      expect(state['F0'].fixed).to.be.true;
      expect(state['F0'].top).to.equal('0px');

      expect(state['F4'].sticky).to.be.true;
      expect(state['F4'].top).to.equal('11px');
    });

    it('should override implicit top = auto to 0 and padding + border', () => {
      fixedLayer.borderTop_ = 1;
      element1.computedStyle['position'] = 'fixed';
      element1.computedStyle['top'] = '12px';
      element1.autoTop = '0px';
      element1.offsetWidth = 10;
      element1.offsetHeight = 10;
      element5.computedStyle['position'] = 'sticky';
      element5.computedStyle['top'] = '12px';
      element5.autoTop = '12px';

      expect(vsyncTasks).to.have.length(1);
      const state = {};
      vsyncTasks[0].measure(state);

      expect(state['F0'].fixed).to.be.true;
      expect(state['F0'].top).to.equal('0px');

      expect(state['F4'].sticky).to.be.true;
      expect(state['F4'].top).to.equal('12px');
    });

    it('should override implicit top = auto to 0 w/transient padding', () => {
      element1.computedStyle['position'] = 'fixed';
      element1.computedStyle['top'] = '11px';
      element1.autoTop = '0px';
      element1.offsetWidth = 10;
      element1.offsetHeight = 10;
      element5.computedStyle['position'] = 'sticky';
      element5.computedStyle['top'] = '11px';
      element5.autoTop = '11px';

      expect(vsyncTasks).to.have.length(1);
      const state = {};
      vsyncTasks[0].measure(state);

      expect(state['F0'].fixed).to.equal(true);
      expect(state['F0'].top).to.equal('0px');

      // Update to transient padding.
      env.sandbox.stub(fixedLayer, 'update').callsFake(() => {});
      fixedLayer.updatePaddingTop(22, /* transient */ true);
      vsyncTasks[0].measure(state);
      expect(state['F0'].fixed).to.be.true;
      expect(state['F0'].top).to.equal('0px');
      expect(state['F4'].sticky).to.be.true;
      expect(state['F4'].top).to.equal('11px');
      expect(fixedLayer.paddingTop_).to.equal(22);
      expect(fixedLayer.committedPaddingTop_).to.equal(11);

      // Update to non-transient padding.
      fixedLayer.updatePaddingTop(22, /* transient */ false);
      vsyncTasks[0].measure(state);
      expect(state['F0'].fixed).to.be.true;
      expect(state['F0'].top).to.equal(''); // Reset completely.
      expect(state['F4'].sticky).to.be.true;
      expect(state['F4'].top).to.equal('11px');
      expect(fixedLayer.paddingTop_).to.equal(22);
      expect(fixedLayer.committedPaddingTop_).to.equal(22);
    });

    it('should always collect and update top = 0', () => {
      element1.computedStyle['position'] = 'fixed';
      element1.computedStyle['top'] = '0px';
      element1.autoTop = '0px';
      element1.offsetWidth = 10;
      element1.offsetHeight = 10;
      element5.computedStyle['position'] = 'sticky';
      element5.computedStyle['top'] = '0px';
      element5.autoTop = '0px';

      expect(vsyncTasks).to.have.length(1);
      const state = {};
      vsyncTasks[0].measure(state);

      expect(state['F0'].fixed).to.be.true;
      expect(state['F0'].top).to.equal('0px');

      expect(state['F4'].sticky).to.be.true;
      expect(state['F4'].top).to.equal('0px');
    });

    it('should handle transitions', () => {
      element1.computedStyle['position'] = 'fixed';
      element1.computedStyle['transition'] = 'all .4s ease';
      element1.computedStyle['top'] = '0px';
      element1.autoTop = '0px';
      element1.offsetWidth = 10;
      element1.offsetHeight = 10;
      element5.computedStyle['position'] = 'sticky';
      element5.computedStyle['transition'] = 'all .4s ease';
      element5.computedStyle['top'] = '0px';
      element5.autoTop = '0px';

      expect(vsyncTasks).to.have.length(1);
      const state = {};
      vsyncTasks[0].measure(state);

      expect(state['F0'].fixed).to.be.true;
      expect(state['F0'].top).to.equal('0px');
      expect(element1.style.transition).to.equal('none !important');

      expect(state['F4'].sticky).to.be.true;
      expect(state['F4'].top).to.equal('0px');
      expect(element5.style.transition).to.equal('none !important');

      vsyncTasks[0].mutate({});
      expect(element1.style.transition).to.equal('');
      expect(element5.style.transition).to.equal('');
    });

    it('should mutate element to fixed without top', () => {
      const fe = fixedLayer.elements_[0];
      fixedLayer.mutateElement_(fe, 1, {
        fixed: true,
        sticky: false,
        top: '',
      });

      expect(fe.fixedNow).to.be.true;
      expect(fe.stickyNow).to.be.false;
      expect(fe.element.style.top).to.equal('15px');
      expect(fixedLayer.transferLayer_).to.be.null;
    });

    it('should mutate element to sticky without top', () => {
      const fe = fixedLayer.elements_[4];
      fixedLayer.mutateElement_(fe, 1, {
        fixed: false,
        sticky: true,
        top: '',
      });

      expect(fe.fixedNow).to.be.false;
      expect(fe.stickyNow).to.be.true;
      expect(fe.element.style.top).to.equal('15px');
      expect(fixedLayer.transferLayer_).to.be.null;
    });

    it('should mutate element to fixed with top', () => {
      const fe = fixedLayer.elements_[0];
      element1.style.top = '';
      fixedLayer.mutateElement_(fe, 1, {
        fixed: true,
        top: '17px',
      });

      expect(fe.fixedNow).to.be.true;
      expect(fe.element.style.top).to.equal('calc(17px + 11px)');
    });

    it('should add needed padding to sticky top if transferring', () => {
      const fe = fixedLayer.elements_[4];
      fixedLayer.transfer_ = true;
      fe.element.style.top = '';
      fixedLayer.mutateElement_(fe, 1, {
        sticky: true,
        top: '17px',
      });

      expect(fe.stickyNow).to.be.true;
      expect(fe.element.style.top).to.equal('17px');
    });

    it('should not add unneeded padding to sticky top if transferring', () => {
      const fe = fixedLayer.elements_[4];
      fixedLayer.transfer_ = true;
      fixedLayer.paddingTop_ = 0;
      fe.element.style.top = '';
      fixedLayer.mutateElement_(fe, 1, {
        sticky: true,
        top: '17px',
      });

      expect(fe.stickyNow).to.be.true;
      expect(fe.element.style.top).to.equal('calc(17px - 11px)');
    });

    it('should mutate element to sticky with top', () => {
      const fe = fixedLayer.elements_[4];
      fixedLayer.mutateElement_(fe, 1, {
        sticky: true,
        top: '17px',
      });

      expect(fe.stickyNow).to.be.true;
      expect(fe.element.style.top).to.equal('calc(17px + 11px)');
    });

    it('should reset top upon being removed from fixedlayer', () => {
      expect(fixedLayer.elements_).to.have.length(5);

      // Add.
      fixedLayer.addElement(element6, '*');
      expect(fixedLayer.elements_).to.have.length(6);
      const fe = fixedLayer.elements_[5];
      expect(fe.id).to.equal('F5');
      expect(fe.element).to.equal(element6);
      expect(fe.selectors).to.deep.equal(['*']);
      fixedLayer.mutateElement_(fe, 1, {
        fixed: true,
        top: '17px',
      });

      expect(fe.fixedNow).to.be.true;
      expect(fe.element.style.top).to.equal('calc(17px + 11px)');
      // Remove.
      fixedLayer.vsync_ = {
        mutate(callback) {
          callback();
        },
      };
      fixedLayer.removeElement(element6);
      expect(fixedLayer.elements_).to.have.length(5);
      expect(element6.style.top).to.equal('');
    });

    it('should reset sticky top upon being removed from fixedlayer', () => {
      expect(fixedLayer.elements_).to.have.length(5);

      const fe = fixedLayer.elements_[4];
      fixedLayer.mutateElement_(fe, 1, {
        sticky: true,
        top: '17px',
      });

      expect(fe.stickyNow).to.be.true;
      expect(fe.element.style.top).to.equal('calc(17px + 11px)');

      // Remove.
      fixedLayer.vsync_ = {
        mutate(callback) {
          callback();
        },
      };
      fixedLayer.removeElement(element5);
      expect(fixedLayer.elements_).to.have.length(4);
      expect(element5.style.top).to.equal('');
    });

    it('should transform fixed elements with anchored top', () => {
      const fe = fixedLayer.elements_[0];
      fixedLayer.mutateElement_(fe, 1, {
        fixed: true,
        top: '17px',
      });
      expect(fe.fixedNow).to.be.true;
      expect(fe.element.style.top).to.equal('calc(17px + 11px)');

      fixedLayer.transformMutate('translateY(-10px)');
      expect(fe.element.style.transform).to.equal('translateY(-10px)');
      expect(fe.element.style.transition).to.equal('none');

      // Reset back.
      fixedLayer.transformMutate(null);
      expect(fe.element.style.transform).to.equal('');
      expect(fe.element.style.transition).to.equal('');
    });

    it('should NOT transform sticky elements with anchored top', () => {
      const fe = fixedLayer.elements_[4];
      fixedLayer.mutateElement_(fe, 1, {
        sticky: true,
        top: '17px',
      });
      expect(fe.stickyNow).to.be.true;
      expect(fe.element.style.top).to.equal('calc(17px + 11px)');

      fixedLayer.transformMutate('translateY(-10px)');
      expect(fe.element.style.transform).to.be.undefined;
      expect(fe.element.style.transition).to.equal('');

      // Reset back.
      fixedLayer.transformMutate(null);
      expect(fe.element.style.transform).to.be.undefined;
      expect(fe.element.style.transition).to.equal('');
    });

    it('should compound transform with anchored top', () => {
      const fe = fixedLayer.elements_[0];
      fixedLayer.mutateElement_(fe, 1, {
        fixed: true,
        top: '17px',
        transform: 'scale(2)',
      });

      fixedLayer.transformMutate('translateY(-10px)');
      expect(fe.element.style.transform).to.equal('scale(2) translateY(-10px)');
    });

    it('should NOT transform fixed elements w/o anchored top', () => {
      const fe = fixedLayer.elements_[0];
      fe.element.style.transform = '';
      fixedLayer.mutateElement_(fe, 1, {
        fixed: true,
        top: '',
      });
      expect(fe.fixedNow).to.be.true;

      fixedLayer.transformMutate('translateY(-10px)');
      expect(fe.element.style.transform).to.equal('');
    });

    it('should user error when inline styles may be overriden', () => {
      // Set both attribute and property since element1 is a fake element.
      element1.setAttribute('style', 'bottom: 10px');
      element1.style.bottom = '10px';

      const userError = env.sandbox.stub(user(), 'error');
      fixedLayer.setup();
      // Expect error regarding inline styles.
      expect(userError).calledWithMatch(
        'FixedLayer',
        /not supported yet for fixed or sticky elements/
      );
    });

    describe('hidden toggle', () => {
      let mutationObserver;
      beforeEach(() => {
        fixedLayer.observeHiddenMutations();
        mutationObserver = Services.hiddenObserverForDoc(
          documentApi.documentElement
        ).mutationObserver_;
      });

      it('should trigger an update', () => {
        element1.computedStyle['position'] = 'fixed';
        element1.offsetWidth = 10;
        element1.offsetHeight = 10;
        element1.computedStyle['display'] = 'none';

        expect(vsyncTasks).to.have.length(1);
        const state = {};
        vsyncTasks[0].measure(state);

        expect(state['F0'].fixed).to.be.false;

        element1.computedStyle['display'] = '';

        env.sandbox.stub(timer, 'delay').callsFake((callback) => {
          callback();
        });
        return mutationObserver
          .__mutate({
            attributeName: 'hidden',
          })
          .then(() => {
            expect(vsyncTasks).to.have.length(2);
            const state = {};
            vsyncTasks[0].measure(state);

            expect(state['F0'].fixed).to.be.true;
            expect(state['F0'].top).to.equal('');
            expect(state['F0'].zIndex).to.equal('');
          });
      });
    });

    it('should ignore descendants of already-tracked elements', () => {
      const updateStub = env.sandbox.stub(fixedLayer, 'update');
      expect(fixedLayer.elements_).to.have.length(5);

      element1.appendChild(element6);

      // Add.
      fixedLayer.addElement(element6);
      expect(updateStub).not.to.have.been.called;
      expect(fixedLayer.elements_).to.have.length(5);
    });

    it('should replace descendants of tracked elements', () => {
      const updateStub = env.sandbox.stub(fixedLayer, 'update');
      expect(fixedLayer.elements_).to.have.length(5);

      element6.appendChild(element1);

      // Add.
      fixedLayer.addElement(element6);
      expect(updateStub).to.be.calledOnce;
      expect(fixedLayer.elements_).to.have.length(5);

      const fe = fixedLayer.elements_[4];
      expect(fe.id).to.equal('F5');
      expect(fe.element).to.equal(element6);
      expect(fe.selectors).to.deep.equal(['*']);
      expect(fe.forceTransfer).to.be.undefined;
    });
  });

  describe('with-transfer', () => {
    let fixedLayer;

    const borderTop = 0;
    const paddingTop = 11;
    const transfer = true;

    beforeEach(() => {
      installViewerServiceForDoc(ampdoc);
      viewer = Services.viewerForDoc(ampdoc);
      viewer.isEmbedded = () => true;

      fixedLayer = new FixedLayer(
        ampdoc,
        vsyncApi,
        borderTop,
        paddingTop,
        transfer
      );
      fixedLayer.setup();
    });

    it('should not instantiate transfer layer on setup', () => {
      const win = new FakeWindow();
      const ampdoc = new AmpDocSingle(win);
      installPlatformService(win);
      installTimerService(win);
      installViewerServiceForDoc(ampdoc);
      viewer = Services.viewerForDoc(ampdoc);
      viewer.isEmbedded = () => true;

      const fixedLayer = new FixedLayer(
        ampdoc,
        vsyncApi,
        borderTop,
        paddingTop,
        transfer
      );
      fixedLayer.setup();
      expect(fixedLayer.transferLayer_).to.be.null;
    });

    it('should collect turn off transferrable', () => {
      element1.computedStyle['position'] = 'fixed';
      element1.offsetWidth = 10;
      element1.offsetHeight = 10;
      element5.computedStyle['position'] = 'sticky';

      expect(vsyncTasks).to.have.length(1);
      const state = {};
      vsyncTasks[0].measure(state);

      expect(state['F0'].fixed).to.be.true;
      expect(state['F0'].transferrable).to.be.false;

      expect(state['F1'].fixed).to.equal(false);

      expect(state['F4'].sticky).to.be.true;
      expect(state['F4'].transferrable).to.be.false;
    });

    it('should collect turn on transferrable with top = 0', () => {
      element1.computedStyle['position'] = 'fixed';
      element1.offsetWidth = 10;
      element1.offsetHeight = 10;
      element1.computedStyle['top'] = '0px';
      element5.computedStyle['position'] = 'sticky';
      element5.computedStyle['top'] = '0px';

      expect(vsyncTasks).to.have.length(1);
      const state = {};
      vsyncTasks[0].measure(state);

      expect(state['F0'].fixed).to.be.true;
      expect(state['F0'].transferrable).to.be.true;
      expect(state['F0'].top).to.equal('0px');

      expect(state['F4'].sticky).to.be.true;
      expect(state['F4'].transferrable).to.be.false;
      expect(state['F4'].top).to.equal('0px');
    });

    it('should collect turn on transferrable with top != 0', () => {
      element1.computedStyle['position'] = 'fixed';
      element1.offsetWidth = 10;
      element1.offsetHeight = 10;
      element1.computedStyle['top'] = '2px';
      element5.computedStyle['position'] = 'sticky';
      element5.computedStyle['top'] = '2px';

      expect(vsyncTasks).to.have.length(1);
      const state = {};
      vsyncTasks[0].measure(state);

      expect(state['F0'].fixed).to.be.true;
      expect(state['F0'].transferrable).to.be.true;
      expect(state['F0'].top).to.equal('2px');

      expect(state['F4'].sticky).to.be.true;
      expect(state['F4'].transferrable).to.be.false;
      expect(state['F4'].top).to.equal('2px');
    });

    it('should collect turn on transferrable with bottom = 0', () => {
      element1.computedStyle['position'] = 'fixed';
      element1.offsetWidth = 10;
      element1.offsetHeight = 10;
      element1.computedStyle['top'] = '';
      element1.computedStyle['bottom'] = '0px';
      element5.computedStyle['position'] = 'sticky';
      element5.computedStyle['top'] = '';
      element5.computedStyle['bottom'] = '0px';

      expect(vsyncTasks).to.have.length(1);
      const state = {};
      vsyncTasks[0].measure(state);

      expect(state['F0'].fixed).to.be.true;
      expect(state['F0'].transferrable).to.be.true;

      expect(state['F4'].sticky).to.be.true;
      expect(state['F4'].transferrable).to.be.false;
    });

    it('should not disregard invisible element if it has forceTransfer', () => {
      element1.computedStyle['position'] = 'fixed';
      element1.offsetWidth = 0;
      element1.offsetHeight = 0;
      element5.computedStyle['position'] = 'sticky';

      expect(vsyncTasks).to.have.length(1);
      let state = {};
      vsyncTasks[0].measure(state);
      expect(state['F0'].fixed).to.be.false;
      expect(state['F0'].transferrable).to.be.false;
      expect(state['F4'].sticky).to.be.true;
      expect(state['F4'].transferrable).to.be.false;

      // Add.
      state = {};
      fixedLayer.setupElement_(element1, '*', 'fixed', true);
      expect(vsyncTasks).to.have.length(1);
      vsyncTasks[0].measure(state);

      expect(state['F0'].fixed).to.be.true;
      expect(state['F0'].transferrable).to.be.true;
    });

    it('should disregard element if it has forceTransfer=false', () => {
      element1.computedStyle['position'] = 'fixed';
      element1.offsetWidth = 10;
      element1.offsetHeight = 10;
      element1.computedStyle['top'] = '0px';
      element5.computedStyle['position'] = 'sticky';

      expect(vsyncTasks).to.have.length(1);
      let state = {};
      vsyncTasks[0].measure(state);
      expect(state['F0'].fixed).to.be.true;
      expect(state['F0'].transferrable).to.be.true;
      expect(state['F4'].sticky).to.be.true;
      expect(state['F4'].transferrable).to.be.false;

      // Add.
      state = {};
      fixedLayer.setupElement_(element1, '*', 'fixed', false);
      expect(vsyncTasks).to.have.length(1);
      vsyncTasks[0].measure(state);

      expect(state['F0'].fixed).to.be.true;
      expect(state['F0'].transferrable).to.be.false;
    });

    it('should collect turn on transferrable with bottom != 0', () => {
      element1.computedStyle['position'] = 'fixed';
      element1.offsetWidth = 10;
      element1.offsetHeight = 10;
      element1.style['top'] = '';
      element1.computedStyle['top'] = '';
      element1.computedStyle['bottom'] = '2px';
      element5.computedStyle['position'] = 'sticky';
      element5.style['top'] = '';
      element5.computedStyle['top'] = '';
      element5.computedStyle['bottom'] = '2px';

      expect(vsyncTasks).to.have.length(1);
      const state = {};
      vsyncTasks[0].measure(state);

      expect(state['F0'].fixed).to.be.true;
      expect(state['F0'].transferrable).to.be.true;

      expect(state['F4'].sticky).to.be.true;
      expect(state['F4'].transferrable).to.be.false;
    });

    it('should collect z-index', () => {
      element1.computedStyle['position'] = 'fixed';
      element1.offsetWidth = 10;
      element1.offsetHeight = 10;
      element1.computedStyle['zIndex'] = '101';

      expect(vsyncTasks).to.have.length(1);
      const state = {};
      vsyncTasks[0].measure(state);

      expect(state['F0'].fixed).to.be.true;
      expect(state['F0'].zIndex).to.equal('101');
    });

    it('should transfer element', () => {
      const fe = fixedLayer.elements_[0];
      fixedLayer.mutateElement_(fe, 1, {
        fixed: true,
        transferrable: true,
        zIndex: '11',
      });

      expect(fe.fixedNow).to.be.true;
      expect(fe.placeholder).to.exist;
      expect(fe.placeholder).to.have.attribute('hidden');

      expect(fixedLayer.transferLayer_).to.exist;
      const layer = fixedLayer.transferLayer_.layer_;
      expect(layer.style.pointerEvents).to.equal('none');

      expect(fe.element.parentElement).to.equal(layer);
      expect(fe.element.style.pointerEvents).to.equal('initial');
      expect(fe.element.style.zIndex).to.equal('calc(10001 + 11)');
    });

    it('should ignore transfer when non-transferrable', () => {
      const fe = fixedLayer.elements_[0];
      fixedLayer.mutateElement_(fe, 1, {
        fixed: true,
        transferrable: false,
      });

      expect(fe.fixedNow).to.be.true;
      expect(fe.placeholder).to.not.exist;
      expect(fixedLayer.transferLayer_).to.not.exist;
    });

    it('should return transfered element if it no longer matches', () => {
      const fe = fixedLayer.elements_[0];
      fe.element.matches = () => false;
      fixedLayer.mutateElement_(fe, 1, {
        fixed: true,
        transferrable: true,
        zIndex: '11',
      });

      expect(fe.fixedNow).to.be.true;
      expect(fe.placeholder).to.exist;
      expect(fixedLayer.transferLayer_).to.exist;
      expect(fe.element.parentElement).to.not.equal(
        fixedLayer.transferLayer_.layer_
      );
      expect(fe.placeholder.parentElement).to.be.null;
      expect(fe.element.style.zIndex).to.equal('');
    });

    it('should remove transfered element if it no longer exists', () => {
      const fe = fixedLayer.elements_[0];

      // Add.
      fixedLayer.mutateElement_(fe, 1, {
        fixed: true,
        transferrable: true,
        zIndex: '11',
      });
      expect(fe.fixedNow).to.be.true;
      expect(fe.placeholder).to.exist;
      expect(fe.element.parentElement).to.equal(
        fixedLayer.transferLayer_.layer_
      );
      expect(fixedLayer.transferLayer_).to.exist;
      expect(fixedLayer.transferLayer_.layer_.id).to.equal('doc-body-id');

      // Remove from DOM.
      fe.element.parentElement.removeChild(fe.element);
      fixedLayer.mutateElement_(fe, 1, {
        fixed: false,
        transferrable: false,
      });
      expect(fe.fixedNow).to.be.false;
      expect(fe.placeholder.parentElement).to.not.exist;
    });

    it('should disregard transparent element', () => {
      element1.computedStyle['position'] = 'fixed';
      element1.offsetWidth = 10;
      element1.offsetHeight = 10;
      element1.computedStyle['top'] = '0px';
      element1.computedStyle['opacity'] = '0';

      expect(vsyncTasks).to.have.length(1);
      const state = {};
      vsyncTasks[0].measure(state);

      expect(state['F0'].fixed).to.equal(true);
      expect(state['F0'].transferrable).to.equal(false);
    });

    it('should force transfer for visibility=hidden element', () => {
      element1.computedStyle['position'] = 'fixed';
      element1.offsetWidth = 10;
      element1.offsetHeight = 10;
      element1.computedStyle['top'] = '0px';
      element1.computedStyle['visibility'] = 'hidden';

      expect(vsyncTasks).to.have.length(1);
      const state = {};
      vsyncTasks[0].measure(state);

      expect(state['F0'].fixed).to.equal(true);
      expect(state['F0'].transferrable).to.equal(true);
    });

    it('should user error when inline styles may be overriden', () => {
      // Set both attribute and property since element1 is a fake element.
      element1.setAttribute('style', 'bottom: 10px');
      element1.style.bottom = '10px';

      const userError = env.sandbox.stub(user(), 'error');
      fixedLayer.setup();
      // Expect error regarding inline styles.
      expect(userError).calledWithMatch(
        'FixedLayer',
        /not supported yet for fixed or sticky elements/
      );
    });

    describe('hidden toggle', () => {
      let mutationObserver;
      beforeEach(() => {
        fixedLayer.observeHiddenMutations();
        mutationObserver = Services.hiddenObserverForDoc(
          documentApi.documentElement
        ).mutationObserver_;
      });

      it('should trigger an update', () => {
        element1.computedStyle['position'] = 'fixed';
        element1.offsetWidth = 10;
        element1.offsetHeight = 10;
        element1.computedStyle['display'] = 'none';

        expect(vsyncTasks).to.have.length(1);
        const state = {};
        vsyncTasks[0].measure(state);

        expect(state['F0'].fixed).to.be.false;

        element1.computedStyle['display'] = '';

        env.sandbox.stub(timer, 'delay').callsFake((callback) => {
          callback();
        });
        return mutationObserver
          .__mutate({
            attributeName: 'hidden',
          })
          .then(() => {
            expect(vsyncTasks).to.have.length(2);
            const state = {};
            vsyncTasks[0].measure(state);

            expect(state['F0'].fixed).to.be.true;
            expect(state['F0'].top).to.equal('');
            expect(state['F0'].zIndex).to.equal('');
          });
      });
    });

    it('should sync attributes between body and layer', () => {
      const {body} = ampdoc.win.document;

      // Necessary to create the fixed layer
      element1.computedStyle['position'] = 'fixed';
      element1.offsetWidth = 10;
      element1.offsetHeight = 10;
      element1.computedStyle['top'] = '0px';
      fixedLayer.vsync_ = {
        runPromise({measure, mutate}, state) {
          measure(state);
          mutate(state);
          return Promise.resolve();
        },
      };

      body.setAttribute('test', 'hello');
      fixedLayer.update();
      expect(fixedLayer.transferLayer_).to.exist;
      const layer = fixedLayer.transferLayer_.layer_;
      expect(layer.getAttribute('test')).to.equal('hello');

      body.removeAttribute('test');
      body.setAttribute('test1', 'hello1');
      body.setAttribute('test2', 'hello2');
      fixedLayer.update();
      expect(layer.getAttribute('test')).to.equal(null);
      expect(layer.getAttribute('test1')).to.equal('hello1');
      expect(layer.getAttribute('test2')).to.equal('hello2');

      body.removeAttribute('test1');
      body.removeAttribute('test2');
      fixedLayer.update();
      expect(layer.getAttribute('test')).to.equal(null);
      expect(layer.getAttribute('test1')).to.equal(null);
      expect(layer.getAttribute('test2')).to.equal(null);

      body.attributes.push(new FakeAttr('[class]', 'amp-bind'));
      fixedLayer.update();
      expect(layer.getAttribute('[class]')).to.equal('amp-bind');

      // [i-amphtml-lightbox] is an exception and should be preserved.
      layer.setAttribute('i-amphtml-lightbox', '');
      fixedLayer.update();
      expect(layer.hasAttribute('i-amphtml-lightbox')).to.be.true;
      expect(body.hasAttribute('i-amphtml-lightbox')).to.be.false;
    });

    it('should sync invalid-named attributes to layer', () => {
      // Holy poop it's hard to inject an invalid-named attribute.
      // This is a sanity check using real Elements to ensure we can insert
      // a cloned attribute into another element.
      let div = document.createElement('div');
      div.innerHTML = '<div [class]="amp-bind"></div>';
      div = div.firstElementChild;
      const attr = div.attributes[0];
      expect(attr.name).to.equal('[class]');
      expect(attr.value).to.equal('amp-bind');

      const body = document.createElement('body');
      body.attributes.setNamedItem(attr.cloneNode(false));

      expect(div.getAttribute('[class]')).to.equal('amp-bind');
      expect(body.getAttribute('[class]')).to.equal('amp-bind');
      expect(body.attributes.getNamedItem('[class]')).to.not.equal(
        div.attributes.getNamedItem('[class]')
      );
    });
  });
});

describes.sandboxed('FixedLayer Setup Execution Bailouts', {}, () => {
  let win;
  let ampdoc;
  let viewer;
  let vsyncApi;

  beforeEach(() => {
    win = new FakeWindow();
    window.__AMP_MODE = {
      localDev: false,
      development: false,
      test: false,
      version: '$internalRuntimeVersion$',
    };

    const vsyncTasks = [];
    vsyncApi = {
      runPromise: (task) => {
        vsyncTasks.push(task);
        return Promise.resolve();
      },
      mutate: (mutator) => {
        vsyncTasks.push({mutate: mutator});
      },
    };
  });

  it('should not perform setup when served canonically', () => {
    ampdoc = new AmpDocSingle(win);
    installPlatformService(win);
    installTimerService(win);
    installViewerServiceForDoc(ampdoc);
    viewer = Services.viewerForDoc(ampdoc);
    viewer.isEmbedded = () => false;

    const fixedLayer = new FixedLayer(
      ampdoc,
      vsyncApi,
      /* borderTop */ 0,
      /* paddingTop */ 11,
      /* transfer */ false
    );
    const executed = fixedLayer.setup();
    expect(executed).to.be.false;
  });

  it('should perform setup when served within a viewer', () => {
    ampdoc = new AmpDocSingle(win);
    installPlatformService(win);
    installTimerService(win);
    installViewerServiceForDoc(ampdoc);
    viewer = Services.viewerForDoc(ampdoc);
    viewer.isEmbedded = () => true;

    const fixedLayer = new FixedLayer(
      ampdoc,
      vsyncApi,
      /* borderTop */ 0,
      /* paddingTop */ 11,
      /* transfer */ false
    );
    const executed = fixedLayer.setup();
    expect(executed).to.be.true;
  });
});

describes.sandboxed(
  'FixedLayer Setup Execution Bailouts with Local Development',
  {},
  () => {
    let win;
    let ampdoc;
    let viewer;
    let vsyncApi;

    beforeEach(() => {
      win = new FakeWindow();
      window.__AMP_MODE = {
        localDev: true,
        development: false,
        test: false,
        version: '$internalRuntimeVersion$',
      };

      const vsyncTasks = [];
      vsyncApi = {
        runPromise: (task) => {
          vsyncTasks.push(task);
          return Promise.resolve();
        },
        mutate: (mutator) => {
          vsyncTasks.push({mutate: mutator});
        },
      };
    });

    it('should perform setup when served canonically', () => {
      ampdoc = new AmpDocSingle(win);
      installPlatformService(win);
      installTimerService(win);
      installViewerServiceForDoc(ampdoc);
      viewer = Services.viewerForDoc(ampdoc);
      viewer.isEmbedded = () => false;

      const fixedLayer = new FixedLayer(
        ampdoc,
        vsyncApi,
        /* borderTop */ 0,
        /* paddingTop */ 11,
        /* transfer */ false
      );
      const executed = fixedLayer.setup();
      expect(executed).to.be.true;
    });

    it('should perform setup when served within a viewer', () => {
      ampdoc = new AmpDocSingle(win);
      installPlatformService(win);
      installTimerService(win);
      installViewerServiceForDoc(ampdoc);
      viewer = Services.viewerForDoc(ampdoc);
      viewer.isEmbedded = () => true;

      const fixedLayer = new FixedLayer(
        ampdoc,
        vsyncApi,
        /* borderTop */ 0,
        /* paddingTop */ 11,
        /* transfer */ false
      );
      const executed = fixedLayer.setup();
      expect(executed).to.be.true;
    });
  }
);

describes.sandboxed(
  'FixedLayer bug due to calling browser native instead of AMP implementation',
  {},
  (env) => {
    let win;
    let ampdoc;
    let viewer;
    let vsyncApi;

    beforeEach(() => {
      win = new FakeWindow();
      window.__AMP_MODE = {
        localDev: true,
        development: false,
        test: false,
        version: '$internalRuntimeVersion$',
      };

      const vsyncTasks = [];
      vsyncApi = {
        runPromise: async (task) => {
          vsyncTasks.push(task);
        },
        mutate: (mutator) => {
          vsyncTasks.push({mutate: mutator});
        },
      };
    });

    it('should call Animation.animate in animateFixedElements', () => {
      const animateSpy = env.sandbox.spy(Animation, 'animate');

      ampdoc = new AmpDocSingle(win);
      installPlatformService(win);
      installTimerService(win);
      installViewerServiceForDoc(ampdoc);
      viewer = Services.viewerForDoc(ampdoc);
      viewer.isEmbedded = () => true;

      const fixedLayer = new FixedLayer(
        ampdoc,
        vsyncApi,
        /* borderTop */ 0,
        /* paddingTop */ 11,
        /* transfer */ false
      );
      fixedLayer.animateFixedElements(123, 456, 789, 'ease-in', false);
      expect(animateSpy).to.be.calledOnce;
    });
  }
);
