// eslint-disable-next-line @typescript-eslint/triple-slash-reference
/// <reference path="../../../../test/src/custom_typings/chai.d.ts" />
/* eslint-disable no-undef */
/* eslint-disable @typescript-eslint/no-explicit-any */
import { ZuiOptionElement, ZuiSelectDropdownElement } from '@zywave/zui-select';
import { ZuiDialogElement } from '@zywave/zui-dialog';
import { assert } from '@esm-bundle/chai';
import { awaitEvent, randNumber, randString, sleep } from '../../../../test/src/util/helpers.js';
import { executeServerCommand, sendKeys } from '@web/test-runner-commands';

async function open(element: ZuiSelectDropdownElement) {
  await element.updateComplete;
  element.showPicker();
  await sleep(10);
  await element.updateComplete;
}

async function close(element: ZuiSelectDropdownElement) {
  element.closest('body')?.click();
  await sleep(10);
  await element.updateComplete;
}

async function selectOption(element: ZuiSelectDropdownElement, index: number) {
  await open(element);
  const option = element.item(index);
  assert.exists(option, `No option found with index ${index}.`);
  option.selected = true;
  await close(element);
  return option.value;
}

async function toggleSelectAllOption(element: ZuiSelectDropdownElement) {
  // forgive me, as I hack around the inability to simply "click" on DOM element
  await open(element);

  (element as any)._highlightedIndex = -1;
  element.requestUpdate();
  await element.updateComplete;

  (element.shadowRoot?.querySelector('input') as HTMLElement).dispatchEvent(
    new KeyboardEvent('keydown', { key: 'Enter' })
  );

  await close(element);
}

function generateOptions(element: ZuiSelectDropdownElement, optionCount: number) {
  const result: ZuiOptionElement[] = [];
  for (let i = 0; i < optionCount; i++) {
    const option: ZuiOptionElement = document.createElement('zui-option');
    option.value = randString();
    option.innerText = randString();
    element.append(option);
    result.push(option);
  }

  return result;
}

