import { expect } from 'chai';
import sinon from 'sinon';
import { beforeEach } from 'vitest';
import Store from '../../../src/scripts/store/store';
import { ActionType, State } from '../../../src';
import { cloneObject } from '../../../src/scripts/lib/utils';
import { AnyAction, StoreListener } from '../../../src/scripts/interfaces/store';

describe('reducers/store', () => {
  let instance: Store;
  let subscribeStub: sinon.SinonStub<[listener: StoreListener], void>;
  let dispatchStub: sinon.SinonStub<[action: AnyAction], void>;
  let getStateStub: sinon.SinonStub<any[], State>;
  let emptyState: State;
  let state: State;

  beforeEach(() => {
    instance = new Store();
    subscribeStub = sinon.stub(instance, 'subscribe');
    dispatchStub = sinon.stub(instance, 'dispatch');
    getStateStub = sinon.stub(instance, 'state');
    emptyState = instance.defaultState;
    state = {
      items: [
        {
          id: 1,
          groupId: 0,
          value: 'Item one',
          label: 'Item one',
          active: false,
          highlighted: false,
          customProperties: {},
          placeholder: false,
          disabled: false,
          selected: false,
          score: 0,
          rank: 0,
        },
        {
          id: 2,
          groupId: 0,
          value: 'Item two',
          label: 'Item two',
          active: true,
          highlighted: false,
          customProperties: {},
          placeholder: false,
          disabled: false,
          selected: false,
          score: 0,
          rank: 0,
        },
        {
          id: 3,
          groupId: 0,
          value: 'Item three',
          label: 'Item three',
          active: true,
          highlighted: true,
          customProperties: {},
          placeholder: false,
          disabled: false,
          selected: false,
          score: 0,
          rank: 0,
        },
      ],
      choices: [
        {
          id: 1,
          elementId: 'choices-test-1',
          groupId: 0,
          value: 'Choice 1',
          label: 'Choice 1',
          disabled: false,
          selected: false,
          active: true,
          score: 9999,
          rank: 9999,
          customProperties: {},
          placeholder: false,
          highlighted: false,
        },
        {
          id: 2,
          elementId: 'choices-test-2',
          groupId: 0,
          value: 'Choice 2',
          label: 'Choice 2',
          disabled: false,
          selected: true,
          active: false,
          score: 9999,
          rank: 9998,
          customProperties: {},
          placeholder: false,
          highlighted: false,
        },
      ],
      groups: [
        {
          id: 1,
          label: 'Group one',
          active: true,
          disabled: false,
          choices: [],
        },
        {
          id: 2,
          label: 'Group two',
          active: true,
          disabled: false,
          choices: [],
        },
      ],
    };
  });

  afterEach(() => {
    subscribeStub.restore();
    dispatchStub.restore();
    getStateStub.restore();
  });

  describe('constructor', () => {
    it('creates redux-like store', () => {
      expect(instance).to.contain.keys(['_state', '_listeners', '_txn']);
    });
  });

  describe('subscribe', () => {
    it('wraps redux-like subscribe method', () => {
      const onChange = (): void => {};
      expect(subscribeStub.callCount).to.equal(0);
      instance.subscribe(onChange);
      expect(subscribeStub.callCount).to.equal(1);
      expect(subscribeStub.firstCall.args[0]).to.equal(onChange);
    });
  });

  describe('dispatch', () => {
    it('wraps redux-like dispatch method', () => {
      const action: AnyAction = { type: ActionType.CLEAR_CHOICES };
      expect(dispatchStub.callCount).to.equal(0);
      instance.dispatch(action);
      expect(dispatchStub.callCount).to.equal(1);
      expect(dispatchStub.firstCall.args[0]).to.equal(action);
    });
  });

  describe('state getter', () => {
    it('returns state', () => {
      getStateStub.value(cloneObject(emptyState));

      expect(instance.state).to.deep.equal(emptyState);
    });
  });

  describe('txn', () => {
    let listenerStub: sinon.SinonStub;

    beforeEach(() => {
      subscribeStub.restore();
      dispatchStub.restore();
      getStateStub.restore();

      instance._state = cloneObject(state);
      listenerStub = sinon.stub();
      instance.subscribe(listenerStub);
    });

    it('coalesce listener events', () => {
      const emptyChoicesState = cloneObject(state);
      emptyChoicesState.choices = [];
      emptyChoicesState.groups = [];

      instance.withTxn(() => {
        const action: AnyAction = { type: ActionType.CLEAR_CHOICES };
        instance.dispatch(action);
        instance.dispatch(action);
      });

      expect(listenerStub.callCount).eq(1);
      expect(instance.state).to.deep.equal(emptyChoicesState);
    });

    it('coalesce listener events with reset', () => {
      instance.withTxn(() => {
        const action: AnyAction = { type: ActionType.CLEAR_CHOICES };
        instance.dispatch(action);
        instance.dispatch(action);
        instance.reset();
      });

      expect(listenerStub.callCount).eq(1);
      expect(instance.state).to.deep.equal(emptyState);
    });
  });

  describe('without txn', () => {
    let listenerStub: sinon.SinonStub;

    beforeEach(() => {
      subscribeStub.restore();
      dispatchStub.restore();
      getStateStub.restore();

      instance._state = cloneObject(state);
      listenerStub = sinon.stub();
      instance.subscribe(listenerStub);
    });

    it('multiple listener events', () => {
      const emptyChoicesState = cloneObject(state);
      emptyChoicesState.choices = [];
      emptyChoicesState.groups = [];

      const action: AnyAction = { type: ActionType.CLEAR_CHOICES };
      instance.dispatch(action);
      instance.dispatch(action);

      expect(listenerStub.callCount).eq(2);
      expect(instance.state).to.deep.equal(emptyChoicesState);
    });

    it('multiple listener events with reset', () => {
      const action: AnyAction = { type: ActionType.CLEAR_CHOICES };
      instance.dispatch(action);
      instance.dispatch(action);
      instance.reset();

      expect(listenerStub.callCount).eq(3);
      expect(instance.state).to.deep.equal(emptyState);
    });
  });

  describe('store selectors', () => {
    beforeEach(() => {
      getStateStub.value(cloneObject(state));
    });

    describe('items getter', () => {
      it('returns items', () => {
        const expectedResponse = state.items;
        expect(instance.items).to.deep.equal(expectedResponse);
      });
    });

    describe('highlightedActiveItems getter', () => {
      it('returns items that are active and highlighted', () => {
        const expectedResponse = state.items.filter((item) => item.highlighted && item.active);
        expect(instance.highlightedActiveItems).to.deep.equal(expectedResponse);
      });
    });

    describe('choices getter', () => {
      it('returns choices', () => {
        const expectedResponse = state.choices;
        expect(instance.choices).to.deep.equal(expectedResponse);
      });
    });

    describe('activeChoices getter', () => {
      it('returns choices that are active', () => {
        const expectedResponse = state.choices.filter((choice) => choice.active);
        expect(instance.activeChoices).to.deep.equal(expectedResponse);
      });
    });

    describe('searchableChoices getter', () => {
      it('returns choices that are not placeholders and are selectable', () => {
        const expectedResponse = state.choices.filter((choice) => !choice.disabled && !choice.placeholder);
        expect(instance.searchableChoices).to.deep.equal(expectedResponse);
      });
    });

    describe('getChoiceById', () => {
      describe('passing id', () => {
        it('returns active choice by passed id', () => {
          const id: number = 1;
          const expectedResponse = state.choices.find((choice) => choice.id === id);
          const actualResponse = instance.getChoiceById(id);
          expect(actualResponse).to.deep.equal(expectedResponse);
        });
      });
    });

    describe('groups getter', () => {
      it('returns groups', () => {
        const expectedResponse = state.groups;
        expect(instance.groups).to.deep.equal(expectedResponse);
      });
    });

    describe('activeGroups getter', () => {
      it('returns active groups', () => {
        const expectedResponse = state.groups.filter((group) => group.active);
        expect(instance.activeGroups).to.deep.equal(expectedResponse);
      });
    });

    describe('getGroupById', () => {
      it('returns group by id', () => {
        const id = 1;
        const expectedResponse = state.groups.find((group) => group.id === id);
        const actualResponse = instance.getGroupById(id);
        expect(actualResponse).to.deep.equal(expectedResponse);
      });
    });
  });
});
