// unit test ShellComponent with Spectator

import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { ApplicationProcess, ApplicationService } from '@core/application';
import { AuthService } from '@core/auth';
import { Config } from '@core/config';
import { BreadcrumbService } from '@core/breadcrumb';
import { DomainAvailabilityService } from '@domain/availability';
import { DomainDashboardService } from '@domain/isa';
import { NotificationsHub } from '@hub/notifications';
import { ModalNotificationsComponent } from '@modal/notifications';
import { Spectator, createComponentFactory, SpyObject, createSpyObject } from '@ngneat/spectator';
import { DashboardComponent } from '@page/dashboard';
import { ShellFooterComponent } from '@shell/footer';
import { ShellHeaderComponent } from '@shell/header';
import { ShellProcessComponent, ShellProcessTabComponent } from '@shell/process';
import { UiIconComponent } from '@ui/icon';
import { UiModalService } from '@ui/modal';
import { EnvelopeDTO, MessageBoardItemDTO } from 'apps/hub/notifications/src/lib/defs';
import { MockComponent } from 'ng-mocks';
import { of } from 'rxjs';
import { first } from 'rxjs/operators';
import { ShellComponent } from './shell.component';

// DummyComponent Class
@Component({
  selector: 'dummy-component',
  template: '<div></div>',
})
class DummyComponent {
  constructor() {}
}

