/*
 * Copyright (C) 2018 Robert Bosch GmbH Copyright (C) 2018 Robert Bosch Manufacturing Solutions GmbH, Germany. All rights reserved.
 */

import { Location } from '@angular/common';
import { Component, DebugElement } from '@angular/core';
import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing';
import { MatIconModule } from '@angular/material/icon';
import { MatSidenavModule } from '@angular/material/sidenav';
import { By } from '@angular/platform-browser';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { ActivatedRoute, Params, Router } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { BehaviorSubject } from 'rxjs';
import { CommandbarSidebarRoutingNavigatorDirective } from './commandbar-sidebar-routing-navigator.directive';
import { CommandbarSidebarSlideComponent } from './commandbar-sidebar-slide.component';
import { CommandbarSidebarComponent } from './commandbar-sidebar.component';

@Component({
  template: '<router-outlet></router-outlet>',
})
class TestAppComponent {}

@Component({
  template: '<bci-commandbar-sidebar bciCommandbarSidebarRoutingNavigator></bci-commandbar-sidebar>',
})
class TestComponent {}

describe('CommandbarSidebarRoutingNavigatorDirective', () => {
  let fixture: ComponentFixture<TestComponent>;
  let sidebarComponent: CommandbarSidebarComponent;
  let sidebarNavigator: CommandbarSidebarRoutingNavigatorDirective;
  let debugElement: DebugElement;

  let activatedRoute: Partial<ActivatedRoute>;
  let router: Partial<Router>;
  let location: Partial<Location> = null;

  let routerNavigateSpy: jest.SpyInstance;
  let navigatorGoToSpy: jest.SpyInstance;
  let locationBackSpy: jest.SpyInstance;

  const initAfterInitialNavigation = function () {
    debugElement = fixture.debugElement.query(By.directive(CommandbarSidebarRoutingNavigatorDirective));
    sidebarComponent = debugElement.injector.get(CommandbarSidebarComponent);
    sidebarNavigator = debugElement.injector.get(CommandbarSidebarRoutingNavigatorDirective);

    activatedRoute = debugElement.injector.get(ActivatedRoute);
    routerNavigateSpy = jest.spyOn(router, 'navigate');
    navigatorGoToSpy = jest.spyOn(sidebarNavigator, 'goTo');
    locationBackSpy = jest.spyOn(location, 'back');

    fixture.detectChanges();
    fixture.autoDetectChanges();
  };

  const expectOnSlide = function (slideKey: string = undefined) {
    if (!!slideKey) {
      expect(location.path()).toEqual('/test;filterSidebar=' + slideKey);
      // 'Sidebar expected to be open (on ' + slideKey + ').'
      expect(sidebarComponent.opened).toBeTruthy();
    } else {
      expect(location.path()).toEqual('/test');
      // 'Sidebar expected to be closed.'
      expect(sidebarComponent.opened).toBeFalsy();
    }
  };

  beforeEach(
    waitForAsync(() => {
      activatedRoute = {
        params: new BehaviorSubject<Params>({ id: '1' }),
      };

      TestBed.configureTestingModule({
        imports: [
          MatIconModule,
          MatSidenavModule,
          RouterTestingModule.withRoutes([{ path: 'test', component: TestComponent }]),
          NoopAnimationsModule,
        ],
        declarations: [
          TestAppComponent,
          TestComponent,
          CommandbarSidebarComponent,
          CommandbarSidebarRoutingNavigatorDirective,
          CommandbarSidebarSlideComponent,
        ],
        providers: [],
      }).compileComponents();

      activatedRoute = TestBed.inject(ActivatedRoute);

      fixture = TestBed.createComponent(TestAppComponent);
      fixture.detectChanges();

      router = fixture.debugElement.injector.get(Router);
      location = fixture.debugElement.injector.get(Location);
    })
  );

  describe('without initially opened sidebar', () => {
    beforeEach(
      waitForAsync(() => {
        router.navigate(['/test']).then(() => {
          initAfterInitialNavigation();
        });
      })
    );

    it('should create an instance', () => {
      expect(sidebarComponent).toBeTruthy();
      expect(sidebarNavigator).toBeTruthy();
    });

    it('should be closed initially', fakeAsync(() => {
      tick(5000);

      expect(sidebarComponent.opened).toBeFalsy();
      expect(location.path()).toEqual('/test');
      expectOnSlide();
    }));

    it('should be opened with main slide by navigator', fakeAsync(() => {
      jest.spyOn(sidebarNavigator, 'goToMain');
      expect(sidebarNavigator.goToMain).not.toHaveBeenCalled();
      expect(sidebarComponent.opened).toBeFalsy();

      sidebarNavigator.open();
      tick(5000);

      expect(sidebarNavigator.goToMain).toHaveBeenCalled();
      expect(routerNavigateSpy).toHaveBeenCalledWith([{ filterSidebar: 'main' }], {
        relativeTo: activatedRoute,
        replaceUrl: false,
      });
      expect(sidebarComponent.activeSlide).toEqual(undefined);
      expect(sidebarComponent.opened).toBeTruthy();
      expectOnSlide('main');
    }));

    it('should be opened with sub-slide by navigator', fakeAsync(() => {
      expect(sidebarComponent.opened).toBeFalsy();

      sidebarNavigator.goTo('sub');
      tick(5000);

      expect(routerNavigateSpy).toHaveBeenCalledWith([{ filterSidebar: 'sub' }], {
        relativeTo: activatedRoute,
        replaceUrl: false,
      });
      expect(sidebarComponent.activeSlide).toEqual('sub');
      expect(sidebarComponent.opened).toBeTruthy();
      expectOnSlide('sub');
    }));

    it('should stay on main slide without new history entry', fakeAsync(() => {
      sidebarNavigator.goToMain();
      tick(5000);
      sidebarNavigator.goToMain();
      tick(5000);

      expect(navigatorGoToSpy.mock.calls).toEqual([['main'], ['main']]);
      expect(routerNavigateSpy.mock.calls).toEqual([
        [[{ filterSidebar: 'main' }], { relativeTo: activatedRoute, replaceUrl: false }],
      ]);
      expectOnSlide('main');
    }));

    it('should replace sub slide without new history entry', fakeAsync(() => {
      sidebarNavigator.goToMain();
      tick(5000);
      sidebarNavigator.goTo('sub');
      tick(5000);
      sidebarNavigator.goTo('sub2');
      tick(5000);

      expect(navigatorGoToSpy.mock.calls).toEqual([['main'], ['sub'], ['sub2']]);
      expect(routerNavigateSpy.mock.calls).toEqual([
        [[{ filterSidebar: 'main' }], { relativeTo: activatedRoute, replaceUrl: false }],
        [[{ filterSidebar: 'sub' }], { relativeTo: activatedRoute, replaceUrl: false }],
        [[{ filterSidebar: 'sub2' }], { relativeTo: activatedRoute, replaceUrl: true }],
      ]);
      expectOnSlide('sub2');
    }));

    it('should go back to main slide', fakeAsync(() => {
      sidebarNavigator.goToMain();
      tick(5000);
      sidebarNavigator.goTo('sub');
      tick(5000);
      sidebarNavigator.goToMain();
      tick(5000);

      expect(navigatorGoToSpy.mock.calls).toEqual([['main'], ['sub'], ['main']]);
      expect(routerNavigateSpy.mock.calls).toEqual([
        [[{ filterSidebar: 'main' }], { relativeTo: activatedRoute, replaceUrl: false }],
        [[{ filterSidebar: 'sub' }], { relativeTo: activatedRoute, replaceUrl: false }],
      ]);
      expect(locationBackSpy).toHaveBeenCalledTimes(1);
      expectOnSlide('main');
    }));

    it('should go back to close sidebar from sub-slide', fakeAsync(() => {
      sidebarNavigator.goToMain();
      tick(5000);
      sidebarNavigator.goTo('sub');
      tick(5000);
      sidebarNavigator.close();
      tick(5000);

      expect(navigatorGoToSpy.mock.calls).toEqual([['main'], ['sub']]);
      expect(routerNavigateSpy.mock.calls).toEqual([
        [[{ filterSidebar: 'main' }], { relativeTo: activatedRoute, replaceUrl: false }],
        [[{ filterSidebar: 'sub' }], { relativeTo: activatedRoute, replaceUrl: false }],
      ]);
      expect(locationBackSpy).toHaveBeenCalledTimes(2);
      expect(location.path()).toEqual('/test');
      // not possible because no router events in tests for location.back due to https://github.com/angular/angular/issues/20269
      // expectOnSlide();
    }));

    it('should go back to close sidebar', fakeAsync(() => {
      const observedParams = [];
      activatedRoute.params.subscribe((params) => observedParams.push(params));

      sidebarNavigator.goToMain();
      tick(5000);
      sidebarNavigator.close();
      tick(5000);

      expect(navigatorGoToSpy.mock.calls).toEqual([['main']]);
      expect(routerNavigateSpy.mock.calls).toEqual([
        [[{ filterSidebar: 'main' }], { relativeTo: activatedRoute, replaceUrl: false }],
      ]);
      expect(locationBackSpy).toHaveBeenCalledTimes(1);
      expect(location.path()).toEqual('/test');
      // not possible because no router events in tests for location.back due to https://github.com/angular/angular/issues/20269
      // expect(observedParams).toEqual([{}, {filterSidebar: 'main'}, {}]);
      // expectOnSlide();
    }));
  });

  describe('with initially opened sidebar on main slide', () => {
    beforeEach(
      waitForAsync(() => {
        router.navigate(['/test', { filterSidebar: 'main' }]).then(() => {
          initAfterInitialNavigation();
        });
      })
    );

    it('should create an instance', () => {
      expect(sidebarComponent).toBeTruthy();
      expect(sidebarNavigator).toBeTruthy();
    });

    it('should be opened with main slide initially', fakeAsync(() => {
      expect(sidebarComponent.opened).toBeTruthy();
      expectOnSlide('main');
    }));

    it('should open sub-slide by navigator', fakeAsync(() => {
      sidebarNavigator.goTo('sub');
      tick(5000);

      expect(routerNavigateSpy).toHaveBeenCalledWith([{ filterSidebar: 'sub' }], {
        relativeTo: activatedRoute,
        replaceUrl: false,
      });
      expect(sidebarComponent.activeSlide).toEqual('sub');
      expect(sidebarComponent.opened).toBeTruthy();
      expectOnSlide('sub');
    }));

    it('should stay on main slide without new history entry', fakeAsync(() => {
      sidebarNavigator.goToMain();
      tick(5000);
      sidebarNavigator.goToMain();
      tick(5000);

      expect(navigatorGoToSpy.mock.calls).toEqual([['main'], ['main']]);
      expect(routerNavigateSpy.mock.calls).toEqual([]);
      expectOnSlide('main');
    }));

    it('should replace sub slide without new history entry', fakeAsync(() => {
      sidebarNavigator.goTo('sub');
      tick(5000);
      sidebarNavigator.goTo('sub2');
      tick(5000);

      expect(navigatorGoToSpy.mock.calls).toEqual([['sub'], ['sub2']]);
      expect(routerNavigateSpy.mock.calls).toEqual([
        [[{ filterSidebar: 'sub' }], { relativeTo: activatedRoute, replaceUrl: false }],
        [[{ filterSidebar: 'sub2' }], { relativeTo: activatedRoute, replaceUrl: true }],
      ]);
      expectOnSlide('sub2');
    }));

    it('should go back to main slide', fakeAsync(() => {
      sidebarNavigator.goTo('sub');
      tick(5000);
      sidebarNavigator.goToMain();
      tick(5000);

      expect(navigatorGoToSpy.mock.calls).toEqual([['sub'], ['main']]);
      expect(routerNavigateSpy.mock.calls).toEqual([
        [[{ filterSidebar: 'sub' }], { relativeTo: activatedRoute, replaceUrl: false }],
      ]);
      expect(locationBackSpy).toHaveBeenCalledTimes(1);
      expectOnSlide('main');
    }));

    it('should go back once and replace main-route to close sidebar from sub-slide', fakeAsync(() => {
      sidebarNavigator.goTo('sub');
      tick(5000);
      sidebarNavigator.close();
      tick(5000);

      expect(navigatorGoToSpy.mock.calls).toEqual([['sub']]);
      expect(routerNavigateSpy.mock.calls).toEqual([
        [[{ filterSidebar: 'sub' }], { relativeTo: activatedRoute, replaceUrl: false }],
        [[{}], { relativeTo: activatedRoute, replaceUrl: true }],
      ]);
      expect(locationBackSpy).toHaveBeenCalledTimes(1);
      expect(location.path()).toEqual('/test');
      // not possible because no router events in tests for location.back due to https://github.com/angular/angular/issues/20269
      // expectOnSlide();
    }));

    it('should not go back but replace route to close sidebar', fakeAsync(() => {
      const observedParams = [];
      activatedRoute.params.subscribe((params) => observedParams.push(params));

      sidebarNavigator.close();
      tick(5000);

      expect(navigatorGoToSpy.mock.calls).toEqual([]);
      expect(routerNavigateSpy.mock.calls).toEqual([[[{}], { relativeTo: activatedRoute, replaceUrl: true }]]);
      expect(locationBackSpy).toHaveBeenCalledTimes(0);
      expect(location.path()).toEqual('/test');
      // not possible because no router events in tests for location.back due to https://github.com/angular/angular/issues/20269
      // expect(observedParams).toEqual([{}, {filterSidebar: 'main'}, {}]);
      // expectOnSlide();
    }));
  });

  describe('with initially opened sidebar on sub slide', () => {
    beforeEach(
      waitForAsync(() => {
        router.navigate(['/test', { filterSidebar: 'sub' }]).then(() => {
          initAfterInitialNavigation();
        });
      })
    );

    it('should create an instance', () => {
      expect(sidebarComponent).toBeTruthy();
      expect(sidebarNavigator).toBeTruthy();
    });

    it('should be opened with main slide initially', fakeAsync(() => {
      expect(sidebarComponent.opened).toBeTruthy();
      expectOnSlide('sub');
    }));

    it('should go to main slide without new history entry', fakeAsync(() => {
      sidebarNavigator.goToMain();
      tick(5000);
      sidebarNavigator.goToMain();
      tick(5000);

      expect(navigatorGoToSpy.mock.calls).toEqual([['main'], ['main']]);
      expect(routerNavigateSpy.mock.calls).toEqual([
        [[{ filterSidebar: 'main' }], { relativeTo: activatedRoute, replaceUrl: true }],
      ]);
      expectOnSlide('main');
    }));

    it('should replace sub slide without new history entry', fakeAsync(() => {
      sidebarNavigator.goTo('sub2');
      tick(5000);

      expect(navigatorGoToSpy.mock.calls).toEqual([['sub2']]);
      expect(routerNavigateSpy.mock.calls).toEqual([
        [[{ filterSidebar: 'sub2' }], { relativeTo: activatedRoute, replaceUrl: true }],
      ]);
      expectOnSlide('sub2');
    }));

    it('should go replace main-route to close sidebar from sub-slide', fakeAsync(() => {
      sidebarNavigator.close();
      tick(5000);

      expect(navigatorGoToSpy.mock.calls).toEqual([]);
      expect(routerNavigateSpy.mock.calls).toEqual([[[{}], { relativeTo: activatedRoute, replaceUrl: true }]]);
      expect(locationBackSpy).toHaveBeenCalledTimes(0);
      expect(location.path()).toEqual('/test');
      // not possible because no router events in tests for location.back due to https://github.com/angular/angular/issues/20269
      // expectOnSlide();
    }));

    it('should not go back but replace route to close sidebar', fakeAsync(() => {
      const observedParams = [];
      activatedRoute.params.subscribe((params) => observedParams.push(params));

      sidebarNavigator.close();
      tick(5000);

      expect(navigatorGoToSpy.mock.calls).toEqual([]);
      expect(routerNavigateSpy.mock.calls).toEqual([[[{}], { relativeTo: activatedRoute, replaceUrl: true }]]);
      expect(locationBackSpy).toHaveBeenCalledTimes(0);
      expect(location.path()).toEqual('/test');
      // not possible because no router events in tests for location.back due to https://github.com/angular/angular/issues/20269
      // expect(observedParams).toEqual([{}, {filterSidebar: 'main'}, {}]);
      // expectOnSlide();
    }));
  });
});