suite('zui-select-dropdown', () => {
  let element: ZuiSelectDropdownElement;

  setup(() => {
    element = document.createElement('zui-select-dropdown');
    document.body.append(element);
  });

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

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

  test('item method exposes matching option', async () => {
    const options = generateOptions(element, 10);
    await open(element);

    for (let i = 0; i < 10; i++) {
      const option = element.item(i);
      assert.exists(option);
      assert.equal(option.value, options[i].value);
      assert.equal(option.label, options[i].innerHTML);
    }
  });

  test('item method returns null when no options', async () => {
    await open(element);
    const option = element.item(0);
    assert.isNull(option);
  });

  test('item method returns null when index out of bounds', async () => {
    generateOptions(element, 10);
    await open(element);
    const option = element.item(11);
    assert.isNull(option);
  });

  test('queryHandler is called with user input', async () => {
    element.searchable = true;
    let resultQuery: string | undefined;
    element.queryHandler = (query, options) => {
      resultQuery = query;
      return options;
    };

    await element.updateComplete;

    const text = randString();
    await executeServerCommand('fill-text', { selector: `zui-select-dropdown input`, text });
    await element.updateComplete;

    assert.equal(resultQuery, text);
  });

  test('user input triggers query event', async () => {
    element.searchable = true;
    element.debounce = 1_000;

    await element.updateComplete;

    const text = randString();
    const userInputPromise = executeServerCommand('fill-text', { selector: `zui-select-dropdown input`, text }).then(
      () => element.updateComplete
    );
    let queryEvent: CustomEvent<string>;
    const queryPromise = awaitEvent(element, 'query').then((e) => (queryEvent = e as CustomEvent<string>));
    await Promise.all([userInputPromise, queryPromise]);
    assert.exists(queryEvent);
    assert.equal(queryEvent.detail, text);
  });

  test('empty groups are not rendered with hide-empty-groups attribute', async () => {
    element.hideEmptyGroups = true;

    const group1 = document.createElement('zui-option-group');
    group1.label = 'Group 1';

    const option1 = document.createElement('zui-option');
    option1.value = 'option1';
    option1.innerText = 'Test Option 1';
    group1.append(option1);
    element.append(group1);

    const group2 = document.createElement('zui-option-group');
    group2.label = 'Group 2';
    element.append(group2);

    element.requestUpdate();
    await element.updateComplete;

    await open(element);

    const renderedOptionGroups = element.shadowRoot?.querySelectorAll('.group-label');
    assert.exists(renderedOptionGroups);
    assert.lengthOf(renderedOptionGroups, 1);
    assert.equal(renderedOptionGroups?.[0].textContent?.trim(), 'Group 1');
  });

  suite('single select', () => {
    const optionCount = 10;

    setup(() => {
      element.multiple = false;
      generateOptions(element, optionCount);
    });

    suite('searchable behavior', () => {
      setup(() => {
        element.searchable = true;
      });

      // disabled test which violates encapsulation
      test.skip('modifying input without clearing it does not modify selected property of option', async () => {
        // // Choose an option that is NOT the first
        // const optionIndex = 1 + randNumber(optionCount - 1);
        // const option = element._zuiOptions[optionIndex];
        // assert.exists(option, `No option found at index ${optionIndex}`);
        // option.selected = true;
        // element.requestUpdate();
        // await element.updateComplete;
        // const text = randString();
        // element.shadowRoot?.querySelector('input')?.focus();
        // sendKeys({
        //   type: text,
        // });
        // await awaitEvent(element.shadowRoot?.querySelector('input') as HTMLInputElement, 'input', 1000);
        // await element.updateComplete;
        // assert.isTrue(element._zuiOptions[optionIndex].selected);
      });

      test('modifying input value which can be parsed as falsy does not modify selected property of option', async () => {
        const option: ZuiOptionElement = document.createElement('zui-option');
        const almostFalsyValue = 'fals';
        option.value = almostFalsyValue;
        option.innerText = almostFalsyValue;
        option.selected = true;
        element.append(option);
        element.requestUpdate();
        await element.updateComplete;

        element.shadowRoot?.querySelector('input')?.focus();

        sendKeys({
          type: 'e',
        });

        await awaitEvent(element.shadowRoot?.querySelector('input') as HTMLInputElement, 'input');
        await element.updateComplete;
        assert.isTrue(option.selected);
      });
    });

    suite('form association', () => {
      let form: HTMLFormElement;
      const name = randString();
      setup(() => {
        form = document.createElement('form');
        document.body.append(form);
        form.append(element);
        element.setAttribute('name', name);
      });

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

      test('selected option value included in form submission', async () => {
        const value = await selectOption(element, randNumber(optionCount));

        const formData = new FormData(form);
        const formValues = formData.getAll(name);

        assert.isNotEmpty(formValues);
        assert.equal(formValues.length, 1);
        assert.include(formValues, value);
      });

      test('only one selected option value included in form submission', async () => {
        let value;
        for (let i = 0; i < 3; i++) {
          value = await selectOption(element, i);
        }

        const formData = new FormData(form);
        const formValues = formData.getAll(name);

        assert.isNotEmpty(formValues);
        assert.equal(formValues.length, 1);
        assert.include(formValues, value);
      });
    });
  });

  suite('multiselect', () => {
    const optionCount = 10;
    setup(() => {
      element.multiple = true;
      generateOptions(element, optionCount);
    });

    suite('truncation', () => {
      test('maximumResultsDisplayCount defaults to 5', () => {
        assert.equal(element.maximumResultsDisplayCount, 5);
      });

      test('truncatedResultMessageFormat defaults to null', () => {
        assert.isNull(element.truncatedResultMessageFormat);
      });

      test('truncation does not occur when truncatedResultMessageFormat is null', async () => {
        element.truncatedResultMessageFormat = null;
        let selection = 6;
        while (selection > 0) {
          await selectOption(element, --selection);
        }

        assert.equal(element.shadowRoot?.querySelectorAll('.selection').length, 6);
        assert.notExists(element.shadowRoot?.querySelector('.selection.truncated'));
      });

      test('truncation is applied when more than maximumResultsDisplayCount selected', async () => {
        element.truncatedResultMessageFormat = '{0}';
        element.maximumResultsDisplayCount = 2;

        let selection = 3;
        while (selection > 0) {
          await selectOption(element, --selection);
        }

        assert.equal(element.shadowRoot?.querySelectorAll('.selection:not(.truncated)').length, 2);
        assert.exists(element.shadowRoot?.querySelector('.selection.truncated'));
      });
    });

    suite('enable-select-all validation', () => {
      test('enable-select-all is false when multiple is not set', () => {
        element.multiple = false;
        element.selectAllOptionLabel = 'Select all';
        element.enableSelectAll = true;
        element.enableSelectAllOverride = false;

        assert.isFalse(element.enableSelectAll);
      });

      test('enable-select-all is false when selectAllOptionLabel is not set', () => {
        element.multiple = false;
        element.selectAllOptionLabel = 'Select all';
        element.enableSelectAll = true;
        element.enableSelectAllOverride = false;

        assert.isFalse(element.enableSelectAll);
      });

      suite('enable-select-all-override is true', () => {
        setup(() => {
          element.enableSelectAllOverride = true;
          element.selectAllOptionLabel = 'Select all';
          element.enableSelectAll = true;
          element.selectAllOptionValue = '<ALL>';
          element.selectAllResultLabel = 'All selected';
        });

        test('enable-select-all is false when selectAllOptionValue is not set', () => {
          element.selectAllOptionValue = null;
          assert.isFalse(element.enableSelectAll);
        });

        test('enable-select-all is false when selectAllResultLabel is not set', () => {
          element.selectAllResultLabel = null;
          assert.isFalse(element.enableSelectAll);
        });

        test('enable-select-all is true', () => {
          assert.isTrue(element.enableSelectAll);
        });
      });

      suite('enable-select-all-override is false', () => {
        setup(() => {
          element.enableSelectAllOverride = false;
          element.selectAllOptionLabel = 'Select all';
          element.enableSelectAll = true;
        });

        test('enable-select-all is true', () => {
          assert.isTrue(element.enableSelectAll);
        });
      });
    });

    suite('select all behavior', () => {
      setup(() => {
        element.enableSelectAllOverride = false;
        element.selectAllOptionLabel = 'Select all';
        element.enableSelectAll = true;
      });

      suite('enable-select-all-override is false', () => {
        test('when user selects all other options manually, select all option is checked', async () => {
          let selection = optionCount;
          while (selection > 0) {
            await selectOption(element, --selection);
          }

          assert.equal(element.shadowRoot?.querySelectorAll('.selection').length, optionCount);
          assert.isTrue(element.allSelected);

          await open(element);
          assert.exists(element.shadowRoot?.querySelector('.option.select-all.selected'));
        });

        test('when user chooses select all option, all options selected in result container', async () => {
          await toggleSelectAllOption(element);

          assert.equal(element.shadowRoot?.querySelectorAll('.selection').length, optionCount);
          assert.isTrue(element.allSelected);

          await open(element);
          assert.exists(element.shadowRoot?.querySelector('.option.select-all.selected'));
        });
      });

      suite('enable-select-all-override is true', () => {
        setup(() => {
          element.enableSelectAllOverride = true;
          element.selectAllOptionValue = '<ALL>';
          element.selectAllResultLabel = 'All selected';
        });

        test('when user selects all other options manually, selectAllResultLabel is not rendered instead', async () => {
          let selection = optionCount;
          while (selection > 0) {
            await selectOption(element, --selection);
          }

          assert.equal(element.shadowRoot?.querySelectorAll('.selection').length, optionCount);
          assert.notExists(element.shadowRoot?.querySelector('.selection.all-selected'));
          assert.isFalse(element.allSelected);
        });

        test('when user chooses select all option, selectAllResultLabel is rendered instead', async () => {
          await toggleSelectAllOption(element);
          assert.equal(element.shadowRoot?.querySelectorAll('.selection').length, 1);
          assert.exists(element.shadowRoot?.querySelector('.selection.all-selected'));
          assert.isTrue(element.allSelected);
        });

        test('when user chooses select all option, selectAllOptionValue is only value in form submission', async () => {
          const form = document.createElement('form');
          element.setAttribute('name', 'test');
          form.append(element);

          await toggleSelectAllOption(element);

          const formData = new FormData(form);

          const values = formData.getAll('test');

          assert.equal(values.length, 1);
          assert.include(values, element.selectAllOptionValue);

          form.remove();
        });
      });
    });

    suite('form association', () => {
      let form: HTMLFormElement;
      const name = randString();
      setup(() => {
        form = document.createElement('form');
        document.body.append(form);
        form.append(element);
        element.setAttribute('name', name);
      });

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

      test('multiple selected option values included in form submission', async () => {
        const values: string[] = [];
        for (let i = 0; i < 3; i++) {
          const value = await selectOption(element, i);
          values.push(value);
        }

        const formData = new FormData(form);
        const formValues = formData.getAll(name);

        assert.isNotEmpty(formValues);
        assert.equal(formValues.length, values.length);
        assert.includeMembers(formValues, values);
      });
    });

    test('clear() properly clears all selected options', async () => {
      const values: string[] = [];
      for (let i = 0; i < 3; i++) {
        const value = await selectOption(element, i);
        values.push(value);
      }

      assert.equal((element as any)._formValue?.length, values.length);

      element.clear();

      await element.updateComplete;

      assert.isNull((element as any)._formValue);
    });
  });

  suite('zui-select-dropdown in dialogs', () => {
    let dialog: ZuiDialogElement;

    setup(() => {
      dialog = document.createElement('zui-dialog') as ZuiDialogElement;
      document.body.append(dialog);
    });

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

    test('zui-select-dropdown is in a scrolling dialog', async () => {
      const longContentDiv = document.createElement('div');
      longContentDiv.setAttribute('slot', 'content');
      longContentDiv.style.height = '10000px';
      longContentDiv.append(element);
      dialog.append(longContentDiv);
      dialog.open();

      await dialog.updateComplete;

      assert.isTrue(dialog.opened, 'zui-dialog is opened');

      const dialogIsScrolling = dialog.shadowRoot?.querySelector('dialog')?.classList.contains('scrolling');
      assert.exists(dialogIsScrolling, 'zui-dialog is scrolling');
      assert.exists(dialog.querySelector('zui-select-dropdown'), 'zui-select-dropdown is, in fact, in zui-dialog');

      await open(element);

      assert.exists(
        element.shadowRoot?.querySelector('.in-scrolling-dialog'),
        'zui-select-dropdown is in a dialog that scrolls, therefore the wrapper has the class "in-scrolling-dialog"'
      );
    });

    test('zui-select-dropdown is in a dialog that is not scrolling', async () => {
      const dialogIsScrolling = dialog.shadowRoot?.querySelector('dialog')?.classList.contains('scrolling');
      dialog.append(element);
      dialog.open();

      await open(element);

      assert.isTrue(dialog.opened, 'zui-dialog is opened');
      assert.notExists(dialogIsScrolling, 'zui-dialog is not scrolling');
      assert.exists(dialog.querySelector('zui-select-dropdown'), 'zui-select-dropdown is, in fact, in zui-dialog');
      assert.notExists(
        dialog.shadowRoot?.querySelector('.in-scrolling-dialog'),
        'zui-select-dropdown is in a dialog that does not scroll'
      );
    });
  });

  suite('initial state detection', () => {
    let element: ZuiSelectDropdownElement;
    const optionCount = 10;

    setup(() => {
      element = document.createElement('zui-select-dropdown');
      element.multiple = true;
      element.enableSelectAll = true;
      element.selectAllOptionLabel = 'Select all';
      document.body.append(element);
    });

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

    test('correctly detects when all options are initially selected', async () => {
      // Generate options and set them all as selected
      const options = generateOptions(element, optionCount);
      options.forEach((option) => {
        option.selected = true;
      });

      // Wait for the element to initialize
      await element.updateComplete;

      // Verify the initial state
      assert.isTrue(element.allSelected);
      await open(element);
      assert.exists(element.shadowRoot?.querySelector('.option.select-all.selected'));
      await toggleSelectAllOption(element);
      assert.isFalse(element.allSelected);

      // check if each option is selected
      options.forEach((option) => {
        assert.isFalse(option.selected);
      });
    });
  });

  suite('async loading with queryHandler', () => {
    let element: ZuiSelectDropdownElement;
    let resolveQuery: (value: any) => void;

    setup(() => {
      element = document.createElement('zui-select-dropdown');
      element.searchable = true;
      document.body.append(element);
    });

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

    test('does not show "no results found" message while async loading', async () => {
      // Set up a queryHandler that returns a promise we can control
      element.queryHandler = () => {
        return new Promise((resolve) => {
          resolveQuery = resolve;
        });
      };

      await element.updateComplete;

      // Open the dropdown to trigger the queryHandler
      await open(element);

      // At this point, the queryHandler has been called but not resolved
      // The "no results found" message should NOT be visible
      const noResultsMessage = element.shadowRoot?.querySelector('.option.readonly');
      assert.notExists(noResultsMessage, 'No results message should not be shown while loading');

      // Verify the spinner is shown instead
      const spinner = element.shadowRoot?.querySelector('zui-spinner[active]');
      assert.exists(spinner, 'Spinner should be shown while loading');

      // Now resolve the query with no results
      resolveQuery([]);
      await element.updateComplete;
      await sleep(10);

      // After async loading completes with no results, the message SHOULD be shown
      const noResultsMessageAfter = element.shadowRoot?.querySelector('.option.readonly');
      assert.exists(
        noResultsMessageAfter,
        'No results message should be shown after loading completes with no results'
      );
      assert.equal(noResultsMessageAfter?.textContent, 'No results');
    });

    test('shows options after async loading completes with results', async () => {
      // Set up a queryHandler that returns a promise we can control
      element.queryHandler = () => {
        return new Promise((resolve) => {
          resolveQuery = resolve;
        });
      };

      await element.updateComplete;

      // Open the dropdown to trigger the queryHandler
      await open(element);

      // The "no results found" message should NOT be visible while loading
      const noResultsMessageWhileLoading = element.shadowRoot?.querySelector('.option.readonly');
      assert.notExists(noResultsMessageWhileLoading, 'No results message should not be shown while loading');

      // Resolve the query with some results
      resolveQuery([
        { label: 'Option 1', value: 'opt1' },
        { label: 'Option 2', value: 'opt2' },
      ]);
      await element.updateComplete;
      await sleep(10);

      // After async loading completes with results, options should be shown
      const options = element.shadowRoot?.querySelectorAll('.option:not(.readonly)');
      assert.exists(options);
      assert.isAtLeast(options.length, 2, 'Options should be shown after loading completes');

      // No results message should NOT be shown
      const noResultsMessageAfter = element.shadowRoot?.querySelector('.option.readonly');
      assert.notExists(noResultsMessageAfter, 'No results message should not be shown when options are available');
    });
  });
});
