import { ChangeDetectorRef } from '@angular/core';
import { fakeAsync } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { createComponentFactory, createHostFactory, Spectator } from '@ngneat/spectator';
import { UiIconModule } from '@ui/icon';
import { NativeContainerService } from 'native-container';
import { EMPTY, NEVER, of, Subject } from 'rxjs';
import { UiAutocompleteModule } from '../autocomplete';
import { UiSearchboxComponent } from './searchbox.component';

describe('UiSearchbox', () => {
  let spectator: Spectator<UiSearchboxComponent>;

  const createComponent = createComponentFactory({
    component: UiSearchboxComponent,
    imports: [FormsModule, UiIconModule],
  });

  const createHostWithAutocomplete = createHostFactory({
    component: UiSearchboxComponent,
    imports: [FormsModule, UiIconModule, UiAutocompleteModule],
    template: `
    <ui-searchbox>
      <ui-autocomplete *ngFor="let item of items">
        <button [uiAutocompleteItem]="item">{{ item }}</button>
      </ui-autocomplete>
    </ui-searchbox>
    `,
  });

  let nativeContainerMock = {
    isUiWebview: () => ({ isNative: false }),
    openScanner: (type: string) => EMPTY,
  };

  describe('without autocomplete', () => {
    beforeEach(() => {
      spectator = createComponent({
        providers: [{ provide: NativeContainerService, useValue: nativeContainerMock }],
      });
    });

    it('should create', () => {
      expect(spectator.component).toBeTruthy();
    });

    describe('input Element', () => {
      let inputEl: Element;

      beforeEach(() => {
        inputEl = spectator.query('input');
      });

      it('should have the value "Unit Test"', fakeAsync(() => {
        spectator.setInput({ query: 'Unit Test' });
        spectator.tick();
        expect(inputEl).toHaveValue('Unit Test');
      }));

      it('should emit queryChange if input value changes', () => {
        let queryChangeResult;
        spectator.output('queryChange').subscribe((query) => (queryChangeResult = query));
        spectator.typeInElement('My Input', inputEl);
        expect(queryChangeResult).toBe('My Input');
      });

      it('should have "Unit Test" as placeholder', () => {
        spectator.setInput({ placeholder: 'Unit Test' });
        expect(inputEl).toHaveAttribute('placeholder', 'Unit Test');
      });

      it('should focus the input element when focus is called', () => {
        spectator = createComponent({
          props: {
            focusAfterViewInit: false,
          },
        });

        inputEl = spectator.query('input');

        spectator.setInput({ focusAfterViewInit: false });
        spectator.component.focus();
        expect(inputEl).toBeFocused();
      });

      it('should call clearHint on focus', () => {
        spyOn(spectator.component, 'clearHint');
        spectator.focus('input');
        expect(spectator.component.clearHint).toHaveBeenCalled();
      });

      it('should call onKeyup when a Key is pressed', () => {
        spyOn(spectator.component, 'onKeyup');
        spectator.dispatchKeyboardEvent(inputEl, 'keyup', 'Enter');
        expect(spectator.component.onKeyup).toHaveBeenCalled();
      });

      it('should call focusLost on blur', () => {
        spyOn(spectator.component, 'focusLost');
        spectator.blur(inputEl);
        expect(spectator.component.focusLost).toHaveBeenCalled();
      });
    });

    describe('search button', () => {
      it('should show the search button when canScan is false', () => {
        spyOnProperty(spectator.component, 'canScan', 'get').and.returnValue(false);
        spectator.detectComponentChanges();
        expect(spectator.query('.search-button.search')).toBeVisible();
      });

      it('should not show the search button when canScan is true', () => {
        spyOnProperty(spectator.component, 'canScan', 'get').and.returnValue(true);
        spectator.detectComponentChanges();
        expect(spectator.query('.search-button.search')).not.toBeVisible();
      });

      it('should emit search with the current query on click', () => {
        spectator.setInput({
          query: 'my query',
        });
        spyOnProperty(spectator.component, 'canScan', 'get').and.returnValue(false);
        spectator.detectComponentChanges();
        const buttonEl = spectator.query('.search-button.search');

        let searchResult;

        spectator.output('search').subscribe((query) => (searchResult = query));

        spectator.click(buttonEl);

        expect(searchResult).toBe('my query');
      });

      it('should not be visible when loading is true', () => {
        spectator.setInput({ loading: true });
        expect(spectator.query('.search-button.search')).not.toBeVisible();
      });

      it('should be visible when loading is false', () => {
        spectator.setInput({ loading: false });
        expect(spectator.query('.search-button.search')).toBeVisible();
      });
    });

    describe('scan button', () => {
      it('should not be visible when loading is true', () => {
        spectator.setInput({ loading: true });
        expect(spectator.query('.search-button.scan')).not.toBeVisible();
      });

      it('should be visible when loading is false', () => {
        spyOnProperty(spectator.component, 'canScan', 'get').and.returnValue(true);
        spectator.detectComponentChanges();
        spectator.setInput({ loading: false });
        expect(spectator.query('.search-button.scan')).toBeVisible();
      });

      it('should be visible when canScan is true', () => {
        spyOnProperty(spectator.component, 'canScan', 'get').and.returnValue(true);
        spectator.detectComponentChanges();
        expect(spectator.query('.search-button.scan')).toBeVisible();
      });

      it('should not be visible when canScan is false', () => {
        spyOnProperty(spectator.component, 'canScan', 'get').and.returnValue(false);
        spectator.detectComponentChanges();
        expect(spectator.query('.search-button.scan')).not.toBeVisible();
      });

      it('should call startScan on click', () => {
        spyOnProperty(spectator.component, 'canScan', 'get').and.returnValue(true);
        spectator.detectComponentChanges();
        spyOn(spectator.component, 'startScan');
        spectator.click('.search-button.scan');
        expect(spectator.component.startScan).toHaveBeenCalled();
      });
    });

    describe('canScan', () => {
      it('should return true if is Native and query is empty and scanProvider is set', () => {
        spectator.setInput({
          query: '',
          scanProvider: { scan: () => NEVER },
        });
        spyOn(nativeContainerMock, 'isUiWebview').and.returnValue({ isNative: true });

        expect(spectator.component.canScan).toBe(true);
      });

      it('should return false if is Native and query is not empty and scanRequestType is set', () => {
        spectator.setInput({
          query: 'my query',
          scanProvider: { scan: () => NEVER },
        });
        spyOn(nativeContainerMock, 'isUiWebview').and.returnValue({ isNative: true });

        expect(spectator.component.canScan).toBe(false);
      });

      it('should return false if is Native and query is empty and scanRequestType is not set', () => {
        spectator.setInput({
          query: '',
        });
        spyOn(nativeContainerMock, 'isUiWebview').and.returnValue({ isNative: true });

        expect(spectator.component.canScan).toBe(false);
      });

      it('should return false if is not Native and query is empty and scanRequestType is set', () => {
        spectator.setInput({
          query: '',
          scanProvider: { scan: () => NEVER },
        });
        spyOn(nativeContainerMock, 'isUiWebview').and.returnValue({ isNative: false });

        expect(spectator.component.canScan).toBe(false);
      });
    });

    describe('setQuery', () => {
      it('should set the property query and emit queryChange', () => {
        let queryChangeResult;
        spectator.output('queryChange').subscribe((query) => (queryChangeResult = query));
        spectator.component.setQuery('my query');
        expect(spectator.component.query).toBe('my query');
        expect(queryChangeResult).toBe('my query');
      });

      it('should set the property query and not emit queryChange', () => {
        let queryChangeResult;
        spectator.output('queryChange').subscribe((query) => (queryChangeResult = query));
        spectator.component.setQuery('my query', false);
        expect(spectator.component.query).toBe('my query');
        expect(queryChangeResult).toBeUndefined();
      });
    });

    describe('onKeyup', () => {
      it('should emit search with the current query when key is Enter', () => {
        spectator.component.query = 'my query';
        let searchResult;
        spectator.output('search').subscribe((search) => (searchResult = search));
        spectator.component.onKeyup(new KeyboardEvent('', { key: 'Enter' }));
        expect(searchResult).toBe('my query');
      });

      it('should not emit search with the current query when key is ArrowUp or ArrowDown', () => {
        spectator.component.query = 'my query';
        let searchResult;
        spectator.output('search').subscribe((search) => (searchResult = search));
        spectator.component.onKeyup(new KeyboardEvent('', { key: 'ArrowUp' }));
        spectator.component.onKeyup(new KeyboardEvent('', { key: 'ArrowDown' }));
        expect(searchResult).toBeUndefined();
      });

      it('should call handleArrowUpDownEvent when key is ArrowUp', () => {
        spyOn(spectator.component, 'handleArrowUpDownEvent');
        spectator.component.onKeyup(new KeyboardEvent('', { key: 'ArrowUp' }));
        expect(spectator.component.handleArrowUpDownEvent).toHaveBeenCalled();
      });

      it('should call handleArrowUpDownEvent when key is ArrowDown', () => {
        spyOn(spectator.component, 'handleArrowUpDownEvent');
        spectator.component.onKeyup(new KeyboardEvent('', { key: 'ArrowDown' }));
        expect(spectator.component.handleArrowUpDownEvent).toHaveBeenCalled();
      });

      it('should not call handleArrowUpDownEvent when key is Enter', () => {
        spyOn(spectator.component, 'handleArrowUpDownEvent');
        spectator.component.onKeyup(new KeyboardEvent('', { key: 'Enter' }));
        expect(spectator.component.handleArrowUpDownEvent).not.toHaveBeenCalled();
      });
    });

    describe('startScan', () => {
      it('should call scanProvider.scan', () => {
        spectator.setInput({
          scanProvider: { scan: () => NEVER },
        });

        spyOn(spectator.component.scanProvider, 'scan').and.callThrough();

        spectator.component.startScan();

        expect(spectator.component.scanProvider.scan).toHaveBeenCalled();
      });

      it('should emit scan and call setQuery', () => {
        spectator.setInput({
          scanProvider: { scan: () => of('my scan') },
        });
        let scanResult;
        spectator.output('scan').subscribe((scan) => (scanResult = scan));

        spyOn(spectator.component, 'setQuery').and.callThrough();

        spectator.component.startScan();

        expect(scanResult).toBe('my scan');
        expect(spectator.component.setQuery).toHaveBeenCalledWith('my scan');
      });

      it('should not emit scan and not call setQuery', () => {
        spectator.setInput({
          scanProvider: { scan: () => NEVER },
        });
        let scanResult;
        spectator.output('scan').subscribe((scan) => (scanResult = scan));

        spyOn(spectator.component, 'setQuery').and.callThrough();

        spectator.component.startScan();

        expect(scanResult).toBeUndefined();
        expect(spectator.component.setQuery).not.toHaveBeenCalled();
      });

      it('should call subscriptions.add if scanRequestType is set', () => {
        spectator.setInput({
          scanProvider: { scan: () => NEVER },
        });
        spyOn(spectator.component.subscriptions, 'add');
        spectator.component.startScan();
        expect(spectator.component.subscriptions.add).toHaveBeenCalled();
      });

      it('should not call subscriptions.add if scanRequestType is not set', () => {
        spyOn(spectator.component.subscriptions, 'add');
        try {
          spectator.component.startScan();
        } catch {}
        expect(spectator.component.subscriptions.add).not.toHaveBeenCalled();
      });
    });

    describe('canClear', () => {
      it('should return true if query is not empty', () => {
        spectator.setInput({ query: 'my query' });
        expect(spectator.component.canClear).toBe(true);
      });

      it('should return false if query is empty', () => {
        expect(spectator.component.canClear).toBe(false);
      });
    });

    describe('clear button', () => {
      it('should be visible if canClear is true', () => {
        spyOnProperty(spectator.component, 'canClear', 'get').and.returnValue(true);
        spectator.detectComponentChanges();
        expect(spectator.query('.clear-button')).toBeVisible();
      });

      it('should not be visible if canClear is false', () => {
        spyOnProperty(spectator.component, 'canClear', 'get').and.returnValue(false);
        spectator.detectComponentChanges();
        expect(spectator.query('.clear-button')).not.toBeVisible();
      });

      it('should call setQuery with empty string and focus on click', () => {
        spyOnProperty(spectator.component, 'canClear', 'get').and.returnValue(true);
        spyOn(spectator.component, 'focus');
        spectator.detectComponentChanges();

        spyOn(spectator.component, 'setQuery');
        spectator.click('.clear-button');
        expect(spectator.component.setQuery).toHaveBeenCalledWith('');
        expect(spectator.component.focus).toHaveBeenCalled();
      });
    });

    describe('showHint', () => {
      it('should return true if hint is set', () => {
        spectator.setInput({ hint: 'my hint' });
        expect(spectator.component.showHint).toBe(true);
      });

      it('should return false if hint is not set', () => {
        expect(spectator.component.showHint).toBe(false);
      });
    });

    describe('.hint', () => {
      it('should be visible if showHint is true', () => {
        spyOnProperty(spectator.component, 'showHint', 'get').and.returnValue(true);
        spectator.detectComponentChanges();
        expect(spectator.query('.hint')).toBeVisible();
      });

      it('should not be visible if showHint is false', () => {
        spyOnProperty(spectator.component, 'showHint', 'get').and.returnValue(false);
        spectator.detectComponentChanges();
        expect(spectator.query('.hint')).not.toBeVisible();
      });

      it('should call focus on click', () => {
        spectator.setInput({ hint: 'my hint' });
        spectator.detectComponentChanges();
        spyOn(spectator.component, 'focus');
        spectator.click('.hint');
        expect(spectator.component.focus).toHaveBeenCalled();
      });

      it('should render the hint input as text', () => {
        spectator.setInput({ hint: 'my hint' });
        spectator.detectComponentChanges();
        expect(spectator.query('.hint')).toHaveText('my hint', true);
      });
    });

    describe('clearHint', () => {
      it('should set hint to empty string', () => {
        spectator.setInput({ hint: 'my hint' });
        spectator.component.clearHint();
        expect(spectator.component.hint).toBe('');
      });
    });

    describe('focus', () => {
      it('should focus the input element', () => {
        spyOn(spectator.component.input.nativeElement, 'focus');
        spectator.component.focus();
        expect(spectator.component.input.nativeElement.focus).toHaveBeenCalled();
      });
    });

    describe('ngAfterViewInit', () => {
      it('should call focus if focusAfterViewInit is true', () => {
        spectator.setInput({ focusAfterViewInit: true });
        spyOn(spectator.component, 'focus');
        spectator.component.ngAfterViewInit();
        expect(spectator.component.focus).toHaveBeenCalled();
      });

      it('should not call focus if focusAfterViewInit is false', () => {
        spectator.setInput({ focusAfterViewInit: false });
        spyOn(spectator.component, 'focus');
        spectator.component.ngAfterViewInit();
        expect(spectator.component.focus).not.toHaveBeenCalled();
      });
    });

    describe('ngOnDestroy', () => {
      it('should unsubscribe all subscriptions', () => {
        spyOn(spectator.component.subscriptions, 'unsubscribe');
        spectator.component.ngOnDestroy();
        expect(spectator.component.subscriptions.unsubscribe).toHaveBeenCalled();
      });
    });

    describe('.load-indicator', () => {
      it('should be visible when loading is true', () => {
        spectator.setInput({ loading: true });
        expect(spectator.query('.load-indicator')).toBeVisible();
      });

      it('should not be visible when loading is false', () => {
        spectator.setInput({ loading: false });
        expect(spectator.query('.load-indicator')).not.toBeVisible();
      });
    });
  });

  describe('with autocomplete', () => {
    beforeEach(() => {
      spectator = createHostWithAutocomplete(undefined, {
        hostProps: {
          items: ['First item', 'Second Item'],
        },
      });
    });

    describe('handleArrowUpDownEvent', () => {
      it('should call handleKeyboardEvent with the event', () => {
        const event = new KeyboardEvent('keyup');
        spyOn(spectator.component.autocomplete, 'handleKeyboardEvent');
        spectator.component.handleArrowUpDownEvent(event);

        expect(spectator.component.autocomplete.handleKeyboardEvent).toHaveBeenCalledWith(event);
      });

      it('should call autocompleteValueSelector and setQuery when autocomplete.selectedItem is set', () => {
        const event = new KeyboardEvent('keyup');
        spyOn(spectator.component, 'autocompleteValueSelector').and.returnValue('my query');
        spyOn(spectator.component, 'setQuery');

        spectator.component.autocomplete.listKeyManager.setFirstItemActive();

        spectator.component.handleArrowUpDownEvent(event);
        expect(spectator.component.autocompleteValueSelector).toHaveBeenCalledWith('First item');
        expect(spectator.component.setQuery).toHaveBeenCalledWith('my query', false);
      });
    });

    describe('focusLost', () => {
      it('should call close on autocomplete', fakeAsync(() => {
        spyOn(spectator.component.autocomplete, 'close');
        spectator.component.focusLost();
        spectator.tick(100);
        expect(spectator.component.autocomplete.close).toHaveBeenCalled();
      }));
    });
  });
});
