/*!
 * Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved.
 *
 * Alfresco Example Content Application
 *
 * This file is part of the Alfresco Example Content Application.
 * If the software was purchased under a paid Alfresco license, the terms of
 * the paid license agreement will prevail. Otherwise, the software is
 * provided under the following open source license terms:
 *
 * The Alfresco Example Content Application is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * The Alfresco Example Content Application is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
 */

import { Router, ActivatedRoute } from '@angular/router';
import { TestBed, ComponentFixture, fakeAsync, tick } from '@angular/core/testing';
import {
  UserPreferencesService,
  AlfrescoApiService,
  AlfrescoApiServiceMock,
  AuthenticationService,
  TranslationMock,
  TranslationService,
  PipeModule
} from '@alfresco/adf-core';
import { UploadService, NodesApiService, DiscoveryApiService } from '@alfresco/adf-content-services';
import { AppState, ClosePreviewAction } from '@alfresco/aca-shared/store';
import { PreviewComponent } from './preview.component';
import { BehaviorSubject, Observable, of, throwError } from 'rxjs';
import { ContentApiService, AppHookService, DocumentBasePageService } from '@alfresco/aca-shared';
import { Store, StoreModule } from '@ngrx/store';
import { Node, NodePaging, FavoritePaging, SharedLinkPaging, PersonEntry, ResultSetPaging, RepositoryInfo } from '@alfresco/js-api';
import { PreviewModule } from '../preview.module';
import { TranslateModule } from '@ngx-translate/core';
import { RouterTestingModule } from '@angular/router/testing';
import { HttpClientModule } from '@angular/common/http';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { EffectsModule } from '@ngrx/effects';

class DocumentBasePageServiceMock extends DocumentBasePageService {
  canUpdateNode(): boolean {
    return true;
  }
  canUploadContent(): boolean {
    return true;
  }
}

export const INITIAL_APP_STATE: AppState = {
  appName: 'Alfresco Content Application',
  logoPath: 'assets/images/alfresco-logo-white.svg',
  customCssPath: '',
  webFontPath: '',
  sharedUrl: '',
  user: {
    isAdmin: null,
    id: null,
    firstName: '',
    lastName: ''
  },
  selection: {
    nodes: [],
    libraries: [],
    isEmpty: true,
    count: 0
  },
  navigation: {
    currentFolder: null
  },
  currentNodeVersion: null,
  infoDrawerOpened: false,
  infoDrawerPreview: false,
  infoDrawerMetadataAspect: '',
  showFacetFilter: true,
  fileUploadingDialog: true,
  documentDisplayMode: 'list',
  showLoader: false,
  repository: {
    status: {
      isQuickShareEnabled: true
    }
  } as any
};

