// eslint-disable-next-line @typescript-eslint/triple-slash-reference
/// <reference path="../../../../test/src/custom_typings/chai.d.ts" />
/* eslint-disable no-undef */
import { ZuiDialogElement } from '@zywave/zui-dialog/dist/zui-dialog.js';
import { assert } from '@esm-bundle/chai';
import { awaitEvent, constructHtml, sleep } from '../../../../test/src/util/helpers';
import { conditionalTest } from '../../../../test/src/util/mocha-helpers';
/* @ts-ignore */

function getOverlayElement(dialog: ZuiDialogElement) {
  return dialog.shadowRoot?.querySelector('dialog') ?? dialog.shadowRoot?.querySelector('.dialog');
}

function getDialogDisplayValue(dialog: ZuiDialogElement) {
  return window.getComputedStyle(dialog).getPropertyValue('display');
}

function assertDialogState(dialog: ZuiDialogElement, opened: boolean) {
  assert.equal(opened, dialog.opened);
  opened ? assert.isTrue(dialog.hasAttribute('opened')) : assert.isFalse(dialog.hasAttribute('opened'));
}

function createDialog() {
  return document.createElement('zui-dialog') as ZuiDialogElement;
}

suite('zui-dialog', () => {
  let element: ZuiDialogElement;

  setup(() => {
    element = createDialog();
    document.body.appendChild(element);
  });

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

  test('`.opened` getter and setter property is supported', async () => {
    const dialogOpen = () => {
      element.opened = true;
    };
    const dialogClose = () => {
      element.opened = false;
    };
    await element.updateComplete;
    assert.isFalse(element.opened);
    await Promise.all([awaitEvent(element, 'open'), Promise.resolve(dialogOpen())]);
    await element.updateComplete;
    assert.isTrue(element.opened);
    await Promise.all([awaitEvent(element, 'close'), Promise.resolve(dialogClose())]);
    await element.updateComplete;
    assert.isFalse(element.opened);
  });

  test('`.canceled` retrieves boolean if last close was by cancel', async () => {
    const footerButtonsStr = `
      <div slot="footer">
        <zui-button dialog-confirm>Confirm</zui-button>
        <zui-button dialog-close class="link">Cancel</zui-button>
      </div>
    `;
    element.appendChild(constructHtml(footerButtonsStr));
    await element.updateComplete;

    // .canceled is false if click dialog-confirm
    element.opened = true;
    await element.updateComplete;
    (document.querySelector('[slot="footer"] [dialog-confirm]') as HTMLElement)?.click();
    await element.updateComplete;
    assert.isFalse(element.canceled);

    // .canceled is true if click dialog-close
    element.opened = true;
    await element.updateComplete;
    (document.querySelector('[slot="footer"] [dialog-close]') as HTMLElement)?.click();
    await element.updateComplete;
    assert.isTrue(element.canceled);

    // .canceled is true if click overlay
    element.opened = true;
    await element.updateComplete;
    getOverlayElement(element)?.click();
    await element.updateComplete;
    assert.isTrue(element.canceled);
  });

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

  test('calling .open() makes dialog visible', async () => {
    await element.updateComplete;
    assert.equal(getDialogDisplayValue(element), 'none');
    element.open();
    await element.updateComplete;
    assertDialogState(element, true);
    assert.isNotTrue(getDialogDisplayValue(element), 'none');
  });

  test('calling .open() on dialog raises "open" event', async () => {
    let wasOpen = false;

    element.addEventListener('open', () => (wasOpen = true));

    await element.updateComplete;
    element.open();
    await element.updateComplete;
    assertDialogState(element, true);
    assert.isTrue(wasOpen);
  });

  test('calling .open() makes dialog disappear', async () => {
    await element.updateComplete;
    assert.equal(getDialogDisplayValue(element), 'none');
    element.open();
    await element.updateComplete;
    assertDialogState(element, true);
    await element.updateComplete;
    element.close();
    await element.updateComplete;
    assertDialogState(element, false);
    assert.isNotTrue(getDialogDisplayValue(element), 'none');
  });

  test('manually calling .close() on dialog raises "close" event with "event.detail: false"', async () => {
    let wasClosed = false;
    let eventDetail: boolean;

    element.addEventListener('close', (e: CustomEvent) => {
      wasClosed = true;
      eventDetail = e.detail;
    });

    await element.updateComplete;
    element.open();
    await element.updateComplete;
    assertDialogState(element, true);
    element.close();
    await element.updateComplete;
    assertDialogState(element, false);
    assert.isTrue(wasClosed);
    await element.updateComplete;
    assert.isFalse(eventDetail);
  });

  test('clicking element with "dialog-confirm" attr closes open dialog, raises "close" event with "event.detail: true"', async () => {
    let wasClosed = false;
    let eventDetail: boolean;
    const confirmElStr = `<div slot="footer" dialog-confirm>Confirm</div>`;
    const confirmElFrag = constructHtml(confirmElStr);

    function getConfirmElement(): HTMLElement {
      return element.querySelector('[dialog-confirm]');
    }

    element.addEventListener('close', (e: CustomEvent) => {
      wasClosed = true;
      eventDetail = e.detail;
    });

    element.appendChild(confirmElFrag);
    await element.updateComplete;
    element.open();
    await element.updateComplete;
    assertDialogState(element, true);
    getConfirmElement().click();
    await element.updateComplete;
    assert.isTrue(wasClosed);
    assert.isTrue(eventDetail);
  });

  test('clicking element with "dialog-close" attr closes open dialog, raises "close" event with "event.detail: false"', async () => {
    let wasClosed = false;
    let eventDetail: boolean;
    const cancelElStr = `<div slot="footer" dialog-close>Cancel</div>`;
    const cancelElFrag = constructHtml(cancelElStr);

    function getCancelElement(): HTMLElement {
      return element.querySelector('[dialog-close]');
    }

    element.addEventListener('close', (e: CustomEvent) => {
      wasClosed = true;
      eventDetail = e.detail;
    });

    element.appendChild(cancelElFrag);
    await element.updateComplete;
    element.open();
    await element.updateComplete;
    assertDialogState(element, true);
    getCancelElement().click();
    await element.updateComplete;
    assert.isTrue(wasClosed);
    assert.isFalse(eventDetail);
  });

  test('closing dialog should remove opened attribute', async () => {
    await element.updateComplete;
    element.open();
    await element.updateComplete;
    assertDialogState(element, true);
    element.close();
    await element.updateComplete;
    assertDialogState(element, false);
  });

  test('clicking scrim/gray background area closes dialog', async () => {
    await element.updateComplete;
    element.open();
    await element.updateComplete;
    assertDialogState(element, true);
    await Promise.allSettled([awaitEvent(element, 'close'), getOverlayElement(element)?.click()]);
    assertDialogState(element, false);
  });

  test('"no-cancel-outside-dialog" attr prevent outside click from closing', async () => {
    await element.updateComplete;
    element.setAttribute('no-cancel-outside-dialog', '');
    element.open();
    await element.updateComplete;
    assertDialogState(element, true);
    getOverlayElement(element)?.click();
    await element.updateComplete;
    assertDialogState(element, true);
  });

  conditionalTest(
    window.CSS.supports('contain', 'none'),
    'Dialog component has `contain: none` set to correctly display rendered component',
    async () => {
      await element.updateComplete;
      await sleep(10);
      assert.equal(window.getComputedStyle(element).getPropertyValue('contain'), 'none');
    }
  );

  test('body overflow set to hidden when dialog is opened', async () => {
    const originalOverflow = window.getComputedStyle(document.body).overflow;
    await element.updateComplete;
    element.open();
    await element.updateComplete;
    assertDialogState(element, true);
    assert.equal(window.getComputedStyle(document.body).overflow, 'hidden', 'Overflow not hidden');
    element.close();
    await element.updateComplete;

    assert.equal(window.getComputedStyle(document.body).overflow, originalOverflow, 'Overflow not properly reset');
  });

  test('body overflow properly managed when multiple dialogs opened', async () => {
    const originalOverflow = window.getComputedStyle(document.body).overflow;
    const dialog2 = createDialog();
    document.body.append(dialog2);
    await Promise.all([element.updateComplete, dialog2.updateComplete]);
    element.open();
    dialog2.open();
    await Promise.all([element.updateComplete, dialog2.updateComplete]);
    assert.equal(window.getComputedStyle(document.body).overflow, 'hidden', 'Overflow not hidden - two open');

    // instead of just closing dialog2, we're going to remove it which should have the same effect
    dialog2.remove();
    assert.equal(window.getComputedStyle(document.body).overflow, 'hidden', 'Overflow not hidden - one open');
    element.close();
    await element.updateComplete;
    assert.equal(window.getComputedStyle(document.body).overflow, originalOverflow, 'Overflow not properly reset');
  });

  test('dialog element should be shown when opened attribute initially applied', async () => {
    element.setAttribute('opened', '');
    await element.updateComplete;
    assert.exists(element.shadowRoot?.querySelector('dialog[open]'));
  });

  test('automatically sticky dialog footer when the scrollbar is visible in an opened dialog', async () => {
    await element.updateComplete;
    element.shadowRoot!.querySelector('article')!.style.height = '400px';
    element.shadowRoot!.getElementById('dialogDesc')!.style.height = '1000px';
    await element.updateComplete;
    element.open();
    await element.updateComplete;
    assert.exists(element.shadowRoot?.querySelector('dialog')?.classList.contains('scrolling'));
  });

  test('remove sticky dialog footer when a scrollable dialog is closed', async () => {
    await element.updateComplete;
    element.shadowRoot!.querySelector('article')!.style.height = '400px';
    element.shadowRoot!.getElementById('dialogDesc')!.style.height = '1000px';
    await element.updateComplete;
    element.open();
    await element.updateComplete;
    element.close();
    await element.updateComplete;
    assert.isFalse(element.shadowRoot?.querySelector('dialog')?.classList.contains('scrolling'));
  });

  test('dialog should scroll when lots of dynamic content is injected after dialog opens', async () => {
    await element.updateComplete;

    const HeadingStr = `<h2 id="dynamic-header" slot="header"></h2>`;
    element.appendChild(constructHtml(HeadingStr));
    const ContentStr = `<div id="dynamic-content" slot="content"></div>`;
    element.appendChild(constructHtml(ContentStr));

    const footerButtonsStr = `
      <div id="dynamic-footer" slot="footer">
        <zui-button dialog-confirm>Confirm</zui-button>
        <zui-button dialog-close class="link">Cancel</zui-button>
      </div>
    `;
    element.appendChild(constructHtml(footerButtonsStr));

    const HeadingEl = element.querySelector('#dynamic-header') as HTMLElement;
    const ContentEl = element.querySelector('#dynamic-content') as HTMLElement;

    element.open();

    element.addEventListener('open', () => {
      setTimeout(() => {
        HeadingEl.textContent = 'Dynamic Heading';
        ContentEl.innerHTML = `Yo, VIP, let's kick it!<br />
        Ice ice baby<br />
        Ice ice baby<br />
        All right stop<br />
        Collaborate and listen<br />
        Ice is back with my brand new invention<br />
        Something grabs a hold of me tightly<br />
        Then I flow that a harpoon daily and nightly<br />
        Will it ever stop?<br />
        Yo, I don't know<br />
        Turn off the lights and I'll glow<br />
        To the extreme, I rock a mic like a vandal<br />
        Light up a stage and wax a chump like a candle<br />
        Dance <br />
        Bum rush the speaker that booms<br />
        I'm killin' your brain like a poisonous mushroom<br />
        Deadly, when I play a dope melody<br />
        Anything less that the best is a felony<br />
        Love it or leave it<br />
        You better gain way<br />
        You better hit bull's eye<br />
        The kid don't play<br />
        If there was a problem<br />
        Yo, I'll solve it<br />
        Check out the hook while my DJ revolves it<br />
        Ice ice baby Vanilla<br />
        Ice ice baby Vanilla<br />
        Ice ice baby Vanilla<br />
        Ice ice baby Vanilla<br />
        Now that the party is jumping<br />
        With the bass kicked in, the fingers are pumpin'<br />
        Quick to the point, to the point no faking<br />
        I'm cooking MC's like a pound of bacon<br />
        Burning them if they're not quick and nimble<br />
        I go crazy when I hear a cymbal<br />
        And a hi hat with a souped up tempo<br />
        I'm on a roll and it's time to go solo<br />
        Rollin in my 5.0<br />
        With my ragtop down so my hair can blow<br />
        The girlies on standby<br />
        Waving just to say hi<br />
        Did you stop?<br />
        No, I just drove by<br />
        Kept on pursuing to the next stop<br />
        I busted a left and I'm heading to the next block<br />
        That block was dead<br />
        Yo so I continued to a1a Beachfront Ave<br />
        Girls were hot wearing less than bikinis<br />
        Rock man lovers driving Lamborghini<br />
        Jealous 'cause I'm out getting mine<br />
        Shay with a gauge and Vanilla with a nine<br />
        Ready for the chumps on the wall<br />
        The chumps are acting ill because they're so full of eight balls<br />
        Gunshots ranged out like a bell<br />
        I grabbed my nine<br />
        All I heard were shells<br />
        Fallin' on the concrete real fast<br />
        Jumped in my car, slammed on the gas<br />
        Bumper to bumper the avenue's packed<br />
        I'm tryin' to get away before the jackers jack<br />
        Police on the scene<br />
        You know what I mean<br />
        They passed me up, confronted all the dope fiends<br />
        If there was a problem<br />
        Yo, I'll solve it<br />
        Check out the hook while my DJ revolves it`;
      }, 1000);
    });

    assert.exists(element.shadowRoot?.querySelector('dialog')?.classList.contains('scrolling'));
  });

  test('dialog should close when a form with method="dialog" is submitted', async () => {
    await element.updateComplete;

    const form = document.createElement('form');
    form.setAttribute('method', 'dialog');
    const button = document.createElement('button');
    button.setAttribute('type', 'submit');
    button.textContent = 'Submit';
    form.appendChild(button);
    element.appendChild(form);

    element.open();
    await element.updateComplete;
    assertDialogState(element, true);
    button.click();
    await element.updateComplete;
    assertDialogState(element, false);
  });

  test('dialog should close when form is submitted from a button that specifies formmethod="dialog"', async () => {
    await element.updateComplete;

    const form = document.createElement('form');
    const button = document.createElement('button');
    button.setAttribute('type', 'submit');
    button.setAttribute('formmethod', 'dialog');
    button.textContent = 'Submit';
    form.appendChild(button);
    element.appendChild(form);

    element.open();
    await element.updateComplete;
    assertDialogState(element, true);
    button.click();
    await element.updateComplete;
    assertDialogState(element, false);
  });
});
