import { NO_ERRORS_SCHEMA } from '@angular/core';
import {
  TestBed,
  waitForAsync,
} from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { RouterTestingModule } from '@angular/router/testing';
import {
  NgbModal,
  NgbModalRef,
} from '@ng-bootstrap/ng-bootstrap';
import { TranslateModule } from '@ngx-translate/core';
import { cold } from 'jasmine-marbles';
import { of as observableOf } from 'rxjs';

import { AdminSidebarComponent } from './admin/admin-sidebar/admin-sidebar.component';
import { BrowseService } from './core/browse/browse.service';
import { ConfigurationDataService } from './core/data/configuration-data.service';
import { AuthorizationDataService } from './core/data/feature-authorization/authorization-data.service';
import { FeatureID } from './core/data/feature-authorization/feature-id';
import { ScriptDataService } from './core/data/processes/script-data.service';
import { MenuService } from './shared/menu/menu.service';
import { MenuID } from './shared/menu/menu-id.model';
import { createSuccessfulRemoteDataObject$ } from './shared/remote-data.utils';
import { ConfigurationDataServiceStub } from './shared/testing/configuration-data.service.stub';
import { MenuServiceStub } from './shared/testing/menu-service.stub';
import { createPaginatedList } from './shared/testing/utils.test';
import createSpy = jasmine.createSpy;
import { AuthService } from './core/auth/auth.service';
import { MenuResolverService } from './menu-resolver.service';
import { AuthServiceStub } from './shared/testing/auth-service.stub';

const BOOLEAN = { t: true, f: false };
const MENU_STATE = {
  id: 'some menu',
};
const BROWSE_DEFINITIONS = [
  { id: 'definition1' },
  { id: 'definition2' },
  { id: 'definition3' },
];

