import { aTimeout, expect, fixture, html } from '@open-wc/testing';
import './nile-otp-input';
import type { NileOtpInput } from './nile-otp-input';

const getCells = (el: NileOtpInput) =>
  Array.from(el.shadowRoot!.querySelectorAll<HTMLInputElement>('.otp__cell'));

const inputCell = async (el: NileOtpInput, index: number, value: string) => {
  const cell = getCells(el)[index];
  cell.value = value;
  cell.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
  await el.updateComplete;
  return cell;
};

const pressKey = async (
  el: NileOtpInput,
  index: number,
  key: string,
  opts: Partial<KeyboardEventInit> = {}
) => {
  const cell = getCells(el)[index];
  cell.dispatchEvent(
    new KeyboardEvent('keydown', {
      key,
      bubbles: true,
      composed: true,
      cancelable: true,
      ...opts,
    })
  );
  await el.updateComplete;
  return cell;
};

const pasteInto = async (el: NileOtpInput, index: number, text: string) => {
  const cell = getCells(el)[index];
  const event = new Event('paste', {
    bubbles: true,
    composed: true,
    cancelable: true,
  }) as ClipboardEvent;
  Object.defineProperty(event, 'clipboardData', {
    value: { getData: () => text },
  });
  cell.dispatchEvent(event);
  await el.updateComplete;
};