describe('PreviewComponent', () => {
  let fixture: ComponentFixture<PreviewComponent>;
  let component: PreviewComponent;
  let router: Router;
  let route: ActivatedRoute;
  let preferences: UserPreferencesService;
  let contentApi: ContentApiService;
  let uploadService: UploadService;
  let nodesApiService: NodesApiService;
  let appHookService: AppHookService;
  let store: Store<any>;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [
        PreviewModule,
        NoopAnimationsModule,
        HttpClientModule,
        RouterTestingModule,
        TranslateModule.forRoot(),
        StoreModule.forRoot(
          { app: (state) => state },
          {
            initialState: {
              app: INITIAL_APP_STATE
            },
            runtimeChecks: {
              strictStateImmutability: false,
              strictActionImmutability: false
            }
          }
        ),
        EffectsModule.forRoot([]),
        PipeModule
      ],
      providers: [
        { provide: AlfrescoApiService, useClass: AlfrescoApiServiceMock },
        { provide: TranslationService, useClass: TranslationMock },
        { provide: DocumentBasePageService, useVale: new DocumentBasePageServiceMock() },
        {
          provide: DiscoveryApiService,
          useValue: {
            ecmProductInfo$: new BehaviorSubject<RepositoryInfo | null>(null),
            getEcmProductInfo: (): Observable<RepositoryInfo> => of(new RepositoryInfo({ version: '10.0.0' }))
          }
        },
        {
          provide: AuthenticationService,
          useValue: {
            isEcmLoggedIn: (): boolean => true,
            getRedirect: (): string | null => null,
            setRedirect() {},
            isOauth: (): boolean => false,
            isOAuthWithoutSilentLogin: (): boolean => false
          }
        }
      ]
    });

    fixture = TestBed.createComponent(PreviewComponent);
    component = fixture.componentInstance;

    router = TestBed.inject(Router);
    route = TestBed.inject(ActivatedRoute);
    preferences = TestBed.inject(UserPreferencesService);
    contentApi = TestBed.inject(ContentApiService);
    uploadService = TestBed.inject(UploadService);
    nodesApiService = TestBed.inject(NodesApiService);
    appHookService = TestBed.inject(AppHookService);
    store = TestBed.inject(Store);
  });

  it('should extract the property path root', () => {
    expect(component.getRootField('some.property.path')).toBe('some');
    expect(component.getRootField('some')).toBe('some');
    expect(component.getRootField('')).toBe('');
    expect(component.getRootField(null)).toBe(null);
  });

  it('should navigate to previous node in sub-folder', () => {
    spyOn(router, 'navigate').and.stub();
    const clickEvent = new MouseEvent('click');

    component.previewLocation = 'personal-files';
    component.folderId = 'folder1';
    component.previousNodeId = 'previous1';
    component.onNavigateBefore(clickEvent);

    expect(router.navigate).toHaveBeenCalledWith(['personal-files', 'folder1', 'preview', 'previous1']);
  });

  it('should navigate back to previous node in the root path', () => {
    spyOn(router, 'navigate').and.stub();
    const clickEvent = new MouseEvent('click');

    component.previewLocation = 'personal-files';
    component.folderId = null;
    component.previousNodeId = 'previous1';
    component.onNavigateBefore(clickEvent);

    expect(router.navigate).toHaveBeenCalledWith(['personal-files', 'preview', 'previous1']);
  });

  it('should not navigate back if node unset', () => {
    spyOn(router, 'navigate').and.stub();
    const clickEvent = new MouseEvent('click');

    component.previousNodeId = null;
    component.onNavigateBefore(clickEvent);

    expect(router.navigate).not.toHaveBeenCalled();
  });

  it('should navigate to next node in sub-folder', () => {
    spyOn(router, 'navigate').and.stub();
    const clickEvent = new MouseEvent('click');

    component.previewLocation = 'personal-files';
    component.folderId = 'folder1';
    component.nextNodeId = 'next1';
    component.onNavigateNext(clickEvent);

    expect(router.navigate).toHaveBeenCalledWith(['personal-files', 'folder1', 'preview', 'next1']);
  });

  it('should navigate to next node in the root path', () => {
    spyOn(router, 'navigate').and.stub();
    const clickEvent = new MouseEvent('click');

    component.previewLocation = 'personal-files';
    component.folderId = null;
    component.nextNodeId = 'next1';
    component.onNavigateNext(clickEvent);

    expect(router.navigate).toHaveBeenCalledWith(['personal-files', 'preview', 'next1']);
  });

  it('should not navigate back if node unset', () => {
    spyOn(router, 'navigate').and.stub();
    const clickEvent = new MouseEvent('click');

    component.nextNodeId = null;
    component.onNavigateNext(clickEvent);

    expect(router.navigate).not.toHaveBeenCalled();
  });

  it('should generate preview path for a folder only', () => {
    component.previewLocation = 'personal-files';

    expect(component.getPreviewPath('folder1', null)).toEqual(['personal-files', 'folder1']);
  });

  it('should generate preview path for a folder and a node', () => {
    component.previewLocation = 'personal-files';

    expect(component.getPreviewPath('folder1', 'node1')).toEqual(['personal-files', 'folder1', 'preview', 'node1']);
  });

  it('should generate preview path for a node only', () => {
    component.previewLocation = 'personal-files';

    expect(component.getPreviewPath(null, 'node1')).toEqual(['personal-files', 'preview', 'node1']);
  });

  it('should generate preview for the location only', () => {
    component.previewLocation = 'personal-files';

    expect(component.getPreviewPath(null, null)).toEqual(['personal-files']);
  });

  it('should navigate back to root path upon close', () => {
    spyOn(router, 'navigate').and.stub();

    component.routesSkipNavigation = [];
    component.previewLocation = 'libraries';
    component.folderId = null;

    component.onVisibilityChanged(false);

    expect(router.navigate).toHaveBeenCalledWith(['libraries', {}]);
  });

  it('should navigate back to folder path upon close', () => {
    spyOn(router, 'navigate').and.stub();

    component.routesSkipNavigation = [];
    component.previewLocation = 'libraries';
    component.folderId = 'site1';

    component.onVisibilityChanged(false);

    expect(router.navigate).toHaveBeenCalledWith(['libraries', {}, 'site1']);
  });

  it('should not navigate to root path for certain routes upon close', () => {
    spyOn(router, 'navigate').and.stub();

    component.routesSkipNavigation = ['shared'];
    component.previewLocation = 'shared';
    component.folderId = 'folder1';

    component.onVisibilityChanged(false);

    expect(router.navigate).toHaveBeenCalledWith(['shared', {}]);
  });

  it('should not navigate back if viewer is still visible', () => {
    spyOn(router, 'navigate').and.stub();

    component.routesSkipNavigation = [];
    component.previewLocation = 'shared';

    component.onVisibilityChanged(true);

    expect(router.navigate).not.toHaveBeenCalled();
  });

  it('should enable multiple document navigation from route data', () => {
    route.snapshot.data = {
      navigateMultiple: true
    };

    component.ngOnInit();

    expect(component.navigateMultiple).toBeTruthy();
  });

  it('should not enable multiple document navigation from route data', () => {
    route.snapshot.data = {};

    component.ngOnInit();

    expect(component.navigateMultiple).toBeFalsy();
  });

  it('should fetch navigation source from route', () => {
    route.snapshot.data = {
      navigateSource: 'personal-files'
    };

    component.ngOnInit();

    expect(component.navigateSource).toBe('personal-files');
  });

  it('should fetch case-insensitive source from route', () => {
    route.snapshot.data = {
      navigateSource: 'PERSONAL-FILES'
    };

    component.navigationSources = ['personal-files'];
    component.ngOnInit();

    expect(component.navigateSource).toBe('PERSONAL-FILES');
  });

  it('should fetch only permitted navigation source from route', () => {
    route.snapshot.data = {
      navigateSource: 'personal-files'
    };

    component.navigationSources = ['shared'];
    component.ngOnInit();

    expect(component.navigateSource).toBeNull();
  });

  it('should display document upon init', () => {
    route.params = of({
      folderId: 'folder1',
      nodeId: 'node1'
    });

    spyOn(component, 'displayNode').and.stub();

    component.ngOnInit();

    expect(component.folderId).toBe('folder1');
    expect(component.displayNode).toHaveBeenCalledWith('node1');
  });

  it('should return empty nearest nodes for missing node id', async () => {
    const nearest = await component.getNearestNodes(null, 'folder1');

    expect(nearest).toEqual({ left: null, right: null });
  });

  it('should return empty nearest nodes for missing folder id', async () => {
    const nearest = await component.getNearestNodes('node1', null);

    expect(nearest).toEqual({ left: null, right: null });
  });

  it('should return empty nearest nodes for crashed fields id request', async () => {
    spyOn(component, 'getFileIds').and.returnValue(Promise.reject('err'));

    const nearest = await component.getNearestNodes('node1', 'folder1');

    expect(nearest).toEqual({ left: null, right: null });
  });

  it('should return nearest nodes', async () => {
    spyOn(component, 'getFileIds').and.returnValue(Promise.resolve(['node1', 'node2', 'node3', 'node4', 'node5']));

    let nearest = await component.getNearestNodes('node1', 'folder1');
    expect(nearest).toEqual({ left: null, right: 'node2' });

    nearest = await component.getNearestNodes('node3', 'folder1');
    expect(nearest).toEqual({ left: 'node2', right: 'node4' });

    nearest = await component.getNearestNodes('node5', 'folder1');
    expect(nearest).toEqual({ left: 'node4', right: null });
  });

  it('should return empty nearest nodes if node is already deleted', async () => {
    spyOn(component, 'getFileIds').and.returnValue(Promise.resolve(['node1', 'node2', 'node3', 'node4', 'node5']));

    const nearest = await component.getNearestNodes('node9', 'folder1');
    expect(nearest).toEqual({ left: null, right: null });
  });

  it('should not display node when id is missing', async () => {
    spyOn(router, 'navigate').and.stub();
    spyOn(contentApi, 'getNodeInfo').and.returnValue(of(null));

    await component.displayNode(null);

    expect(contentApi.getNodeInfo).not.toHaveBeenCalled();
    expect(router.navigate).not.toHaveBeenCalled();
  });

  it('should navigate to original location if node not found', async () => {
    spyOn(router, 'navigate').and.stub();
    spyOn(contentApi, 'getNodeInfo').and.returnValue(throwError('error'));

    component.previewLocation = 'personal-files';
    await component.displayNode('folder1');

    expect(contentApi.getNodeInfo).toHaveBeenCalledWith('folder1');
    expect(router.navigate).toHaveBeenCalledWith(['personal-files', 'folder1']);
  });

  it('should navigate to original location if node is not a File', async () => {
    spyOn(router, 'navigate').and.stub();
    spyOn(contentApi, 'getNodeInfo').and.returnValue(
      of({
        isFile: false
      } as Node)
    );

    component.previewLocation = 'personal-files';
    await component.displayNode('folder1');

    expect(contentApi.getNodeInfo).toHaveBeenCalledWith('folder1');
    expect(router.navigate).toHaveBeenCalledWith(['personal-files', 'folder1']);
  });

  it('should navigate to original location in case of Alfresco API errors', async () => {
    spyOn(router, 'navigate').and.stub();
    spyOn(contentApi, 'getNodeInfo').and.returnValue(throwError('error'));

    component.previewLocation = 'personal-files';
    await component.displayNode('folder1');

    expect(contentApi.getNodeInfo).toHaveBeenCalledWith('folder1');
    expect(router.navigate).toHaveBeenCalledWith(['personal-files', 'folder1']);
  });

  it('should fetch and sort file ids for personal-files', async () => {
    preferences.set('personal-files.sorting.key', 'name');
    preferences.set('personal-files.sorting.direction', 'desc');

    spyOn(contentApi, 'getNodeChildren').and.returnValue(
      of({
        list: {
          entries: [{ entry: { id: 'node1', name: 'node 1' } }, { entry: { id: 'node2', name: 'node 2' } }]
        }
      } as NodePaging)
    );

    const ids = await component.getFileIds('personal-files', 'folder1');
    expect(ids).toEqual(['node2', 'node1']);
  });

  it('should fetch file ids for personal-files with default sorting for missing key', async () => {
    preferences.set('personal-files.sorting.key', 'missing');
    preferences.set('personal-files.sorting.direction', 'desc');

    spyOn(contentApi, 'getNodeChildren').and.returnValue(
      of({
        list: {
          entries: [{ entry: { id: 'node1', name: 'node 1' } }, { entry: { id: 'node2', name: 'node 2' } }]
        }
      } as NodePaging)
    );

    const ids = await component.getFileIds('personal-files', 'folder1');
    expect(ids).toEqual(['node1', 'node2']);
  });

  it('should sort file ids for personal-files with [modifiedAt desc]', async () => {
    spyOn(preferences, 'get').and.returnValue(null);

    spyOn(contentApi, 'getNodeChildren').and.returnValue(
      of({
        list: {
          entries: [
            { entry: { id: 'node1', name: 'node 1', modifiedAt: new Date(1) } },
            { entry: { id: 'node2', name: 'node 2', modifiedAt: new Date(2) } }
          ]
        }
      } as NodePaging)
    );

    const ids = await component.getFileIds('personal-files', 'folder1');
    expect(ids).toEqual(['node2', 'node1']);
  });

  it('should fetch and sort file ids for libraries', async () => {
    preferences.set('personal-files.sorting.key', 'name');
    preferences.set('personal-files.sorting.direction', 'desc');

    spyOn(contentApi, 'getNodeChildren').and.returnValue(
      of({
        list: {
          entries: [{ entry: { id: 'node1', name: 'node 1' } }, { entry: { id: 'node2', name: 'node 2' } }]
        }
      } as NodePaging)
    );

    const ids = await component.getFileIds('libraries', 'site1');
    expect(ids).toEqual(['node2', 'node1']);
  });

  it('should require folder id to fetch ids for libraries', async () => {
    const ids = await component.getFileIds('libraries', null);
    expect(ids).toEqual([]);
  });

  it('should sort file ids for libraries with [modifiedAt desc]', async () => {
    spyOn(preferences, 'get').and.returnValue(null);

    spyOn(contentApi, 'getNodeChildren').and.returnValue(
      of({
        list: {
          entries: [
            { entry: { id: 'node1', name: 'node 1', modifiedAt: new Date(1) } },
            { entry: { id: 'node2', name: 'node 2', modifiedAt: new Date(2) } }
          ]
        }
      } as NodePaging)
    );

    const ids = await component.getFileIds('libraries', 'folder1');
    expect(ids).toEqual(['node2', 'node1']);
  });

  it('should fetch and sort ids for favorites', async () => {
    preferences.set('favorites.sorting.key', 'name');
    preferences.set('favorites.sorting.direction', 'desc');

    spyOn(contentApi, 'getFavorites').and.returnValue(
      of({
        list: {
          entries: [
            { entry: { target: { file: { id: 'file3', name: 'file 3' } } } },
            { entry: { target: { file: { id: 'file1', name: 'file 1' } } } },
            { entry: { target: { file: { id: 'file2', name: 'file 2' } } } }
          ]
        }
      } as FavoritePaging)
    );

    const ids = await component.getFileIds('favorites');
    expect(ids).toEqual(['file3', 'file2', 'file1']);
  });

  it('should sort file ids for favorites with [modifiedAt desc]', async () => {
    spyOn(preferences, 'get').and.returnValue(null);

    spyOn(contentApi, 'getFavorites').and.returnValue(
      of({
        list: {
          entries: [
            {
              entry: {
                target: { file: { id: 'file3', modifiedAt: new Date(3) } }
              }
            },
            {
              entry: {
                target: { file: { id: 'file1', modifiedAt: new Date(1) } }
              }
            },
            {
              entry: {
                target: { file: { id: 'file2', modifiedAt: new Date(2) } }
              }
            }
          ]
        }
      } as FavoritePaging)
    );

    const ids = await component.getFileIds('favorites');
    expect(ids).toEqual(['file3', 'file2', 'file1']);
  });

  it('should fetch and sort file ids for shared files', async () => {
    preferences.set('shared.sorting.key', 'name');
    preferences.set('shared.sorting.direction', 'asc');

    spyOn(contentApi, 'findSharedLinks').and.returnValue(
      of({
        list: {
          entries: [
            {
              entry: {
                nodeId: 'node2',
                name: 'node 2',
                modifiedAt: new Date(2)
              }
            },
            {
              entry: {
                nodeId: 'node1',
                name: 'node 1',
                modifiedAt: new Date(1)
              }
            }
          ]
        }
      } as SharedLinkPaging)
    );

    const ids = await component.getFileIds('shared');
    expect(ids).toEqual(['node1', 'node2']);
  });

  it('should sort file ids for favorites with [modifiedAt desc]', async () => {
    spyOn(preferences, 'get').and.returnValue(null);

    spyOn(contentApi, 'findSharedLinks').and.returnValue(
      of({
        list: {
          entries: [
            {
              entry: {
                nodeId: 'node2',
                name: 'node 2',
                modifiedAt: new Date(2)
              }
            },
            {
              entry: {
                nodeId: 'node1',
                name: 'node 1',
                modifiedAt: new Date(1)
              }
            }
          ]
        }
      } as SharedLinkPaging)
    );

    const ids = await component.getFileIds('shared');
    expect(ids).toEqual(['node2', 'node1']);
  });

  it('should fetch and sort ids for recent-files', async () => {
    preferences.set('recent-files.sorting.key', 'name');
    preferences.set('recent-files.sorting.direction', 'asc');

    spyOn(contentApi, 'getPerson').and.returnValue(
      of({
        entry: { id: 'user' }
      } as PersonEntry)
    );

    spyOn(contentApi, 'search').and.returnValue(
      of({
        list: {
          entries: [
            { entry: { id: 'node2', name: 'node 2', modifiedAt: new Date(2) } },
            { entry: { id: 'node1', name: 'node 1', modifiedAt: new Date(1) } }
          ]
        }
      } as ResultSetPaging)
    );

    const ids = await component.getFileIds('recent-files');
    expect(ids).toEqual(['node1', 'node2']);
  });

  it('should sort file ids for favorites with [modifiedAt desc]', async () => {
    spyOn(preferences, 'get').and.returnValue(null);

    spyOn(contentApi, 'getPerson').and.returnValue(
      of({
        entry: { id: 'user' }
      } as PersonEntry)
    );

    spyOn(contentApi, 'search').and.returnValue(
      of({
        list: {
          entries: [
            { entry: { id: 'node2', name: 'node 2', modifiedAt: new Date(2) } },
            { entry: { id: 'node1', name: 'node 1', modifiedAt: new Date(1) } }
          ]
        }
      } as ResultSetPaging)
    );

    const ids = await component.getFileIds('recent-files');
    expect(ids).toEqual(['node2', 'node1']);
  });

  it('should return to parent folder on nodesDeleted event', async () => {
    spyOn(component, 'navigateToFileLocation');
    fixture.detectChanges();
    await fixture.whenStable();
    appHookService.nodesDeleted.next();

    expect(component.navigateToFileLocation).toHaveBeenCalled();
  });

  it('should return to parent folder on fileUploadDeleted event', async () => {
    spyOn(component, 'navigateToFileLocation');
    fixture.detectChanges();
    await fixture.whenStable();
    uploadService.fileUploadDeleted.next();

    expect(component.navigateToFileLocation).toHaveBeenCalled();
  });

  it('should emit nodeUpdated event on fileUploadComplete event', fakeAsync(() => {
    spyOn(nodesApiService.nodeUpdated, 'next');
    fixture.detectChanges();
    uploadService.fileUploadComplete.next({ data: { entry: {} } } as any);
    tick(300);

    expect(nodesApiService.nodeUpdated.next).toHaveBeenCalled();
  }));

  it('should return to parent folder when event emitted from extension', async () => {
    spyOn(component, 'navigateToFileLocation');
    fixture.detectChanges();
    await fixture.whenStable();
    store.dispatch(new ClosePreviewAction());
    expect(component.navigateToFileLocation).toHaveBeenCalled();
  });

  describe('Keyboard navigation', () => {
    beforeEach(() => {
      component.nextNodeId = 'nextNodeId';
      component.previousNodeId = 'previousNodeId';
      spyOn(router, 'navigate').and.stub();
    });

    afterEach(() => {
      fixture.destroy();
    });

    it('should not navigate on keyboard event if target is child of sidebar container', () => {
      const parent = document.createElement('div');
      parent.className = 'adf-viewer__sidebar';

      const child = document.createElement('button');
      child.addEventListener('keyup', function (e) {
        component.onNavigateNext(e);
      });
      parent.appendChild(child);
      document.body.appendChild(parent);

      child.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowLeft' }));

      expect(router.navigate).not.toHaveBeenCalled();
    });

    it('should not navigate on keyboard event if target is child of cdk overlay', () => {
      const parent = document.createElement('div');
      parent.className = 'cdk-overlay-container';

      const child = document.createElement('button');
      child.addEventListener('keyup', function (e) {
        component.onNavigateNext(e);
      });
      parent.appendChild(child);
      document.body.appendChild(parent);

      child.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowLeft' }));

      expect(router.navigate).not.toHaveBeenCalled();
    });
  });
});
