import { shallow } from 'enzyme';
import * as React from 'react';
import { RNCamera } from 'react-native-camera';
import sinon from 'sinon';

import { DefaultScannerMode } from '~/models/app-settings';
import { bleManager } from '~/services/ble-manager';
import { mockUseState, mockUseEffect, mockUseRef } from '~/test/mocks';
import { assert, noop } from '~/test/utils';

import { Scanner } from './scanner';
import { ScannerBle } from './scanner-ble';
import { ScannerCamera } from './scanner-camera';
import { ScannerHid } from './scanner-hid';

const BARCODE_TYPE_OK = 'magnetic_flux_density_3d';
const BARCODE_1 = 'FAKE_BARCODE_1';
const BARCODE_20_1 = 'FAKE_BARCODE_20_1___';
const BARCODE_20_2 = 'REST';
const BARCODE_20 = BARCODE_20_1 + BARCODE_20_2;

describe('Scanner', () => {
  it('onBarcodeDetected works in hid mode', () => {
    const onBarcodeDetected = sinon.spy();
    const wrapper = shallow(<Scanner mode="hid" onBarcodeDetected={onBarcodeDetected} />);

    wrapper.find(ScannerHid).prop('onBarcodeDetected')('AAAA');

    assert.calledOnceWithExactly(onBarcodeDetected, 'AAAA');
  });

  it('onBarcodeDetected works in ble mode', () => {
    const onBarcodeDetected = sinon.spy();
    const wrapper = shallow(<Scanner mode="ble" onBarcodeDetected={onBarcodeDetected} />);

    wrapper.find(ScannerBle).prop('onBarcodeDetected')('AAAA');

    assert.calledOnceWithExactly(onBarcodeDetected, 'AAAA');
  });

  it('onBarcodeDetected works in camera mode', () => {
    const onBarcodeDetected = sinon.spy();
    const wrapper = shallow(<Scanner mode="camera" onBarcodeDetected={onBarcodeDetected} />);

    wrapper.find(ScannerCamera).prop('onBarcodeDetected')('AAAA');

    assert.calledOnceWithExactly(onBarcodeDetected, 'AAAA');
  });

  it("DefaultScannerMode is 'ble'", function () {
    expect(DefaultScannerMode).toBe('ble');
  });
});

describe('ScannerBle', () => {
  it('register data listener on component mount', () => {
    const stub = sinon.stub(bleManager, 'addListener');

    shallow(<ScannerBle onBarcodeDetected={noop} />);

    assert.calledOnceWithInitialArgs(stub, 'data');
    assert.argIsInstanceOf(stub.firstCall, 1, Function);

    stub.restore();
  });

  it('remove data listener when the component unmount', async function () {
    const onBarcodeDetected = sinon.spy();
    const timestamp = Date.now();
    const data = {
      type: BARCODE_TYPE_OK,
      value: BARCODE_1,
      timestamp,
    };
    const useEffect = mockUseEffect();
    shallow(<ScannerBle onBarcodeDetected={onBarcodeDetected} />);

    const { cleanUp, useEffectMock } = await useEffect;
    cleanUp();
    bleManager.bus.emit('data', data);

    assert.notCalled(onBarcodeDetected);

    useEffectMock.restore();
  });

  it('simulates valid data received from BLE scanner', () => {
    const onBarcodeDetected = sinon.spy();
    const timestamp = Date.now();
    const data = {
      type: BARCODE_TYPE_OK,
      value: BARCODE_1,
      timestamp,
    };

    shallow(<ScannerBle onBarcodeDetected={onBarcodeDetected} />);

    bleManager.bus.emit('data', data);

    assert.calledOnceWithExactly(onBarcodeDetected, BARCODE_1);
  });

  it('listener is removed from bleManager when the component unmounts', async function () {
    const setDeviceId = sinon.spy();

    const useState = mockUseState()
      .onCall(0)
      .callsFake((init) => [init, setDeviceId]);

    const useEffect = mockUseEffect();

    shallow(<ScannerBle onBarcodeDetected={() => null} />);

    const { cleanUp, useEffectMock } = await useEffect;
    cleanUp();

    assert.notCalled(setDeviceId);

    useState.restore();
    useEffectMock.restore();
  });

  it('simulates a barcode with more than 20 digits', async function () {
    const onBarcodeDetected = sinon.spy();
    const timestamp = Date.now();
    const data1 = {
      type: BARCODE_TYPE_OK,
      value: BARCODE_20_1,
      timestamp,
    };
    const data2 = {
      type: BARCODE_TYPE_OK,
      value: BARCODE_20_2,
      timestamp: timestamp + 1000,
    };

    shallow(<ScannerBle onBarcodeDetected={onBarcodeDetected} />);

    bleManager.bus.emit('data', data1);
    bleManager.bus.emit('data', data2);

    assert.calledOnceWithExactly(onBarcodeDetected, BARCODE_20);
    expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 100);
    expect(clearTimeout).toHaveBeenCalled();
  });

  it('simulates a barcode with 20 digits and wait until timeout finish', function () {
    const onBarcodeDetected = sinon.spy();
    const timestamp = Date.now();
    const data = {
      type: BARCODE_TYPE_OK,
      value: BARCODE_20_1,
      timestamp,
    };

    shallow(<ScannerBle onBarcodeDetected={onBarcodeDetected} />);
    bleManager.bus.emit('data', data);
    jest.runAllTimers();

    assert.calledOnceWithExactly(onBarcodeDetected, BARCODE_20_1);
  });
});