describe('menuResolver', () => {
  let resolver: MenuResolverService;

  let menuService;
  let browseService;
  let authorizationService;
  let scriptService;
  let mockNgbModal;
  let configurationDataService;

  beforeEach(waitForAsync(() => {
    menuService = new MenuServiceStub();
    spyOn(menuService, 'getMenu').and.returnValue(observableOf(MENU_STATE));
    spyOn(menuService, 'addSection');

    browseService = jasmine.createSpyObj('browseService', {
      getBrowseDefinitions: createSuccessfulRemoteDataObject$(createPaginatedList(BROWSE_DEFINITIONS)),
    });
    authorizationService = jasmine.createSpyObj('authorizationService', {
      isAuthorized: observableOf(true),
    });
    scriptService = jasmine.createSpyObj('scriptService', {
      scriptWithNameExistsAndCanExecute: observableOf(true),
    });
    mockNgbModal = {
      open: jasmine.createSpy('open').and.returnValue(
        { componentInstance: {}, closed: observableOf({}) } as NgbModalRef,
      ),
    };

    configurationDataService = new ConfigurationDataServiceStub();
    spyOn(configurationDataService, 'findByPropertyName').and.returnValue(observableOf(true));

    TestBed.configureTestingModule({
      imports: [TranslateModule.forRoot(), NoopAnimationsModule, RouterTestingModule, AdminSidebarComponent],
      providers: [
        { provide: MenuService, useValue: menuService },
        { provide: BrowseService, useValue: browseService },
        { provide: AuthorizationDataService, useValue: authorizationService },
        { provide: ScriptDataService, useValue: scriptService },
        { provide: ConfigurationDataService, useValue: configurationDataService },
        { provide: NgbModal, useValue: mockNgbModal },
        { provide: AuthService, useValue: AuthServiceStub },
        MenuResolverService,
      ],
      schemas: [NO_ERRORS_SCHEMA],
    });
    resolver = TestBed.inject(MenuResolverService);
  }));

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

  describe('resolve', () => {
    it('should create all menus', (done) => {
      spyOn(resolver, 'createPublicMenu$').and.returnValue(observableOf(true));
      spyOn(resolver, 'createAdminMenuIfLoggedIn$').and.returnValue(observableOf(true));

      resolver.resolve(null, null).subscribe(resolved => {
        expect(resolved).toBeTrue();
        expect(resolver.createPublicMenu$).toHaveBeenCalled();
        expect(resolver.createAdminMenuIfLoggedIn$).toHaveBeenCalled();
        done();
      });
    });

    it('should return an Observable that emits true as soon as all menus are created', () => {
      spyOn(resolver, 'createPublicMenu$').and.returnValue(cold('--(t|)', BOOLEAN));
      spyOn(resolver, 'createAdminMenuIfLoggedIn$').and.returnValue(cold('----(t|)', BOOLEAN));

      expect(resolver.resolve(null, null)).toBeObservable(cold('----(t|)', BOOLEAN));
    });
  });

  describe('createPublicMenu$', () => {
    it('should retrieve the menu by ID return an Observable that emits true as soon as it is created', () => {
      (menuService as any).getMenu.and.returnValue(cold('--u--m--', {
        u: undefined,
        m: MENU_STATE,
      }));

      expect(resolver.createPublicMenu$()).toBeObservable(cold('-----(t|)', BOOLEAN));
      expect(menuService.getMenu).toHaveBeenCalledOnceWith(MenuID.PUBLIC);
    });

    describe('contents', () => {
      beforeEach((done) => {
        resolver.createPublicMenu$().subscribe((_) => {
          done();
        });
      });

      it('should include community list link', () => {
        expect(menuService.addSection).toHaveBeenCalledWith(MenuID.PUBLIC, jasmine.objectContaining({
          id: 'browse_global_communities_and_collections', visible: true,
        }));
      });

      it('should include browse dropdown', () => {
        expect(menuService.addSection).toHaveBeenCalledWith(MenuID.PUBLIC, jasmine.objectContaining({
          id: 'browse_global_by_definition1', parentID: 'browse_global', visible: true,
        }));
        expect(menuService.addSection).toHaveBeenCalledWith(MenuID.PUBLIC, jasmine.objectContaining({
          id: 'browse_global_by_definition2', parentID: 'browse_global', visible: true,
        }));
        expect(menuService.addSection).toHaveBeenCalledWith(MenuID.PUBLIC, jasmine.objectContaining({
          id: 'browse_global_by_definition3', parentID: 'browse_global', visible: true,
        }));

        expect(menuService.addSection).toHaveBeenCalledWith(MenuID.PUBLIC, jasmine.objectContaining({
          id: 'browse_global', visible: true,
        }));
      });
    });
  });

  describe('createAdminMenu$', () => {
    const dontShowAdminSections = () => {
      it('should not show site admin section', () => {
        expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
          id: 'admin_search', visible: false,
        }));
        expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
          id: 'registries', visible: false,
        }));
        expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
          parentID: 'registries', visible: false,
        }));
        expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
          id: 'curation_tasks', visible: false,
        }));
        expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
          id: 'workflow', visible: false,
        }));
      });

      it('should not show access control section', () => {
        expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
          id: 'access_control', visible: false,
        }));
        expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
          parentID: 'access_control', visible: false,
        }));
      });

      // We check that the menu section has not been called with visible set to true
      // The reason why we don't check if it has been called with visible set to false
      // Is because the function does not get called unless a user is authorised
      it('should not show the import section', () => {
        expect(menuService.addSection).not.toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
          id: 'import', visible: true,
        }));
      });

      // We check that the menu section has not been called with visible set to true
      // The reason why we don't check if it has been called with visible set to false
      // Is because the function does not get called unless a user is authorised
      it('should not show the export section', () => {
        expect(menuService.addSection).not.toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
          id: 'export', visible: true,
        }));
      });
    };

    const dontShowNewSection = () => {
      it('should not show the "New" section', () => {
        expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
          id: 'new_community', visible: false,
        }));
        expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
          id: 'new_collection', visible: false,
        }));
        expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
          id: 'new_item', visible: false,
        }));
        expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
          id: 'new', visible: false,
        }));
      });
    };

    const dontShowEditSection = () => {
      it('should not show the "Edit" section', () => {
        expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
          id: 'edit_community', visible: false,
        }));
        expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
          id: 'edit_collection', visible: false,
        }));
        expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
          id: 'edit_item', visible: false,
        }));
        expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
          id: 'edit', visible: false,
        }));
      });
    };

    it('should retrieve the menu by ID return an Observable that emits true as soon as it is created', () => {
      (menuService as any).getMenu.and.returnValue(cold('--u--m', {
        u: undefined,
        m: MENU_STATE,
      }));

      expect(resolver.createAdminMenu$()).toBeObservable(cold('-----(t|)', BOOLEAN));
      expect(menuService.getMenu).toHaveBeenCalledOnceWith(MenuID.ADMIN);
    });

    describe('for regular user', () => {
      beforeEach(() => {
        authorizationService.isAuthorized = createSpy('isAuthorized').and.callFake((featureID) => {
          return observableOf(false);
        });
      });

      beforeEach((done) => {
        resolver.createAdminMenu$().subscribe((_) => {
          done();
        });
      });

      dontShowAdminSections();
      dontShowNewSection();
      dontShowEditSection();
    });

    describe('regular user who can submit', () => {
      beforeEach(() => {
        authorizationService.isAuthorized = createSpy('isAuthorized')
          .and.callFake((featureID: FeatureID) => {
            return observableOf(featureID === FeatureID.CanSubmit);
          });
      });

      beforeEach((done) => {
        resolver.createAdminMenu$().subscribe((_) => {
          done();
        });
      });

      it('should show "New Item" section', () => {
        expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
          id: 'new_item', visible: true,
        }));
        expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
          id: 'new', visible: true,
        }));
      });

      dontShowAdminSections();
      dontShowEditSection();
    });

    describe('regular user who can edit items', () => {
      beforeEach(() => {
        authorizationService.isAuthorized = createSpy('isAuthorized')
          .and.callFake((featureID: FeatureID) => {
            return observableOf(featureID === FeatureID.CanEditItem);
          });
      });

      beforeEach((done) => {
        resolver.createAdminMenu$().subscribe((_) => {
          done();
        });
      });

      it('should show "Edit Item" section', () => {
        expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
          id: 'edit_item', visible: true,
        }));
        expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
          id: 'edit', visible: true,
        }));
      });

      dontShowAdminSections();
      dontShowNewSection();
    });

    describe('for site admin', () => {
      beforeEach(() => {
        authorizationService.isAuthorized = createSpy('isAuthorized').and.callFake((featureID: FeatureID) => {
          return observableOf(featureID === FeatureID.AdministratorOf);
        });
      });

      beforeEach((done) => {
        resolver.createAdminMenu$().subscribe((_) => {
          done();
        });
      });

      it('should show new_process', () => {
        expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
          id: 'new_process', visible: true,
        }));
      });

      it('should contain site admin section', () => {
        expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
          id: 'admin_search', visible: true,
        }));
        expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
          id: 'registries', visible: true,
        }));
        expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
          parentID: 'registries', visible: true,
        }));
        expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
          id: 'curation_tasks', visible: true,
        }));
        expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
          id: 'workflow', visible: true,
        }));
        expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
          id: 'workflow', visible: true,
        }));
        expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
          id: 'import', visible: true,
        }));
        expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
          id: 'import_batch', parentID: 'import', visible: true,
        }));
        expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
          id: 'export', visible: true,
        }));
        expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
          id: 'export_batch', parentID: 'export', visible: true,
        }));
      });
    });

    describe('for community admin', () => {
      beforeEach(() => {
        authorizationService.isAuthorized = createSpy('isAuthorized').and.callFake((featureID: FeatureID) => {
          return observableOf(featureID === FeatureID.IsCommunityAdmin);
        });
      });

      beforeEach((done) => {
        resolver.createAdminMenu$().subscribe((_) => {
          done();
        });
      });

      it('should show edit_community', () => {
        expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
          id: 'edit_community', visible: true,
        }));
      });
    });

    describe('for collection admin', () => {
      beforeEach(() => {
        authorizationService.isAuthorized = createSpy('isAuthorized').and.callFake((featureID: FeatureID) => {
          return observableOf(featureID === FeatureID.IsCollectionAdmin);
        });
      });

      beforeEach((done) => {
        resolver.createAdminMenu$().subscribe((_) => {
          done();
        });
      });

      it('should show edit_collection', () => {
        expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
          id: 'edit_collection', visible: true,
        }));
      });
    });

    describe('for group admin', () => {
      beforeEach(() => {
        authorizationService.isAuthorized = createSpy('isAuthorized').and.callFake((featureID: FeatureID) => {
          return observableOf(featureID === FeatureID.CanManageGroups);
        });
      });

      beforeEach((done) => {
        resolver.createAdminMenu$().subscribe((_) => {
          done();
        });
      });

      it('should show access control section', () => {
        expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
          id: 'access_control', visible: true,
        }));
        expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
          parentID: 'access_control', visible: true,
        }));
      });
    });
  });
});