describe('ShellComponent', () => {
  let spectator: Spectator<ShellComponent>;
  let applicationServiceMock: SpyObject<ApplicationService>;
  let modalServiceMock: SpyObject<UiModalService>;
  let notificationsHubMock: SpyObject<NotificationsHub>;
  let router: Router;
  let breadcrumbServiceMock: SpyObject<BreadcrumbService>;
  let authServiceMock: SpyObject<AuthService>;

  const createComponent = createComponentFactory({
    component: ShellComponent,
    imports: [
      RouterTestingModule.withRoutes([
        { path: 'kunde', component: DummyComponent },
        { path: 'kunde/dashboard', component: DashboardComponent },
      ]),
    ],
    declarations: [
      MockComponent(ShellHeaderComponent),
      MockComponent(ShellFooterComponent),
      MockComponent(ShellProcessComponent),
      MockComponent(ShellProcessTabComponent),
      MockComponent(UiIconComponent),
    ],
    mocks: [BreadcrumbService, DomainAvailabilityService, AuthService, DomainDashboardService, Config],
  });

  beforeEach(() => {
    applicationServiceMock = createSpyObject(ApplicationService);
    applicationServiceMock.getSection$.and.returnValue(of('customer'));
    applicationServiceMock.getProcesses$.and.returnValue(of([]));
    applicationServiceMock.getProcessById$.and.returnValue(of({ id: 4000 }));
    applicationServiceMock.getActivatedProcessId$.and.returnValue(of(undefined));
    applicationServiceMock.getLastActivatedProcessWithSectionAndType$.and.returnValue(of({}));
    applicationServiceMock.getLastActivatedProcessWithSection$.and.returnValue(of({}));

    notificationsHubMock = createSpyObject(NotificationsHub);
    notificationsHubMock.notifications$ = of({});

    modalServiceMock = createSpyObject(UiModalService);

    authServiceMock = createSpyObject(AuthService);

    spectator = createComponent({
      providers: [
        { provide: ApplicationService, useValue: applicationServiceMock },
        { provide: NotificationsHub, useValue: notificationsHubMock },
        { provide: UiModalService, useValue: modalServiceMock },
        { provide: AuthService, useValue: authServiceMock },
      ],
    });

    breadcrumbServiceMock = spectator.inject(BreadcrumbService);
    router = spectator.inject(Router);
  });

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

  describe('shell-header', () => {
    it('should call setSection() on sectionChange event with the section argument', () => {
      spyOn(spectator.component, 'setSection');
      spectator.triggerEventHandler('shell-header', 'sectionChange', 'branch');
      expect(spectator.component.setSection).toHaveBeenCalledWith('branch');
    });

    it('should render the header buttons', () => {
      // Test verhält sich anders, wenn die größe des Browserfensters kleiner ist als 640px, da
      // die Buttons dann unsichtbar werden und ins Drei-Punkt Menü verschoben werden.
      if (document.body.clientWidth > 639) {
        expect(spectator.query('shell-header .notifications-btn')).toBeVisible();
        expect(spectator.query('shell-header .dashboard-btn')).toBeVisible();
        expect(spectator.query('shell-header .logout-btn')).toBeVisible();
      } else {
        expect(spectator.query('shell-header .notifications-btn')).not.toBeVisible();
        expect(spectator.query('shell-header .dashboard-btn')).not.toBeVisible();
        expect(spectator.query('shell-header .logout-btn')).not.toBeVisible();
      }
    });

    it('should have a anchor tag which navigates to /kunde/dashboard', () => {
      const anchor = spectator.query('shell-header a');
      expect(anchor).toHaveAttribute('href', '/kunde/dashboard');
    });
  });

  describe('shell-process', () => {
    it('should call addProcess() on addProcess event', () => {
      spyOn(spectator.component, 'addProcess');
      spectator.triggerEventHandler('shell-process', 'addProcess', undefined);
      expect(spectator.component.addProcess).toHaveBeenCalled();
    });

    describe('shell-process-tab', () => {
      it('should render for each process', () => {
        const processes = [{}, {}, {}];
        applicationServiceMock.getSection$.and.returnValue(of('customer'));
        applicationServiceMock.getProcesses$.and.returnValue(of(processes));
        spectator.detectComponentChanges();
        expect(spectator.queryAll('shell-process-tab')).toHaveLength(processes.length);
      });

      it('should call activateProcess() on activateProcess event', () => {
        const processes = [{ id: 1 }];

        applicationServiceMock.getProcesses$.and.returnValue(of(processes));
        spectator.detectComponentChanges();

        spyOn(spectator.component, 'activateProcess');
        spectator.triggerEventHandler('shell-process-tab', 'activateProcess', processes[0].id);
        expect(spectator.component.activateProcess).toHaveBeenCalledWith(1);
      });

      it('should call closeProcess() on closeProcess event', () => {
        const processes = [{ id: 1 }];

        applicationServiceMock.getProcesses$.and.returnValue(of(processes));
        spectator.detectComponentChanges();

        spyOn(spectator.component, 'closeProcess');
        spectator.triggerEventHandler('shell-process-tab', 'closeProcess', processes[0].id);
        expect(spectator.component.closeProcess).toHaveBeenCalledWith(1);
      });
    });
  });

  describe('shell-footer', () => {
    it('should render when section is set', () => {
      applicationServiceMock.getSection$.and.returnValue(of('customer'));
      spectator.detectComponentChanges();
      expect(spectator.query('shell-footer')).toBeVisible();
    });

    it('should not render when section is undefined', () => {
      applicationServiceMock.getSection$.and.returnValue(of(undefined));
      spectator.detectComponentChanges();
      expect(spectator.query('shell-footer')).not.toBeVisible();
    });

    it('should display the menu items for section customer', () => {
      applicationServiceMock.getSection$.and.returnValue(of('customer'));
      spectator.detectComponentChanges();

      const anchors = spectator.queryAll('shell-footer a');
      expect(anchors[0]).toHaveText('Artikelsuche');
      expect(anchors[0]).toHaveAttribute('href', '/kunde/product');
      expect(anchors[1]).toHaveText('Kundensuche');
      expect(anchors[1]).toHaveAttribute('href', '/kunde/customer');
      expect(anchors[2]).toHaveText('Warenausgabe');
      expect(anchors[2]).toHaveAttribute('href', '/kunde/goods/out');
    });

    it('should display the menu items for section branch', () => {
      applicationServiceMock.getSection$.and.returnValue(of('branch'));
      spectator.detectComponentChanges();

      const anchors = spectator.queryAll('shell-footer a');
      expect(anchors[0]).toHaveText('Tätigkeitskalender');
      expect(anchors[0]).toHaveAttribute('href', '/filiale/task-calendar');
      expect(anchors[1]).toHaveText('Abholfach');
      expect(anchors[1]).toHaveAttribute('href', '/filiale/goods/in');
      expect(anchors[2]).toHaveText('Remission');
      expect(anchors[2]).toHaveAttribute('href', '/filiale/remission');
    });
  });

  describe('activatedProcessId$', () => {
    it('should call _appService.getActivatedProcessId$() and return its value', async () => {
      applicationServiceMock.getActivatedProcessId$.and.returnValue(of(1));
      const processId = await spectator.component.activatedProcessId$.pipe(first()).toPromise();
      expect(processId).toBe(1);
      expect(applicationServiceMock.getActivatedProcessId$).toHaveBeenCalled();
    });
  });

  describe('section$', () => {
    it('should call _appService.getSection$() and return its value', async () => {
      applicationServiceMock.getSection$.and.returnValue(of('branch'));
      const section = await spectator.component.section$.pipe(first()).toPromise();
      expect(section).toBe('branch');
      expect(applicationServiceMock.getSection$).toHaveBeenCalled();
    });
  });

  describe('processes$', () => {
    it('should call _appService.processes$() and return its value', async () => {
      applicationServiceMock.getProcesses$.and.returnValue(of([{}, {}]));
      const processes = await spectator.component.processes$.pipe(first()).toPromise();
      expect(processes).toHaveLength(2);
      expect(applicationServiceMock.getProcesses$).toHaveBeenCalledWith('customer');
    });
  });

  describe('remissionProcess$', () => {
    it('should call _appService.getProcessById$() with Remission Id and return its value', async () => {
      applicationServiceMock.getProcessById$.and.returnValue(of({ id: 4000 }));
      await spectator.component.remissionProcess$.pipe(first()).toPromise();
      expect(applicationServiceMock.getProcessById$).toHaveBeenCalled();
    });
  });

  describe('remissionUrl$', () => {
    it('should return the correct url if process.data.active is available', async () => {
      const process = {
        id: 4000,
        data: {
          active: 9999,
        },
      };
      applicationServiceMock.getProcessById$.and.returnValue(of(process));
      const url = await spectator.component.remissionUrl$.pipe(first()).toPromise();
      expect(url).toBe('/filiale/remission/9999/list');
    });

    it('should return the correct url if process.data.active is not available', async () => {
      const process = {
        id: 4000,
        data: {},
      };
      applicationServiceMock.getProcessById$.and.returnValue(of(process));
      const url = await spectator.component.remissionUrl$.pipe(first()).toPromise();
      expect(url).toBe('/filiale/remission');
    });
  });

  describe('remissionQueryParams$', () => {
    it('should return the correct queryParams if process.data.active and process.data.queryParams are available', async () => {
      const process = {
        id: 4000,
        data: {
          active: 9999,
          queryParams: { filter: 'test' },
        },
      };
      applicationServiceMock.getProcessById$.and.returnValue(of(process));
      const queryParams = await spectator.component.remissionQueryParams$.pipe(first()).toPromise();
      expect(queryParams).toEqual(process.data.queryParams);
    });

    it('should return the correct queryParams if process.data.active and process.data.queryParams are not available', async () => {
      const process = {
        id: 4000,
        data: {},
      };
      applicationServiceMock.getProcessById$.and.returnValue(of(process));
      const queryParams = await spectator.component.remissionQueryParams$.pipe(first()).toPromise();
      expect(queryParams).toEqual({});
    });
  });

  describe('setSection()', () => {
    it('should call _appService.setSection() with the argument section', async () => {
      await spectator.component.setSection('customer');
      expect(applicationServiceMock.setSection).toHaveBeenCalledWith('customer');
    });

    it('should call activateProcess if getLastActivatedProcessWithSection returns a value', async () => {
      applicationServiceMock.getLastActivatedProcessWithSection$.and.returnValue(of({ id: 1 }));
      spyOn(spectator.component, 'activateProcess');
      await spectator.component.setSection('customer');
      expect(spectator.component.activateProcess).toHaveBeenCalledWith(1);
    });
  });

  describe('logout()', () => {
    it('should call _authService.logout()', () => {
      spectator.component.logout();
      expect(authServiceMock.logout).toHaveBeenCalled();
    });
  });

  describe('addProcess()', () => {
    it('should call navigate to /kunde/{timestamp}/product', () => {
      spyOn(router, 'navigate');
      spyOn(Date, 'now').and.returnValue(123);
      spectator.component.addProcess();
      expect(router.navigate).toHaveBeenCalledWith(['/kunde', 123, 'product']);
    });
  });

  describe('closeProcess()', () => {
    it('should call _appService.removeProcess() with the processId argument', () => {
      const processes = [{}, {}, {}];
      applicationServiceMock.getSection$.and.returnValue(of('customer'));
      applicationServiceMock.getProcesses$.and.returnValue(of(processes));
      spectator.component.closeProcess(1);
      expect(applicationServiceMock.removeProcess).toHaveBeenCalledWith(1);
    });

    it('should navigate to kunde/dashboard if no process is available', async () => {
      spyOn(router, 'navigate');
      applicationServiceMock.getSection$.and.returnValue(of('customer'));
      applicationServiceMock.getProcesses$.and.returnValue(of([]));
      spectator.detectComponentChanges();
      await spectator.component.closeProcess(1);
      expect(router.navigate).toHaveBeenCalledWith(['/kunde', 'dashboard']);
    });

    it('should not navigate to kunde/dashboard if processes are available', async () => {
      spyOn(router, 'navigate');
      const processes = [
        { id: 1, name: 'test', section: 'customer' },
        { id: 2, name: 'test', section: 'customer' },
      ];

      applicationServiceMock.getLastActivatedProcessWithSection$.and.returnValue(of({}));
      applicationServiceMock.getSection$.and.returnValue(of('customer'));
      applicationServiceMock.getProcesses$.and.returnValue(of(processes));
      await spectator.component.closeProcess(1);
      expect(router.navigate).not.toHaveBeenCalledWith(['/kunde', 'dashboard']);
    });

    it('should activate the next process when it was not the last process', async () => {
      spyOn(spectator.component, 'activateProcess');

      applicationServiceMock.getLastActivatedProcessWithSection$.and.returnValue(
        of({
          id: 2,
          name: 'test',
          section: 'customer',
          activated: 2,
        })
      );

      const processes = [
        { id: 1, name: 'test', section: 'customer', activated: 1 },
        { id: 2, name: 'test', section: 'customer', activated: 2 },
      ];
      applicationServiceMock.getSection$.and.returnValue(of('customer'));
      applicationServiceMock.getProcesses$.and.returnValue(of(processes));
      await spectator.component.closeProcess(1);

      expect(spectator.component.activateProcess).toHaveBeenCalledWith(2);
    });
  });

  describe('activateProcess()', () => {
    it('should get the last activated breadcrumb by key and if it is defined it navigates to its path with queryParams', async () => {
      const crumb = { path: '/kunde/product', params: { id: 1 } };
      spyOn(router, 'navigate');
      breadcrumbServiceMock.getLastActivatedBreadcrumbByKey$.and.returnValue(of(crumb));

      await spectator.component.activateProcess(1);
      expect(router.navigate).toHaveBeenCalledWith([crumb.path], { queryParams: crumb.params });
    });

    it('should navigate to /kunde if no breadcrumb for this process exists', async () => {
      breadcrumbServiceMock.getLastActivatedBreadcrumbByKey$.and.returnValue(of(undefined));
      spyOn(router, 'navigate');

      await spectator.component.activateProcess(1);
      expect(router.navigate).toHaveBeenCalledWith(['/kunde']);
    });
  });

  describe('processAction()', () => {
    it('should navigate to cart when process type is cart', () => {
      spyOn(router, 'navigate');

      const process: ApplicationProcess = { id: 1, name: 'Vorgang', section: 'customer', type: 'cart' };
      spectator.component.processAction(process);
      expect(router.navigate).toHaveBeenCalledWith(['/kunde', process.id, 'cart']);
    });

    it('should not navigate to when process type is not cart', () => {
      spyOn(router, 'navigate');

      const process: ApplicationProcess = { id: 1, name: 'Vorgang', section: 'customer', type: 'goods-out' };
      spectator.component.processAction(process);
      expect(router.navigate).not.toHaveBeenCalled();
    });
  });

  describe('openNotifications()', () => {
    it('should call modalService.open() with the ModalNotificationComponent', async () => {
      const notifications: EnvelopeDTO<MessageBoardItemDTO[]> = {
        data: [{}, {}, {}],
      };

      spectator.component.notifications$ = of(notifications);

      await spectator.component.openNotifications();
      expect(modalServiceMock.open).toHaveBeenCalledWith({
        content: ModalNotificationsComponent,
        data: notifications,
        config: {
          showScrollbarY: false,
        },
      });
    });
  });
});
