import { createServiceFactory, SpectatorService, SpyObject } from '@ngneat/spectator';
import { Store } from '@ngrx/store';
import { Observable, of } from 'rxjs';
import { first } from 'rxjs/operators';
import { ApplicationProcess } from './defs';

import { ApplicationService } from './application.service';
import * as actions from './store/application.actions';

describe('ApplicationService', () => {
  let spectator: SpectatorService<ApplicationService>;
  let store: SpyObject<Store>;
  const createService = createServiceFactory({
    service: ApplicationService,
    mocks: [Store],
  });

  beforeEach(() => {
    spectator = createService({});
    store = spectator.inject(Store);
  });

  it('should be created', () => {
    expect(spectator.service).toBeTruthy();
  });

  describe('activatedProcessId$', () => {
    it('should return an observable', () => {
      expect(spectator.service.activatedProcessId$).toBeInstanceOf(Observable);
    });
  });

  describe('activatedProcessId', () => {
    it('should return the process id as a number', () => {
      spyOnProperty(spectator.service['activatedProcessIdSubject'] as any, 'value').and.returnValue(2);
      expect(spectator.service.activatedProcessId).toBe(2);
    });
  });

  describe('getProcesses$()', () => {
    it('should call select on store and return all selected processes', async () => {
      const processes: ApplicationProcess[] = [
        { id: 1, name: 'Vorgang', type: 'cart', section: 'customer', data: { count: 1 } },
        { id: 2, name: 'Vorgang', type: 'task-calendar', section: 'branch' },
      ];
      store.select.and.returnValue(of(processes));
      const result = await spectator.service.getProcesses$().pipe(first()).toPromise();
      expect(result).toEqual(processes);
      expect(store.select).toHaveBeenCalled();
    });

    it('should call select on store and return all section customer processes', async () => {
      const processes: ApplicationProcess[] = [
        { id: 1, name: 'Vorgang', type: 'cart', section: 'customer', data: { count: 1 } },
        { id: 2, name: 'Vorgang', type: 'task-calendar', section: 'branch' },
      ];
      store.select.and.returnValue(of(processes));
      const result = await spectator.service.getProcesses$('customer').pipe(first()).toPromise();
      expect(result).toEqual([processes[0]]);
      expect(store.select).toHaveBeenCalled();
    });

    it('should call select on store and return all section branch processes', async () => {
      const processes: ApplicationProcess[] = [
        { id: 1, name: 'Vorgang', type: 'cart', section: 'customer', data: { count: 1 } },
        { id: 2, name: 'Vorgang', type: 'task-calendar', section: 'branch' },
      ];
      store.select.and.returnValue(of(processes));
      const result = await spectator.service.getProcesses$('branch').pipe(first()).toPromise();
      expect(result).toEqual([processes[1]]);
      expect(store.select).toHaveBeenCalled();
    });
  });

  describe('getProcessById$()', () => {
    it('should return the process by id', async () => {
      const processes: ApplicationProcess[] = [
        { id: 1, name: 'Vorgang 1', section: 'customer' },
        { id: 2, name: 'Vorgang 2', section: 'customer' },
      ];
      spyOn(spectator.service, 'getProcesses$').and.returnValue(of(processes));

      const process = await spectator.service.getProcessById$(1).toPromise();
      expect(process.id).toBe(1);
    });
  });

  describe('getSection$()', () => {
    it('should return the selected section branch', async () => {
      const section = 'branch';
      store.select.and.returnValue(of(section));
      const result = await spectator.service.getSection$().pipe(first()).toPromise();
      expect(result).toEqual(section);
      expect(store.select).toHaveBeenCalled();
    });
  });

  describe('getActivatedProcessId$', () => {
    it('should return the current selected activated process id', async () => {
      const activatedProcessId = 2;
      store.select.and.returnValue(of({ id: activatedProcessId }));
      const result = await spectator.service.getActivatedProcessId$().pipe(first()).toPromise();
      expect(result).toEqual(activatedProcessId);
      expect(store.select).toHaveBeenCalled();
    });
  });

  describe('activateProcess()', () => {
    it('should dispatch action setActivatedProcess with argument activatedProcessId and action type', () => {
      const activatedProcessId = 2;
      spectator.service.activateProcess(activatedProcessId);
      expect(store.dispatch).toHaveBeenCalledWith({ activatedProcessId, type: actions.setActivatedProcess.type });
    });
  });

  describe('removeProcess()', () => {
    it('should dispatch action removeProcess with argument processId and action type', () => {
      const processId = 2;
      spectator.service.removeProcess(processId);
      expect(store.dispatch).toHaveBeenCalledWith({ processId, type: actions.removeProcess.type });
    });
  });

  describe('createProcess()', () => {
    it('should dispatch action addProcess with process', async () => {
      const process: ApplicationProcess = {
        id: 1,
        name: 'Vorgang 1',
        section: 'customer',
        type: 'cart',
      };

      const timestamp = 100;
      spyOn(spectator.service as any, '_createTimestamp').and.returnValue(timestamp);
      spyOn(spectator.service, 'getProcessById$').and.returnValue(of(undefined));
      await spectator.service.createProcess(process);

      expect(store.dispatch).toHaveBeenCalledWith({
        type: actions.addProcess.type,
        process: {
          ...process,
          activated: 0,
          created: timestamp,
        },
      });
    });

    it('should throw an error if the process id is already existing', async () => {
      const process: ApplicationProcess = {
        id: 1,
        name: 'Vorgang 1',
        section: 'customer',
        type: 'cart',
      };
      spyOn(spectator.service, 'getProcessById$').and.returnValue(of(process));
      await expectAsync(spectator.service.createProcess(process)).toBeRejectedWithError('Process Id existiert bereits');
    });

    it('should throw an error if the process id is not a number', async () => {
      const process: ApplicationProcess = {
        id: undefined,
        name: 'Vorgang 1',
        section: 'customer',
        type: 'cart',
      };
      spyOn(spectator.service, 'getProcessById$').and.returnValue(of({ id: 5, name: 'Vorgang 2', section: 'customer' }));
      await expectAsync(spectator.service.createProcess(process)).toBeRejectedWithError('Process Id nicht gesetzt');
    });
  });

  describe('patchProcess', () => {
    it('should dispatch action patchProcess with changes', async () => {
      const process: ApplicationProcess = {
        id: 1,
        name: 'Vorgang 1',
        section: 'customer',
        type: 'cart',
      };

      await spectator.service.patchProcess(process.id, process);

      expect(store.dispatch).toHaveBeenCalledWith({
        type: actions.patchProcess.type,
        processId: process.id,
        changes: {
          ...process,
        },
      });
    });
  });

  describe('setSection()', () => {
    it('should dispatch action setSection with argument section and action type', () => {
      const section = 'customer';
      spectator.service.setSection(section);
      expect(store.dispatch).toHaveBeenCalledWith({ section, type: actions.setSection.type });
    });
  });

  describe('getLastActivatedProcessWithSectionAndType()', () => {
    it('should return the last activated process by section and type', async () => {
      const processes: ApplicationProcess[] = [
        { id: 1, name: 'Vorgang 1', section: 'customer', type: 'cart', activated: 100 },
        { id: 2, name: 'Vorgang 2', section: 'customer', type: 'cart', activated: 200 },
        { id: 3, name: 'Vorgang 3', section: 'customer', type: 'goodsOut', activated: 300 },
      ];
      spyOn(spectator.service, 'getProcesses$').and.returnValue(of(processes));

      expect(await spectator.service.getLastActivatedProcessWithSectionAndType$('customer', 'cart').pipe(first()).toPromise()).toBe(
        processes[1]
      );
    });
  });

  describe('getLastActivatedProcessWithSection()', () => {
    it('should return the last activated process by section', async () => {
      const processes: ApplicationProcess[] = [
        { id: 1, name: 'Vorgang 1', section: 'customer', activated: 100 },
        { id: 2, name: 'Vorgang 2', section: 'customer', activated: 200 },
        { id: 3, name: 'Vorgang 3', section: 'customer', activated: 300 },
      ];
      spyOn(spectator.service, 'getProcesses$').and.returnValue(of(processes));

      expect(await spectator.service.getLastActivatedProcessWithSection$('customer').pipe(first()).toPromise()).toBe(processes[2]);
    });
  });

  describe('_createTimestamp', () => {
    it('should return the current timestamp in ms', () => {
      expect(spectator.service['_createTimestamp']()).toBeCloseTo(Date.now());
    });
  });
});
