// eslint-disable-next-line @typescript-eslint/triple-slash-reference
/// <reference path="../../../../test/src/custom_typings/chai.d.ts" />
/* eslint-disable no-undef */
import { ZuiFlyoutElement } from '@zywave/zui-flyout';
import { assert } from '@esm-bundle/chai';
import { executeServerCommand } from '@web/test-runner-commands';

// Note: this is a dangerous override, in that we're not implementing all of the ElementInternals API.
// This works, because we simply set aria properties in ZuiFlyoutElement
const internalsLookup: WeakMap<ZuiFlyoutElement, ElementInternalsLike> = new WeakMap();
ZuiFlyoutElement.prototype.attachInternals = function () {
  const internals: ElementInternalsLike = {};
  internalsLookup.set(this, internals);
  return internals as ElementInternals;
};

/**
 * IMPORTANT: Tests all have "await element.updateComplete;" as the first line of the test body intentionally
 * This is to enable other tests for issues where a flyout being created in an open state would fail to render correctly / modify external DOM
 */

suite('zui-flyout', () => {
  let element: ZuiFlyoutElement;

  setup(async () => {
    element = document.createElement('zui-flyout');
    document.body.appendChild(element);
  });

  teardown(() => {
    document.body.removeChild(element);
  });

  test('initializes as a ZuiFlyoutElement', () => {
    assert.instanceOf(element, ZuiFlyoutElement);
  });

  test('defaults', async () => {
    await element.updateComplete;

    assert.equal(element.mode, 'overlay', 'mode should default to "overlay"');
    assert.equal(element.opened, false, 'open should default to false');
    assert.equal(element.contentSelector, null, 'contentSelector should default to null');
    assert.equal(element.expanded, false, 'expanded should default to false');

    const internals = internalsLookup.get(element) as ElementInternalsLike;
    assert.equal(internals.role, 'dialog', 'role should be "dialog"');
    assert.equal(internals.ariaModal, 'true', 'aria-modal should be "true"');
    assert.equal(internals.ariaHidden, 'true', 'aria-hidden should be "true"');
  });

  test('opened modifies body overflow', async () => {
    await element.updateComplete;

    element.opened = true;
    await element.updateComplete;

    const body = document.body;
    assert.equal(body.style.overflow, 'hidden', 'body overflow should be hidden when flyout is opened');
  });

  test('closed resets body overflow', async () => {
    await element.updateComplete;
    element.opened = true;
    await element.updateComplete;

    element.opened = false;
    await element.updateComplete;

    const body = document.body;
    assert.equal(body.style.overflow, '', 'body overflow should be empty string when flyout is closed');
  });

  // This technically works, but breaks other tests because other flyouts now have a different body overflow value cached than expected
  test.skip('closed resets body overflow to original value', async () => {
    await element.updateComplete;
    const body = document.body;
    body.style.overflow = 'scroll';

    element.opened = true;
    await element.updateComplete;

    element.opened = false;
    await element.updateComplete;

    assert.equal(
      body.style.overflow,
      'scroll',
      'body overflow should be reset to original value when flyout is closed'
    );

    body.style.overflow = '';
  });

  suite('[mode="overlay"]', () => {
    setup(() => {
      element.mode = 'overlay';
    });

    test('a11y', async () => {
      await element.updateComplete;

      const internals = internalsLookup.get(element) as ElementInternalsLike;

      assert.equal(internals.role, 'dialog', 'role should be "dialog"');
      assert.equal(internals.ariaModal, 'true', 'aria-modal should be "true"');
      assert.equal(internals.ariaHidden, 'true', 'aria-hidden should be "true"');
    });

    test('closed flyout renders a closed dialog', async () => {
      await element.updateComplete;

      element.opened = false;
      await element.updateComplete;
      const dialog = element.shadowRoot?.querySelector('dialog');
      assert.exists(dialog, 'dialog should be rendered');
      assert.isFalse(dialog?.hasAttribute('open'), 'dialog should be closed by default');

      const internals = internalsLookup.get(element) as ElementInternalsLike;
      assert.equal(internals.ariaHidden, 'true', 'aria-hidden should be "true" when closed');

      const body = document.body;
      assert.equal(body.style.overflow, '', 'body overflow should be empty string when flyout is closed');
      assert.isFalse(
        body.classList.contains('zui-flyout-inline-opened'),
        'body should not have class when flyout is closed'
      );
    });

    test('opened flyout renders an open dialog', async () => {
      await element.updateComplete;

      element.opened = true;
      await element.updateComplete;
      const dialog = element.shadowRoot?.querySelector('dialog');
      assert.exists(dialog, 'dialog should be rendered');
      assert.isTrue(dialog?.hasAttribute('open'), 'dialog should be open');

      const internals = internalsLookup.get(element) as ElementInternalsLike;
      assert.equal(internals.ariaHidden, 'false', 'aria-hidden should be "false" when opened');

      const body = document.body;
      assert.equal(body.style.overflow, 'hidden', 'body overflow should be hidden when flyout is opened');
      assert.isFalse(
        body.classList.contains('zui-flyout-inline-opened'),
        'body should not have class when flyout is opened'
      );
    });

    test('calling open renders an open dialog', async () => {
      await element.updateComplete;

      element.open();
      await element.updateComplete;
      const dialog = element.shadowRoot?.querySelector('dialog');
      assert.exists(dialog, 'dialog should be rendered');
      assert.isTrue(dialog?.hasAttribute('open'), 'dialog should be open');

      const internals = internalsLookup.get(element) as ElementInternalsLike;
      assert.equal(internals.ariaHidden, 'false', 'aria-hidden should be "false" when opened');

      const body = document.body;
      assert.equal(body.style.overflow, 'hidden', 'body overflow should be hidden when flyout is opened');
      assert.isFalse(
        body.classList.contains('zui-flyout-inline-opened'),
        'body should not have class when flyout is opened'
      );
    });

    test('calling close renders a closed dialog', async () => {
      await element.updateComplete;

      element.opened = true;
      await element.updateComplete;
      element.close();
      await element.updateComplete;
      const dialog = element.shadowRoot?.querySelector('dialog');
      assert.exists(dialog, 'dialog should be rendered');
      assert.isFalse(dialog?.hasAttribute('open'), 'dialog should be closed');

      const internals = internalsLookup.get(element) as ElementInternalsLike;
      assert.equal(internals.ariaHidden, 'true', 'aria-hidden should be "true" when closed');

      const body = document.body;
      assert.equal(body.style.overflow, '', 'body overflow should be empty string when flyout is closed');
      assert.isFalse(
        body.classList.contains('zui-flyout-inline-opened'),
        'body should not have class when flyout is closed'
      );
    });

    test('escape closes the flyout', async () => {
      await element.updateComplete;

      element.opened = true;
      await element.updateComplete;

      await executeServerCommand('keypress', { key: 'Escape' });
      await element.updateComplete;

      assert.isFalse(element.opened, 'flyout should be closed after escape keypress');
    });

    test('flyout with opened attribute initially has opened dialog', async () => {
      element.remove();

      element = document.createElement('zui-flyout');
      element.setAttribute('opened', '');
      document.body.appendChild(element);

      await element.updateComplete;

      const dialog = element.shadowRoot?.querySelector('dialog');
      assert.exists(dialog, 'dialog should be rendered');
      assert.isTrue(dialog?.hasAttribute('open'), 'dialog should be open');
    });
  });

  suite('[mode="inline"]', () => {
    setup(() => {
      element.mode = 'inline';
    });

    test('a11y', async () => {
      await element.updateComplete;

      const internals = internalsLookup.get(element) as ElementInternalsLike;

      assert.equal(internals.role, 'complementary', 'role should be "complementary"');
      assert.equal(internals.ariaModal, 'false', 'aria-modal should be "false"');
      assert.equal(internals.ariaHidden, 'true', 'aria-hidden should be "true"');
    });

    test('closed flyout renders hidden content-root', async () => {
      await element.updateComplete;

      element.opened = false;
      await element.updateComplete;
      const contentRoot = element.shadowRoot?.querySelector('.content-root');
      assert.exists(contentRoot, 'content-root div should be rendered');
      assert.equal(contentRoot?.tagName.toLowerCase(), 'div', 'content-root should be a div');

      const computedStyle = window.getComputedStyle(contentRoot as Element);
      assert.equal(computedStyle.visibility, 'hidden', 'content-root should be hidden by default');

      const internals = internalsLookup.get(element) as ElementInternalsLike;
      assert.equal(internals.ariaHidden, 'true', 'aria-hidden should be "true" when closed');

      const body = document.body;
      assert.equal(body.style.overflow, '', 'body overflow should be empty string when flyout is closed');
      assert.isFalse(
        body.classList.contains('zui-flyout-inline-opened'),
        'body should not have class when flyout is closed'
      );
    });

    test('opened flyout renders a visible content-root', async () => {
      await element.updateComplete;

      element.opened = true;
      await element.updateComplete;
      const contentRoot = element.shadowRoot?.querySelector('.content-root');
      assert.exists(contentRoot, 'content-root div should be rendered');
      assert.equal(contentRoot?.tagName.toLowerCase(), 'div', 'content-root should be a div');

      const computedStyle = window.getComputedStyle(contentRoot as Element);
      assert.equal(computedStyle.visibility, 'visible', 'content-root should be visible when opened');

      const internals = internalsLookup.get(element) as ElementInternalsLike;
      assert.equal(internals.ariaHidden, 'false', 'aria-hidden should be "false" when opened');

      const body = document.body;
      assert.equal(body.style.overflow, 'hidden', 'body overflow should be "hidden" when flyout is opened');
      assert.isTrue(
        body.classList.contains('zui-flyout-inline-opened'),
        'body should have class when flyout is opened'
      );
    });

    test('calling open renders a visible content-root', async () => {
      await element.updateComplete;

      element.open();
      await element.updateComplete;
      const contentRoot = element.shadowRoot?.querySelector('.content-root');
      assert.exists(contentRoot, 'content-root div should be rendered');
      assert.equal(contentRoot?.tagName.toLowerCase(), 'div', 'content-root should be a div');

      const computedStyle = window.getComputedStyle(contentRoot as Element);
      assert.equal(computedStyle.visibility, 'visible', 'content-root should be visible when opened');

      const internals = internalsLookup.get(element) as ElementInternalsLike;
      assert.equal(internals.ariaHidden, 'false', 'aria-hidden should be "false" when opened');

      const body = document.body;
      assert.equal(body.style.overflow, 'hidden', 'body overflow should be "hidden" when flyout is opened');
      assert.isTrue(
        body.classList.contains('zui-flyout-inline-opened'),
        'body should have class when flyout is opened'
      );
    });

    test('calling close renders a hidden content-root', async () => {
      await element.updateComplete;

      element.opened = true;
      await element.updateComplete;
      element.close();
      await element.updateComplete;
      const contentRoot = element.shadowRoot?.querySelector('.content-root');
      assert.exists(contentRoot, 'content-root div should be rendered');
      assert.equal(contentRoot?.tagName.toLowerCase(), 'div', 'content-root should be a div');

      const computedStyle = window.getComputedStyle(contentRoot as Element);
      assert.equal(computedStyle.visibility, 'hidden', 'content-root should be hidden when closed');

      const internals = internalsLookup.get(element) as ElementInternalsLike;
      assert.equal(internals.ariaHidden, 'true', 'aria-hidden should be "true" when closed');

      const body = document.body;
      assert.equal(body.style.overflow, '', 'body overflow should be empty string when flyout is closed');
      assert.isFalse(
        body.classList.contains('zui-flyout-inline-opened'),
        'body should not have class when flyout is closed'
      );
    });

    suite('contentSelector', () => {
      let contentElement: HTMLElement | null;
      setup(() => {
        contentElement = document.createElement('div');
        contentElement.id = 'content';
        document.body.appendChild(contentElement);
        element.setAttribute('content-selector', '#content');
      });

      teardown(() => {
        contentElement?.remove();
      });

      test('contentSelector has class removed when flyout closed', async () => {
        await element.updateComplete;

        element.opened = false;
        await element.updateComplete;
        assert.isFalse(
          contentElement?.classList.contains('zui-flyout-inline-opened'),
          'contentSelector should have class removed when flyout closed'
        );
      });

      test('contentSelector has class applied when flyout opened', async () => {
        await element.updateComplete;

        element.opened = true;
        await element.updateComplete;
        assert.isTrue(
          contentElement?.classList.contains('zui-flyout-inline-opened'),
          'contentSelector should have class applied when flyout opened'
        );
      });
    });
  });

  suite('two flyouts', () => {
    let secondFlyout: ZuiFlyoutElement;
    setup(async () => {
      // we set the first flyout to inline mode because it modifies other elements
      element.mode = 'inline';
      await element.updateComplete;

      secondFlyout = document.createElement('zui-flyout');
      secondFlyout.mode = 'inline';
      document.body.appendChild(secondFlyout);
      await secondFlyout.updateComplete;
    });

    teardown(() => {
      secondFlyout.remove();
    });

    test('opening flyout closes other flyout', async () => {
      secondFlyout.opened = true;
      await secondFlyout.updateComplete;

      element.opened = true;
      await element.updateComplete;

      assert.isFalse(secondFlyout.opened, 'second flyout should be closed when first flyout is opened');

      const body = document.body;
      assert.equal(body.style.overflow, 'hidden', 'body overflow should be "hidden" when one flyout is opened');
      assert.isTrue(
        body.classList.contains('zui-flyout-inline-opened'),
        'body should have class when one flyout is opened'
      );
    });

    test('closing one flyout keeps body state when other flyout is opened', async () => {
      secondFlyout.opened = true;
      await secondFlyout.updateComplete;

      element.close();
      await element.updateComplete;

      assert.isTrue(secondFlyout.opened, 'second flyout should remain opened when first flyout is closed');
      const body = document.body;
      assert.equal(body.style.overflow, 'hidden', 'body overflow should be "hidden" when one flyout is opened');
      assert.isTrue(
        body.classList.contains('zui-flyout-inline-opened'),
        'body should have class when one flyout is opened'
      );
    });
  });

  suite('controlsList attribute', () => {
    test('close button renders by default', async () => {
      await element.updateComplete;
      element.opened = true;
      await element.updateComplete;

      const closeButton = element.shadowRoot?.querySelector('button.close');
      assert.exists(closeButton, 'close button should be rendered by default');
    });

    test('controlslist="noclose" hides close button', async () => {
      await element.updateComplete;
      element.setAttribute('controlslist', 'noclose');
      await element.updateComplete;

      element.opened = true;
      await element.updateComplete;

      const closeButton = element.shadowRoot?.querySelector('button.close');
      assert.notExists(closeButton, 'close button should not be rendered when controlsList="noclose"');
    });

    test('setting controlsList via property hides close button', async () => {
      await element.updateComplete;
      element.controlsList.add('noclose');
      await element.updateComplete;

      element.opened = true;
      await element.updateComplete;

      const closeButton = element.shadowRoot?.querySelector('button.close');
      assert.notExists(closeButton, 'close button should not be rendered when controlsList contains "noclose"');
    });

    test('removing noclose token shows close button', async () => {
      await element.updateComplete;
      element.setAttribute('controlslist', 'noclose');
      await element.updateComplete;

      element.opened = true;
      await element.updateComplete;

      let closeButton = element.shadowRoot?.querySelector('button.close');
      assert.notExists(closeButton, 'close button should not be rendered initially');

      element.controlsList.remove('noclose');
      await element.updateComplete;

      closeButton = element.shadowRoot?.querySelector('button.close');
      assert.exists(closeButton, 'close button should be rendered after removing noclose token');
    });

    test('controlslist with multiple tokens including noclose hides close button', async () => {
      await element.updateComplete;
      element.setAttribute('controlslist', 'noclose other-token');
      await element.updateComplete;

      element.opened = true;
      await element.updateComplete;

      const closeButton = element.shadowRoot?.querySelector('button.close');
      assert.notExists(
        closeButton,
        'close button should not be rendered with multiple controlsList tokens including noclose'
      );
    });
  });
});

interface ElementInternalsLike {
  role?: string;
  ariaModal?: string;
  ariaHidden?: string;
}