describe('NileOtpInput', () => {
  // ---- Rendering ----

  it('renders default 6 cells', async () => {
    const el = await fixture<NileOtpInput>(
      html`<nile-otp-input></nile-otp-input>`
    );
    expect(el).to.exist;
    expect(getCells(el).length).to.equal(6);
  });

  it('normalizes incoming value (strips non-numeric in numeric mode)', async () => {
    const el = await fixture<NileOtpInput>(
      html`<nile-otp-input value="12A34B"></nile-otp-input>`
    );
    await el.updateComplete;
    expect(el.value).to.equal('1234');
    expect(getCells(el).map(c => c.value)).to.deep.equal([
      '1', '2', '3', '4', '', '',
    ]);
  });

  it('supports configurable length and clamps below 4', async () => {
    const el = await fixture<NileOtpInput>(
      html`<nile-otp-input length="8"></nile-otp-input>`
    );
    expect(getCells(el).length).to.equal(8);

    el.length = 2;
    await el.updateComplete;
    expect(el.length).to.equal(4);
    expect(getCells(el).length).to.equal(4);
  });

  it('renders label when provided', async () => {
    const el = await fixture<NileOtpInput>(
      html`<nile-otp-input label="Verification code"></nile-otp-input>`
    );
    const label = el.shadowRoot!.querySelector('.form-control__label');
    expect(label).to.exist;
    expect(label!.textContent).to.contain('Verification code');
  });

  it('renders help text when provided', async () => {
    const el = await fixture<NileOtpInput>(
      html`<nile-otp-input .helpText=${'Enter your code'}></nile-otp-input>`
    );
    const helpText = el.shadowRoot!.querySelector('.form-control__help-text');
    expect(helpText).to.exist;
    expect(helpText!.textContent).to.contain('Enter your code');
  });

  it('renders error message when provided', async () => {
    const el = await fixture<NileOtpInput>(
      html`<nile-otp-input .errorMessage=${'Invalid code'}></nile-otp-input>`
    );
    const errorMsg = el.shadowRoot!.querySelector('.form-control__error-message');
    expect(errorMsg).to.exist;
    expect(errorMsg!.textContent).to.contain('Invalid code');
  });

  it('renders separators with separator-every', async () => {
    const el = await fixture<NileOtpInput>(html`
      <nile-otp-input separator="-" .separatorEvery=${3}></nile-otp-input>
    `);
    const separators = el.shadowRoot!.querySelectorAll('.otp__separator');
    expect(separators.length).to.equal(1);
    expect(separators[0].textContent!.trim()).to.equal('-');
  });

  it('renders separators from separator-positions', async () => {
    const el = await fixture<NileOtpInput>(html`
      <nile-otp-input
        length="8"
        separator="/"
        separator-positions="1, 3, 5"
      ></nile-otp-input>
    `);
    const separators = el.shadowRoot!.querySelectorAll('.otp__separator');
    expect(separators.length).to.equal(3);
    separators.forEach(s =>
      expect(s.textContent!.trim()).to.equal('/')
    );
  });

  // ---- Input behavior ----

  it('auto-advances focus to next empty cell after typing', async () => {
    const el = await fixture<NileOtpInput>(
      html`<nile-otp-input></nile-otp-input>`
    );
    const cells = getCells(el);

    cells[0].focus();
    await inputCell(el, 0, '1');
    await aTimeout(0);

    expect(el.value).to.equal('1');
    expect(el.shadowRoot!.activeElement).to.equal(cells[1]);
  });

  it('typing a character on a filled cell replaces it and advances', async () => {
    const el = await fixture<NileOtpInput>(
      html`<nile-otp-input value="123456"></nile-otp-input>`
    );
    await el.updateComplete;
    const cells = getCells(el);

    cells[2].focus();
    await el.updateComplete;
    await pressKey(el, 2, '9');
    await aTimeout(0);

    expect(el.value[2]).to.equal('9');
  });

  it('blocks invalid characters in numeric mode', async () => {
    const el = await fixture<NileOtpInput>(
      html`<nile-otp-input></nile-otp-input>`
    );
    const first = getCells(el)[0];
    first.focus();

    const event = new KeyboardEvent('keydown', {
      key: 'A',
      bubbles: true,
      composed: true,
      cancelable: true,
    });
    first.dispatchEvent(event);

    expect(event.defaultPrevented).to.be.true;
  });

  it('allows alphanumeric values when alphanumeric attribute is set', async () => {
    const el = await fixture<NileOtpInput>(
      html`<nile-otp-input alphanumeric></nile-otp-input>`
    );
    const cells = getCells(el);
    cells[0].focus();
    await el.updateComplete;

    await pressKey(el, 0, 'A');
    expect(el.value).to.equal('A');
  });

  it('alphanumeric overrides numeric-only', async () => {
    const el = await fixture<NileOtpInput>(
      html`<nile-otp-input numeric-only alphanumeric></nile-otp-input>`
    );
    const cells = getCells(el);
    cells[0].focus();
    await el.updateComplete;

    await pressKey(el, 0, 'B');
    expect(el.value).to.equal('B');
  });

  it('supports controlled value updates', async () => {
    const el = await fixture<NileOtpInput>(
      html`<nile-otp-input></nile-otp-input>`
    );

    el.value = '2468';
    await el.updateComplete;

    expect(getCells(el).map(c => c.value)).to.deep.equal([
      '2', '4', '6', '8', '', '',
    ]);
  });

  // ---- Focus management (ShadCN style) ----

  it('focus() targets the first empty cell', async () => {
    const el = await fixture<NileOtpInput>(
      html`<nile-otp-input value="12"></nile-otp-input>`
    );
    await el.updateComplete;
    const cells = getCells(el);

    el.focus();
    await aTimeout(0);

    expect(el.shadowRoot!.activeElement).to.equal(cells[2]);
  });

  it('clicking an empty cell beyond the cursor redirects to first empty', async () => {
    const el = await fixture<NileOtpInput>(
      html`<nile-otp-input value="12"></nile-otp-input>`
    );
    await el.updateComplete;
    const cells = getCells(el);

    cells[5].focus();
    await aTimeout(0);
    await el.updateComplete;

    expect(el.shadowRoot!.activeElement).to.equal(cells[2]);
  });

  it('clicking a filled cell focuses it directly', async () => {
    const el = await fixture<NileOtpInput>(
      html`<nile-otp-input value="123"></nile-otp-input>`
    );
    await el.updateComplete;
    const cells = getCells(el);

    cells[1].focus();
    await aTimeout(0);
    await el.updateComplete;

    expect(el.shadowRoot!.activeElement).to.equal(cells[1]);
  });

  // ---- Keyboard navigation ----

  it('Backspace clears current cell and retreats', async () => {
    const el = await fixture<NileOtpInput>(
      html`<nile-otp-input value="123"></nile-otp-input>`
    );
    await el.updateComplete;
    const cells = getCells(el);

    cells[2].focus();
    await pressKey(el, 2, 'Backspace');
    await aTimeout(0);

    expect(el.value).to.equal('12');
    expect(el.shadowRoot!.activeElement).to.equal(cells[1]);
  });

  it('Backspace on empty cell clears previous and moves there', async () => {
    const el = await fixture<NileOtpInput>(
      html`<nile-otp-input value="12"></nile-otp-input>`
    );
    await el.updateComplete;
    const cells = getCells(el);

    cells[2].focus();
    await pressKey(el, 2, 'Backspace');
    await aTimeout(0);

    expect(el.value).to.equal('1');
    expect(cells[1].value).to.equal('');
    expect(el.shadowRoot!.activeElement).to.equal(cells[1]);
  });

  it('Delete clears current cell without moving', async () => {
    const el = await fixture<NileOtpInput>(
      html`<nile-otp-input value="123"></nile-otp-input>`
    );
    await el.updateComplete;
    const cells = getCells(el);

    cells[1].focus();
    await pressKey(el, 1, 'Delete');
    await aTimeout(0);

    expect(el.value).to.equal('13');
    expect(el.shadowRoot!.activeElement).to.equal(cells[1]);
  });

  it('ArrowLeft moves focus back', async () => {
    const el = await fixture<NileOtpInput>(
      html`<nile-otp-input value="123"></nile-otp-input>`
    );
    await el.updateComplete;
    const cells = getCells(el);

    cells[2].focus();
    await pressKey(el, 2, 'ArrowLeft');
    await aTimeout(0);

    expect(el.shadowRoot!.activeElement).to.equal(cells[1]);
  });

  it('ArrowRight cannot move past the first empty cell', async () => {
    const el = await fixture<NileOtpInput>(
      html`<nile-otp-input value="12"></nile-otp-input>`
    );
    await el.updateComplete;
    const cells = getCells(el);

    cells[1].focus();
    await pressKey(el, 1, 'ArrowRight');
    await aTimeout(0);

    expect(el.shadowRoot!.activeElement).to.equal(cells[2]);

    await pressKey(el, 2, 'ArrowRight');
    await aTimeout(0);

    expect(el.shadowRoot!.activeElement).to.equal(cells[2]);
  });

  it('Home goes to first cell', async () => {
    const el = await fixture<NileOtpInput>(
      html`<nile-otp-input value="123"></nile-otp-input>`
    );
    await el.updateComplete;
    const cells = getCells(el);

    cells[2].focus();
    await pressKey(el, 2, 'Home');
    await aTimeout(0);

    expect(el.shadowRoot!.activeElement).to.equal(cells[0]);
  });

  it('End goes to first empty cell', async () => {
    const el = await fixture<NileOtpInput>(
      html`<nile-otp-input value="123"></nile-otp-input>`
    );
    await el.updateComplete;
    const cells = getCells(el);

    cells[0].focus();
    await pressKey(el, 0, 'End');
    await aTimeout(0);

    expect(el.shadowRoot!.activeElement).to.equal(cells[3]);
  });

  it('Space is blocked', async () => {
    const el = await fixture<NileOtpInput>(
      html`<nile-otp-input></nile-otp-input>`
    );
    const cells = getCells(el);
    cells[0].focus();

    const event = new KeyboardEvent('keydown', {
      key: ' ',
      bubbles: true,
      composed: true,
      cancelable: true,
    });
    cells[0].dispatchEvent(event);

    expect(event.defaultPrevented).to.be.true;
  });

  // ---- Paste ----

  it('paste fills from position 0 and focuses next empty', async () => {
    const el = await fixture<NileOtpInput>(
      html`<nile-otp-input></nile-otp-input>`
    );

    await pasteInto(el, 3, '987654');

    expect(el.value).to.equal('987654');
    expect(getCells(el).map(c => c.value).join('')).to.equal('987654');
  });

  it('paste emits nile-paste event', async () => {
    const el = await fixture<NileOtpInput>(
      html`<nile-otp-input></nile-otp-input>`
    );
    let pasteValue = '';

    el.addEventListener('nile-paste', (e: Event) => {
      pasteValue = (e as CustomEvent).detail.value;
    });

    await pasteInto(el, 0, '123456');

    expect(pasteValue).to.equal('123456');
  });

  it('paste is ignored when disabled', async () => {
    const el = await fixture<NileOtpInput>(
      html`<nile-otp-input disabled></nile-otp-input>`
    );

    await pasteInto(el, 0, '123456');

    expect(el.value).to.equal('');
  });

  // ---- Visual states ----

  it('applies warning class', async () => {
    const el = await fixture<NileOtpInput>(
      html`<nile-otp-input warning></nile-otp-input>`
    );
    const base = el.shadowRoot!.querySelector('.otp');
    expect(base!.classList.contains('otp--warning')).to.be.true;
  });

  it('applies error class', async () => {
    const el = await fixture<NileOtpInput>(
      html`<nile-otp-input error></nile-otp-input>`
    );
    const base = el.shadowRoot!.querySelector('.otp');
    expect(base!.classList.contains('otp--error')).to.be.true;
  });

  it('applies success class', async () => {
    const el = await fixture<NileOtpInput>(
      html`<nile-otp-input success></nile-otp-input>`
    );
    const base = el.shadowRoot!.querySelector('.otp');
    expect(base!.classList.contains('otp--success')).to.be.true;
  });

  it('applies disabled class and blocks input', async () => {
    const el = await fixture<NileOtpInput>(
      html`<nile-otp-input disabled></nile-otp-input>`
    );
    const base = el.shadowRoot!.querySelector('.otp');
    expect(base!.classList.contains('otp--disabled')).to.be.true;

    const first = getCells(el)[0];
    first.value = '9';
    first.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
    await el.updateComplete;

    expect(el.value).to.equal('');
    expect(first.value).to.equal('');
  });

  it('applies readonly class and blocks input', async () => {
    const el = await fixture<NileOtpInput>(
      html`<nile-otp-input readonly value="123456"></nile-otp-input>`
    );
    const base = el.shadowRoot!.querySelector('.otp');
    expect(base!.classList.contains('otp--readonly')).to.be.true;

    const first = getCells(el)[0];
    first.value = '9';
    first.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
    await el.updateComplete;

    expect(el.value).to.equal('123456');
  });

  // ---- Masked ----

  it('masked cells use type="password" for filled non-active cells', async () => {
    const el = await fixture<NileOtpInput>(
      html`<nile-otp-input masked value="12"></nile-otp-input>`
    );
    await el.updateComplete;
    const cells = getCells(el);

    expect(cells[0].type).to.equal('password');
    expect(cells[1].type).to.equal('password');
    expect(cells[2].type).to.equal('text');
  });

  it('active masked cell shows text while typing', async () => {
    const el = await fixture<NileOtpInput>(
      html`<nile-otp-input masked value="12"></nile-otp-input>`
    );
    await el.updateComplete;
    const cells = getCells(el);

    cells[1].focus();
    await el.updateComplete;

    expect(cells[1].type).to.equal('text');
    expect(cells[0].type).to.equal('password');
  });

  it('unmasked cells are always type="text"', async () => {
    const el = await fixture<NileOtpInput>(
      html`<nile-otp-input value="12"></nile-otp-input>`
    );
    await el.updateComplete;
    const cells = getCells(el);

    cells.forEach(c => expect(c.type).to.equal('text'));
  });

  // ---- Events ----

  it('emits nile-input and nile-change with value detail', async () => {
    const el = await fixture<NileOtpInput>(
      html`<nile-otp-input></nile-otp-input>`
    );
    let inputDetail: any;
    let changeDetail: any;

    el.addEventListener('nile-input', (e: Event) => {
      inputDetail = (e as CustomEvent).detail;
    });
    el.addEventListener('nile-change', (e: Event) => {
      changeDetail = (e as CustomEvent).detail;
    });

    getCells(el)[0].focus();
    await el.updateComplete;
    await pressKey(el, 0, '7');

    expect(inputDetail.value).to.equal('7');
    expect(inputDetail.complete).to.be.false;
    expect(changeDetail.value).to.equal('7');
  });

  it('emits nile-complete when all cells are filled', async () => {
    const el = await fixture<NileOtpInput>(
      html`<nile-otp-input length="4" value="123"></nile-otp-input>`
    );
    await el.updateComplete;
    let completedValue = '';

    el.addEventListener('nile-complete', (e: Event) => {
      completedValue = (e as CustomEvent).detail.value;
    });

    const cells = getCells(el);
    cells[3].focus();
    await el.updateComplete;
    await pressKey(el, 3, '4');

    expect(completedValue).to.equal('1234');
  });

  it('emits nile-focus when entering and nile-blur when leaving', async () => {
    const el = await fixture<NileOtpInput>(
      html`<nile-otp-input></nile-otp-input>`
    );
    let focused = false;
    let blurred = false;

    el.addEventListener('nile-focus', () => { focused = true; });
    el.addEventListener('nile-blur', () => { blurred = true; });

    getCells(el)[0].focus();
    await el.updateComplete;
    expect(focused).to.be.true;

    getCells(el)[0].blur();
    await aTimeout(50);
    expect(blurred).to.be.true;
  });

  // ---- Public methods ----

  it('clear() empties all cells', async () => {
    const el = await fixture<NileOtpInput>(
      html`<nile-otp-input value="123456"></nile-otp-input>`
    );
    await el.updateComplete;

    el.clear();
    await el.updateComplete;

    expect(el.value).to.equal('');
    getCells(el).forEach(c => expect(c.value).to.equal(''));
  });

  it('clear() is blocked when disabled', async () => {
    const el = await fixture<NileOtpInput>(
      html`<nile-otp-input disabled value="123456"></nile-otp-input>`
    );
    await el.updateComplete;

    el.clear();
    await el.updateComplete;

    expect(el.value).to.equal('123456');
  });

  it('complete getter returns true when all cells filled', async () => {
    const el = await fixture<NileOtpInput>(
      html`<nile-otp-input value="123456"></nile-otp-input>`
    );
    await el.updateComplete;
    expect(el.complete).to.be.true;
  });

  it('complete getter returns false when cells are empty', async () => {
    const el = await fixture<NileOtpInput>(
      html`<nile-otp-input value="12"></nile-otp-input>`
    );
    await el.updateComplete;
    expect(el.complete).to.be.false;
  });

  // ---- Validation ----

  it('validates required and exact length using checkValidity', async () => {
    const el = await fixture<NileOtpInput>(
      html`<nile-otp-input required></nile-otp-input>`
    );
    el.value = '12';
    await el.updateComplete;
    expect(el.checkValidity()).to.be.false;

    el.value = '123456';
    await el.updateComplete;
    expect(el.checkValidity()).to.be.true;
  });

  it('supports custom validity messaging', async () => {
    const el = await fixture<NileOtpInput>(
      html`<nile-otp-input value="123456"></nile-otp-input>`
    );
    el.setCustomValidity('Invalid OTP');
    await el.updateComplete;

    expect(el.checkValidity()).to.be.false;
    expect(el.validationMessage).to.equal('Invalid OTP');

    el.setCustomValidity('');
    await el.updateComplete;
    expect(el.checkValidity()).to.be.true;
  });

  it('validates against custom pattern', async () => {
    const el = await fixture<NileOtpInput>(
      html`<nile-otp-input pattern="[0-9]{6}" value="1234"></nile-otp-input>`
    );
    await el.updateComplete;
    expect(el.checkValidity()).to.be.false;

    el.value = '123456';
    await el.updateComplete;
    expect(el.checkValidity()).to.be.true;
  });

  // ---- Form integration ----

  it('participates in form data submission', async () => {
    const form = await fixture<HTMLFormElement>(html`
      <form id="otp-form">
        <nile-otp-input name="otp" value="321654"></nile-otp-input>
      </form>
    `);
    const data = new FormData(form);

    expect(data.get('otp')).to.equal('321654');
  });

  // ---- Placeholder ----

  it('shows no placeholder by default', async () => {
    const el = await fixture<NileOtpInput>(html`
      <nile-otp-input value="12"></nile-otp-input>
    `);

    const placeholders = getCells(el).map(c => c.getAttribute('placeholder'));
    expect(placeholders).to.deep.equal([null, null, null, null, null, null]);
  });

  it('moves custom placeholder to active cell on focus', async () => {
    const el = await fixture<NileOtpInput>(
      html`<nile-otp-input placeholder="0"></nile-otp-input>`
    );
    const cells = getCells(el);

    cells[0].focus();
    await el.updateComplete;

    const placeholders = getCells(el).map(c => c.getAttribute('placeholder'));
    expect(placeholders[0]).to.equal('0');
    expect(placeholders.slice(1).every(p => p === null)).to.be.true;
  });

  // ---- CSS parts ----

  it('exposes all required CSS parts', async () => {
    const el = await fixture<NileOtpInput>(html`
      <nile-otp-input
        label="Test"
        .helpText=${'Help'}
        .errorMessage=${'Error'}
        separator="-"
        .separatorEvery=${3}
      ></nile-otp-input>
    `);

    expect(el.shadowRoot!.querySelector('[part="form-control"]')).to.exist;
    expect(el.shadowRoot!.querySelector('[part="form-control-label"]')).to.exist;
    expect(el.shadowRoot!.querySelector('[part="form-control-input"]')).to.exist;
    expect(el.shadowRoot!.querySelector('[part="base"]')).to.exist;
    expect(el.shadowRoot!.querySelector('[part="cell"]')).to.exist;
    expect(el.shadowRoot!.querySelector('[part="separator"]')).to.exist;
    expect(el.shadowRoot!.querySelector('[part="form-control-help-text"]')).to.exist;
    expect(el.shadowRoot!.querySelector('[part="form-control-error-message"]')).to.exist;
  });
});