describe('ScannerCamera', () => {
  const DUMMY_BARCODE: any = {
    data: 'AAAA',
  };
  const DUMMY_BARCODE2: any = {
    data: 'BBBB',
  };

  it('simulates barcode data onBarcodeDetected is called', () => {
    const onBarcodeDetected = sinon.spy();
    const wrapper = shallow(<ScannerCamera onBarcodeDetected={onBarcodeDetected} />);

    wrapper.find(RNCamera).prop('onBarCodeRead')(DUMMY_BARCODE);

    assert.calledOnceWithExactly(onBarcodeDetected, 'AAAA');
  });

  it('simulates twice barcode data onBarcodeDetected is called without delay', () => {
    const onBarcodeDetected = sinon.spy();
    const wrapper = shallow(
      <ScannerCamera cameraBarcodeReadTimeout={100} onBarcodeDetected={onBarcodeDetected} />,
    );

    wrapper.find(RNCamera).prop('onBarCodeRead')(DUMMY_BARCODE);
    wrapper.find(RNCamera).prop('onBarCodeRead')(DUMMY_BARCODE2);

    assert.calledOnceWithExactly(onBarcodeDetected, 'AAAA');
  });

  it('simulates twice barcode data onBarcodeDetected is called with delay', () => {
    const onBarcodeDetected = sinon.spy();
    const wrapper = shallow(
      <ScannerCamera cameraBarcodeReadTimeout={100} onBarcodeDetected={onBarcodeDetected} />,
    );

    wrapper.find(RNCamera).prop('onBarCodeRead')(DUMMY_BARCODE);
    jest.runAllTimers();
    wrapper.find(RNCamera).prop('onBarCodeRead')(DUMMY_BARCODE2);

    assert.calledTwiceWithExactly(onBarcodeDetected, ['AAAA'], ['BBBB']);
  });
});

describe('ScannerHID', () => {
  it('simulates barcode data onBarcodeDetected is called', async function () {
    const DUMMY_BARCODE = 'AAAA';
    const DUMMY_BARCODE2 = 'BBB';

    const refString = { current: '' };

    const refInput = {
      current: {
        clear: sinon.spy(),
      },
    };

    const useRef = mockUseRef();
    useRef.onCall(0).callsFake(() => refInput);
    useRef.onCall(1).callsFake(() => refString);

    const onBarcodeDetected = sinon.spy();

    const wrapper = shallow(<ScannerHid onBarcodeDetected={onBarcodeDetected} />);

    wrapper.find({ testID: 'scanner-hid-textinput' }).prop('onChangeText')(DUMMY_BARCODE);

    wrapper.find({ testID: 'scanner-hid-textinput' }).prop('onSubmitEditing')();
    assert.calledOnce(onBarcodeDetected);

    wrapper.find({ testID: 'scanner-hid-textinput' }).prop('onChangeText')(DUMMY_BARCODE2);

    wrapper.find({ testID: 'scanner-hid-textinput' }).prop('onSubmitEditing')();
    assert.calledTwice(onBarcodeDetected);

    useRef.restore();
  });
});
