text: import { unsafeHTML } from '@lion/core';
  import { localize } from '@lion/localize';
  import { localizeTearDown } from '@lion/localize/test-helpers.js';
  import { Required, Validator } from '@lion/form-core';
  import {
    expect,
    fixture,
    html,
    triggerBlurFor,
    triggerFocusFor,
    unsafeStatic,
  } from '@open-wc/testing';
  import sinon from 'sinon';
  import '../lion-field.js';
  
  /**
   * @typedef {import('../src/LionField.js').LionField} LionField
   * @typedef {import('../types/FormControlMixinTypes').FormControlHost} FormControlHost
   * @typedef {FormControlHost & HTMLElement & {__parentFormGroup?:HTMLElement, checked?:boolean}} FormControl
   */
  
  /** @typedef {HTMLElement & {shadowRoot: HTMLElement, assignedNodes: Function}} ShadowHTMLElement */
  
  const tagString = 'lion-field';
  const tag = unsafeStatic(tagString);
  const inputSlotString = '<input slot="input" />';
  const inputSlot = unsafeHTML(inputSlotString);
  
  /**
   * @param {import("../index.js").LionField} formControl
   * @param {string} newViewValue
   */
  function mimicUserInput(formControl, newViewValue) {
    formControl.value = newViewValue; // eslint-disable-line no-param-reassign
    formControl._inputNode.dispatchEvent(new CustomEvent('input', { bubbles: true }));
  }
  
  beforeEach(() => {
    localizeTearDown();
  });
  
  /**
   * @param {HTMLElement} el
   * @param {string} slot
   */
  function getSlot(el, slot) {
    const children = /** @type {any[]} */ (Array.from(el.children));
    return children.find(child => child.slot === slot);
  }
  
  describe('<lion-field>', () => {
    it(`puts a unique id "${tagString}-[hash]" on the native input`, async () => {
      const el = /** @type {LionField} */ (await fixture(html`<${tag}>${inputSlot}</${tag}>`));
      expect(getSlot(el, 'input').id).to.equal(el._inputId);
    });
  
    it(`has a fieldName based on the label`, async () => {
      const el1 = /** @type {LionField} */ (await fixture(
        html`<${tag} label="foo">${inputSlot}</${tag}>`,
      ));
      expect(el1.fieldName).to.equal(el1._labelNode.textContent);
  
      const el2 = /** @type {LionField} */ (await fixture(
        html`<${tag}><label slot="label">bar</label>${inputSlot}</${tag}>`,
      ));
      expect(el2.fieldName).to.equal(el2._labelNode.textContent);
    });
  
    it(`has a fieldName based on the name if no label exists`, async () => {
      const el = /** @type {LionField} */ (await fixture(
        html`<${tag} name="foo">${inputSlot}</${tag}>`,
      ));
      expect(el.fieldName).to.equal(el.name);
    });
  
    it(`can override fieldName`, async () => {
      const el = /** @type {LionField} */ (await fixture(
        html`<${tag} label="foo" .fieldName="${'bar'}">${inputSlot}</${tag}>`,
      ));
      expect(el.__fieldName).to.equal(el.fieldName);
    });
  
    it('fires focus/blur event on host and native input if focused/blurred', async () => {
      const el = /** @type {LionField} */ (await fixture(html`<${tag}>${inputSlot}</${tag}>`));
      const cbFocusHost = sinon.spy();
      el.addEventListener('focus', cbFocusHost);
      const cbFocusNativeInput = sinon.spy();
      el._inputNode.addEventListener('focus', cbFocusNativeInput);
      const cbBlurHost = sinon.spy();
      el.addEventListener('blur', cbBlurHost);
      const cbBlurNativeInput = sinon.spy();
      el._inputNode.addEventListener('blur', cbBlurNativeInput);
  
      await triggerFocusFor(el);
  
      expect(document.activeElement).to.equal(el._inputNode);
      expect(cbFocusHost.callCount).to.equal(1);
      expect(cbFocusNativeInput.callCount).to.equal(1);
      expect(cbBlurHost.callCount).to.equal(0);
      expect(cbBlurNativeInput.callCount).to.equal(0);
  
      await triggerBlurFor(el);
      expect(cbBlurHost.callCount).to.equal(1);
      expect(cbBlurNativeInput.callCount).to.equal(1);
  
      await triggerFocusFor(el);
      expect(document.activeElement).to.equal(el._inputNode);
      expect(cbFocusHost.callCount).to.equal(2);
      expect(cbFocusNativeInput.callCount).to.equal(2);
  
      await triggerBlurFor(el);
      expect(cbBlurHost.callCount).to.equal(2);
      expect(cbBlurNativeInput.callCount).to.equal(2);
    });
  
    it('offers simple getter "this.focused" returning true/false for the current focus state', async () => {
      const el = /** @type {LionField} */ (await fixture(html`<${tag}>${inputSlot}</${tag}>`));
      expect(el.focused).to.equal(false);
      await triggerFocusFor(el);
      expect(el.focused).to.equal(true);
      await triggerBlurFor(el);
      expect(el.focused).to.equal(false);
    });
  
    it('can be cleared which erases value, validation and interaction states', async () => {
      const el = /** @type {LionField} */ (await fixture(
        html`<${tag} value="Some value from attribute">${inputSlot}</${tag}>`,
      ));
      el.clear();
      expect(el.modelValue).to.equal('');
      el.modelValue = 'Some value from property';
      expect(el.modelValue).to.equal('Some value from property');
      el.clear();
      expect(el.modelValue).to.equal('');
    });
  
    it('can be reset which restores original modelValue', async () => {
      const el = /** @type {LionField} */ (await fixture(html`
        <${tag} .modelValue="${'foo'}">
          ${inputSlot}
        </${tag}>`));
      expect(el._initialModelValue).to.equal('foo');
      el.modelValue = 'bar';
      el.reset();
      expect(el.modelValue).to.equal('foo');
    });
  
    describe('Accessibility', () => {
      it(`by setting corresponding aria-labelledby (for label) and aria-describedby (for helpText, feedback)
        ~~~
        <lion-field>
          <input
            id="lion-field-[hash]"
            slot="input"
            aria-labelledby="label-[id]"
            aria-describedby="help-text-[id] feedback-[id]"
          />
          <label slot="label"     id="label-[id]"    >[label]   </label>
          <span  slot="help-text" id="help-text-[id]">[helpText]</span>
          <div   slot="feedback"   id="feedback-[id]">[feedback] </span>
        </lion-field>
        ~~~`, async () => {
        const el = /** @type {LionField} */ (await fixture(html`<${tag}>
              <label slot="label">My Name</label>
              ${inputSlot}
              <span slot="help-text">Enter your Name</span>
              <span slot="feedback">No name entered</span>
            </${tag}>
          `));
        const nativeInput = getSlot(el, 'input');
  
        expect(nativeInput.getAttribute('aria-labelledby')).to.equal(`label-${el._inputId}`);
        expect(nativeInput.getAttribute('aria-describedby')).to.contain(`help-text-${el._inputId}`);
        expect(nativeInput.getAttribute('aria-describedby')).to.contain(`feedback-${el._inputId}`);
      });
  
      it(`allows additional slots (prefix, suffix, before, after) to be included in labelledby
      (via attribute data-label) and in describedby (via attribute data-description)`, async () => {
        const el = /** @type {LionField} */ (await fixture(html`<${tag}>
              ${inputSlot}
              <span slot="before" data-label>[before]</span>
              <span slot="after"  data-label>[after]</span>
              <span slot="prefix" data-description>[prefix]</span>
              <span slot="suffix" data-description>[suffix]</span>
            </${tag}>
          `));
  
        const nativeInput = getSlot(el, 'input');
        expect(nativeInput.getAttribute('aria-labelledby')).to.contain(
          `before-${el._inputId} after-${el._inputId}`,
        );
        expect(nativeInput.getAttribute('aria-describedby')).to.contain(
          `prefix-${el._inputId} suffix-${el._inputId}`,
        );
      });
  
      // TODO: Move test below to FormControlMixin.test.js.
      it(`allows to add to aria description or label via addToAriaLabelledBy() and
        addToAriaDescribedBy()`, async () => {
        const wrapper = /** @type {HTMLElement} */ (await fixture(html`
          <div id="wrapper">
            <${tag}>
              ${inputSlot}
              <label slot="label">Added to label by default</label>
              <div slot="feedback">Added to description by default</div>
            </${tag}>
            <div id="additionalLabel"> This also needs to be read whenever the input has focus</div>
            <div id="additionalDescription"> Same for this </div>
          </div>`));
        const el = /** @type {LionField} */ (wrapper.querySelector(tagString));
        // wait until the field element is done rendering
        await el.updateComplete;
        await el.updateComplete;
  
        const { _inputNode } = el;
  
        // 1. addToAriaLabel()
        // Check if the aria attr is filled initially
        expect(_inputNode.getAttribute('aria-labelledby')).to.contain(`label-${el._inputId}`);
        const additionalLabel = /** @type {HTMLElement} */ (wrapper.querySelector(
          '#additionalLabel',
        ));
        el.addToAriaLabelledBy(additionalLabel);
        const labelledbyAttr = /** @type {string} */ (_inputNode.getAttribute('aria-labelledby'));
        // Now check if ids are added to the end (not overridden)
        expect(labelledbyAttr).to.contain(`label-${el._inputId}`);
        // Should be placed in the end
        expect(
          labelledbyAttr.indexOf(`label-${el._inputId}`) < labelledbyAttr.indexOf('additionalLabel'),
        );
  
        // 2. addToAriaDescription()
        // Check if the aria attr is filled initially
        expect(_inputNode.getAttribute('aria-describedby')).to.contain(`feedback-${el._inputId}`);
        const additionalDescription = /** @type {HTMLElement} */ (wrapper.querySelector(
          '#additionalDescription',
        ));
        el.addToAriaDescribedBy(additionalDescription);
        const describedbyAttr = /** @type {string} */ (_inputNode.getAttribute('aria-describedby'));
  
        // Now check if ids are added to the end (not overridden)
        expect(describedbyAttr).to.contain(`feedback-${el._inputId}`);
        // Should be placed in the end
        expect(
          describedbyAttr.indexOf(`feedback-${el._inputId}`) <
            describedbyAttr.indexOf('additionalDescription'),
        );
      });
    });
  
    describe(`Validation`, () => {
      beforeEach(() => {
        // Reset and preload validation translations
        localizeTearDown();
        localize.addData('en-GB', 'lion-validate', {
          error: {
            hasX: 'This is error message for hasX',
          },
        });
      });
  
      it('should conditionally show error', async () => {
        const HasX = class extends Validator {
          static get validatorName() {
            return 'HasX';
          }
  
          /**
           * @param {string} value
           */
          execute(value) {
            const result = value.indexOf('x') === -1;
            return result;
          }
        };
        const el = /** @type {LionField} */ (await fixture(html`
          <${tag}
            .validators=${[new HasX()]}
            .modelValue=${'a@b.nl'}
          >
            ${inputSlot}
          </${tag}>
        `));
  
        /**
         * @param {import("../index.js").LionField} _sceneEl
         * @param {{ index?: number; el: any; wantedShowsFeedbackFor: any; }} scenario
         */
        const executeScenario = async (_sceneEl, scenario) => {
          const sceneEl = _sceneEl;
          sceneEl.resetInteractionState();
          sceneEl.touched = scenario.el.touched;
          sceneEl.dirty = scenario.el.dirty;
          sceneEl.prefilled = scenario.el.prefilled;
          sceneEl.submitted = scenario.el.submitted;
  
          await sceneEl.updateComplete;
          await sceneEl.feedbackComplete;
          expect(sceneEl.showsFeedbackFor).to.deep.equal(scenario.wantedShowsFeedbackFor);
        };
  
        await executeScenario(el, {
          index: 0,
          el: { touched: true, dirty: true, prefilled: false, submitted: false },
          wantedShowsFeedbackFor: ['error'],
        });
        await executeScenario(el, {
          index: 1,
          el: { touched: false, dirty: false, prefilled: true, submitted: false },
          wantedShowsFeedbackFor: ['error'],
        });
  
        await executeScenario(el, {
          index: 2,
          el: { touched: false, dirty: false, prefilled: false, submitted: true },
          wantedShowsFeedbackFor: ['error'],
        });
  
        await executeScenario(el, {
          index: 3,
          el: { touched: false, dirty: true, prefilled: false, submitted: false },
          wantedShowsFeedbackFor: [],
        });
  
        await executeScenario(el, {
          index: 4,
          el: { touched: true, dirty: false, prefilled: false, submitted: false },
          wantedShowsFeedbackFor: [],
        });
      });
      it('should not run validation when disabled', async () => {
        const HasX = class extends Validator {
          static get validatorName() {
            return 'HasX';
          }
  
          /**
           * @param {string} value
           */
          execute(value) {
            const result = value.indexOf('x') === -1;
            return result;
          }
        };
        const disabledEl = /** @type {LionField} */ (await fixture(html`
          <${tag}
            disabled
            .validators=${[new HasX()]}
            .modelValue=${'a@b.nl'}
          >
            ${inputSlot}
          </${tag}>
        `));
        const el = /** @type {LionField} */ (await fixture(html`
          <${tag}
            .validators=${[new HasX()]}
            .modelValue=${'a@b.nl'}
          >
            ${inputSlot}
          </${tag}>
        `));
  
        expect(el.hasFeedbackFor).to.deep.equal(['error']);
        expect(el.validationStates.error.HasX).to.exist;
  
        expect(disabledEl.hasFeedbackFor).to.deep.equal([]);
        expect(disabledEl.validationStates.error).to.deep.equal({});
      });
  
      it('can be required', async () => {
        const el = /** @type {LionField} */ (await fixture(html`
          <${tag}
            .validators=${[new Required()]}
          >${inputSlot}</${tag}>
        `));
        expect(el.hasFeedbackFor).to.deep.equal(['error']);
        expect(el.validationStates.error.Required).to.exist;
        el.modelValue = 'cat';
        expect(el.hasFeedbackFor).to.deep.equal([]);
        expect(el.validationStates.error.Required).to.not.exist;
      });
  
      it('will only update formattedValue when valid on `user-input-changed`', async () => {
        const formatterSpy = sinon.spy(value => `foo: ${value}`);
        const Bar = class extends Validator {
          static get validatorName() {
            return 'Bar';
          }
  
          /**
           * @param {string} value
           */
          execute(value) {
            const hasError = value !== 'bar';
            return hasError;
          }
        };
        const el = /** @type {LionField} */ (await fixture(html`
          <${tag}
            .modelValue=${'init-string'}
            .formatter=${formatterSpy}
            .validators=${[new Bar()]}
          >${inputSlot}</${tag}>
        `));
  
        expect(formatterSpy.callCount).to.equal(0);
        expect(el.formattedValue).to.equal('init-string');
  
        el.modelValue = 'bar';
        expect(formatterSpy.callCount).to.equal(1);
        expect(el.formattedValue).to.equal('foo: bar');
  
        mimicUserInput(el, 'foo');
        expect(formatterSpy.callCount).to.equal(1);
        expect(el.value).to.equal('foo');
      });
    });
  
    describe(`Content projection`, () => {
      it('renders correctly all slot elements in light DOM', async () => {
        const el = /** @type {LionField} */ (await fixture(html`
          <${tag}>
            <label slot="label">[label]</label>
            ${inputSlot}
            <span slot="help-text">[help-text]</span>
            <span slot="before">[before]</span>
            <span slot="after">[after]</span>
            <span slot="prefix">[prefix]</span>
            <span slot="suffix">[suffix]</span>
            <span slot="feedback">[feedback]</span>
          </${tag}>
        `));
  
        const names = [
          'label',
          'input',
          'help-text',
          'before',
          'after',
          'prefix',
          'suffix',
          'feedback',
        ];
        names.forEach(slotName => {
          const slotLight = /** @type {HTMLElement} */ (el.querySelector(`[slot="${slotName}"]`));
          slotLight.setAttribute('test-me', 'ok');
          // @ts-expect-error
          const slot = /** @type {ShadowHTMLElement} */ (el.shadowRoot.querySelector(
            `slot[name="${slotName}"]`,
          ));
          const assignedNodes = slot.assignedNodes();
          expect(assignedNodes.length).to.equal(1);
          expect(assignedNodes[0].getAttribute('test-me')).to.equal('ok');
        });
      });
    });
  });
  