import {
  DebugElement,
  Pipe,
  PipeTransform,
  PLATFORM_ID,
} from '@angular/core';
import {
  ComponentFixture,
  TestBed,
  waitForAsync,
} from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { TranslateModule } from '@ngx-translate/core';
import { of } from 'rxjs';

import { AuthService } from '../core/auth/auth.service';
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
import { RemoteData } from '../core/data/remote-data';
import { Bitstream } from '../core/shared/bitstream.model';
import { FileService } from '../core/shared/file.service';
import { getMockThemeService } from '../shared/mocks/theme-service.mock';
import {
  createFailedRemoteDataObject,
  createSuccessfulRemoteDataObject,
} from '../shared/remote-data.utils';
import { ThemeService } from '../shared/theme-support/theme.service';
import { SafeUrlPipe } from '../shared/utils/safe-url-pipe';
import { VarDirective } from '../shared/utils/var.directive';
import { ThumbnailComponent } from './thumbnail.component';

@Pipe({
  // eslint-disable-next-line @angular-eslint/pipe-prefix
  name: 'translate',
  standalone: true,
})
class MockTranslatePipe implements PipeTransform {
  transform(key: string): string {
    return 'TRANSLATED ' + key;
  }
}

const CONTENT = 'content.url';

describe('ThumbnailComponent', () => {
  let comp: ThumbnailComponent;
  let fixture: ComponentFixture<ThumbnailComponent>;
  let de: DebugElement;
  let el: HTMLElement;
  let authService;
  let authorizationService;
  let fileService;
  let spy;

  describe('when platform is browser', () => {
    beforeEach(waitForAsync(() => {
      authService = jasmine.createSpyObj('AuthService', {
        isAuthenticated: of(true),
      });
      authorizationService = jasmine.createSpyObj('AuthorizationService', {
        isAuthorized: of(true),
      });
      fileService = jasmine.createSpyObj('FileService', {
        retrieveFileDownloadLink: null,
      });
      fileService.retrieveFileDownloadLink.and.callFake((url) => of(`${url}?authentication-token=fake`));

      TestBed.configureTestingModule({
        imports: [
          TranslateModule.forRoot(),
          ThumbnailComponent,
          SafeUrlPipe,
          MockTranslatePipe,
          VarDirective,
        ],
        providers: [
          { provide: AuthService, useValue: authService },
          { provide: AuthorizationDataService, useValue: authorizationService },
          { provide: FileService, useValue: fileService },
          { provide: ThemeService, useValue: getMockThemeService() },
          { provide: PLATFORM_ID, useValue: 'browser' },
        ],
      }).overrideComponent(ThumbnailComponent, {
        add: {
          imports: [MockTranslatePipe],
        },
      })
        .compileComponents();
    }));

    beforeEach(() => {
      fixture = TestBed.createComponent(ThumbnailComponent);
      fixture.detectChanges();

      authService = TestBed.inject(AuthService);

      comp = fixture.componentInstance; // ThumbnailComponent test instance
      de = fixture.debugElement.query(By.css('div.thumbnail'));
      el = de.nativeElement;
    });

    describe('loading', () => {
      it('should start out with isLoading true', () => {
        expect(comp.isLoading()).toBeTrue();
      });

      it('should set isLoading$ to false once an image is successfully loaded', () => {
        comp.setSrc('http://bit.stream');
        fixture.debugElement.query(By.css('img.thumbnail-content')).triggerEventHandler('load', new Event('load'));
        expect(comp.isLoading()).toBeFalse();
      });

      it('should set isLoading$ to false once the src is set to null', () => {
        comp.setSrc(null);
        expect(comp.isLoading()).toBeFalse();
      });

      it('should show a loading animation while isLoading$ is true', () => {
        expect(de.query(By.css('ds-loading'))).toBeTruthy();

        comp.isLoading.set(false);
        fixture.detectChanges();
        expect(fixture.debugElement.query(By.css('ds-loading'))).toBeFalsy();
      });

      describe('with a thumbnail image', () => {
        beforeEach(() => {
          comp.src.set('https://bit.stream');
          fixture.detectChanges();
        });

        it('should render but hide the image while loading and show it once done', () => {
          let img = fixture.debugElement.query(By.css('img.thumbnail-content'));
          expect(img).toBeTruthy();
          expect(img.classes['d-none']).toBeTrue();

          comp.isLoading.set(false);
          fixture.detectChanges();
          img = fixture.debugElement.query(By.css('img.thumbnail-content'));
          expect(img).toBeTruthy();
          expect(img.classes['d-none']).toBeFalsy();
        });

      });

      describe('without a thumbnail image', () => {
        beforeEach(() => {
          comp.src.set(null);
          fixture.detectChanges();
        });

        it('should only show the HTML placeholder once done loading', () => {
          expect(fixture.debugElement.query(By.css('div.thumbnail-placeholder'))).toBeFalsy();

          comp.isLoading.set(false);
          fixture.detectChanges();
          expect(fixture.debugElement.query(By.css('div.thumbnail-placeholder'))).toBeTruthy();
        });
      });

    });

    const errorHandler = () => {
      let setSrcSpy;

      beforeEach(() => {
        // disconnect error handler to be sure it's only called once
        const img = fixture.debugElement.query(By.css('img.thumbnail-content'));
        img.nativeNode.onerror = null;

        comp.ngOnChanges({});
        setSrcSpy = spyOn(comp, 'setSrc').and.callThrough();
      });

      describe('retry with authentication token', () => {
        it('should remember that it already retried once', () => {
          expect(comp.retriedWithToken).toBeFalse();
          comp.errorHandler();
          expect(comp.retriedWithToken).toBeTrue();
        });

        describe('if not logged in', () => {
          beforeEach(() => {
            authService.isAuthenticated.and.returnValue(of(false));
          });

          it('should fall back to default', () => {
            comp.errorHandler();
            expect(setSrcSpy).toHaveBeenCalledWith(comp.defaultImage);
          });
        });

        describe('if logged in', () => {
          beforeEach(() => {
            authService.isAuthenticated.and.returnValue(of(true));
          });

          describe('and authorized to download the thumbnail', () => {
            beforeEach(() => {
              authorizationService.isAuthorized.and.returnValue(of(true));
            });

            it('should add an authentication token to the thumbnail URL', () => {
              comp.errorHandler();

              if ((comp.thumbnail as RemoteData<Bitstream>)?.hasFailed) {
                // If we failed to retrieve the Bitstream in the first place, fall back to the default
                expect(setSrcSpy).toHaveBeenCalledWith(comp.defaultImage);
              } else {
                expect(setSrcSpy).toHaveBeenCalledWith(CONTENT + '?authentication-token=fake');
              }
            });
          });

          describe('but not authorized to download the thumbnail', () => {
            beforeEach(() => {
              authorizationService.isAuthorized.and.returnValue(of(false));
            });

            it('should fall back to default', () => {
              comp.errorHandler();

              expect(setSrcSpy).toHaveBeenCalledWith(comp.defaultImage);

              // We don't need to check authorization if we failed to retrieve the Bitstreamin the first place
              if (!(comp.thumbnail as RemoteData<Bitstream>)?.hasFailed) {
                expect(authorizationService.isAuthorized).toHaveBeenCalled();
              }
            });
          });
        });
      });

      describe('after retrying with token', () => {
        beforeEach(() => {
          comp.retriedWithToken = true;
        });

        it('should fall back to default', () => {
          comp.errorHandler();
          expect(authService.isAuthenticated).not.toHaveBeenCalled();
          expect(fileService.retrieveFileDownloadLink).not.toHaveBeenCalled();
          expect(setSrcSpy).toHaveBeenCalledWith(comp.defaultImage);
        });
      });
    };

    describe('fallback', () => {
      describe('if there is a default image', () => {
        it('should display the default image', () => {
          comp.src.set('http://bit.stream');
          comp.defaultImage = 'http://default.img';
          comp.errorHandler();
          expect(comp.src()).toBe(comp.defaultImage);
        });

        it('should include the alt text', () => {
          comp.src.set('http://bit.stream');
          comp.defaultImage = 'http://default.img';
          comp.errorHandler();

          fixture.detectChanges();
          const image: HTMLElement = fixture.debugElement.query(By.css('img')).nativeElement;
          expect(image.getAttribute('alt')).toBe('TRANSLATED ' + comp.alt);
        });
      });

      describe('if there is no default image', () => {
        it('should display the HTML placeholder', () => {
          comp.src.set('http://default.img');
          comp.defaultImage = null;
          comp.errorHandler();
          expect(comp.src()).toBe(null);

          fixture.detectChanges();
          const placeholder = fixture.debugElement.query(By.css('div.thumbnail-placeholder')).nativeElement;
          expect(placeholder.innerHTML).toContain('TRANSLATED ' + comp.placeholder);
        });
      });
    });

    describe('with thumbnail as Bitstream', () => {
      let thumbnail;
      beforeEach(() => {
        thumbnail = new Bitstream();
        thumbnail._links = {
          self: { href: 'self.url' },
          bundle: { href: 'bundle.url' },
          format: { href: 'format.url' },
          content: { href: CONTENT },
          thumbnail: undefined,
          accessStatus: { href: 'accessStatus.url' },
        };
        comp.thumbnail = thumbnail;
      });

      describe('if content can be loaded', () => {
        it('should display an image', () => {
          comp.ngOnChanges({});
          fixture.detectChanges();
          const image: HTMLElement = fixture.debugElement.query(By.css('img')).nativeElement;
          expect(image.getAttribute('src')).toBe(thumbnail._links.content.href);
        });

        it('should include the alt text', () => {
          comp.ngOnChanges({});
          fixture.detectChanges();
          const image: HTMLElement = fixture.debugElement.query(By.css('img')).nativeElement;
          expect(image.getAttribute('alt')).toBe('TRANSLATED ' + comp.alt);
        });
      });

      describe('if content can\'t be loaded', () => {
        errorHandler();
      });
    });

    describe('with thumbnail as RemoteData<Bitstream>', () => {
      let thumbnail: Bitstream;

      beforeEach(() => {
        thumbnail = new Bitstream();
        thumbnail._links = {
          self: { href: 'self.url' },
          bundle: { href: 'bundle.url' },
          format: { href: 'format.url' },
          content: { href: CONTENT },
          thumbnail: undefined,
          accessStatus: { href: 'accessStatus.url' },
        };
      });

      describe('if RemoteData succeeded', () => {
        beforeEach(() => {
          comp.thumbnail = createSuccessfulRemoteDataObject(thumbnail);
        });

        describe('if content can be loaded', () => {
          it('should display an image', () => {
            comp.ngOnChanges({});
            fixture.detectChanges();
            const image: HTMLElement = de.query(By.css('img')).nativeElement;
            expect(image.getAttribute('src')).toBe(thumbnail._links.content.href);
          });

          it('should display the alt text', () => {
            comp.ngOnChanges({});
            fixture.detectChanges();
            const image: HTMLElement = de.query(By.css('img')).nativeElement;
            expect(image.getAttribute('alt')).toBe('TRANSLATED ' + comp.alt);
          });
        });

        describe('if content can\'t be loaded', () => {
          errorHandler();
        });
      });

      describe('if RemoteData failed', () => {
        beforeEach(() => {
          comp.thumbnail = createFailedRemoteDataObject();
        });

        it('should show the default image', () => {
          comp.defaultImage = 'default/image.jpg';
          comp.ngOnChanges({});
          expect(comp.src()).toBe('default/image.jpg');
        });
      });
    });
  });

  describe('when platform is server', () => {
    beforeEach(waitForAsync(() => {

      authService = jasmine.createSpyObj('AuthService', {
        isAuthenticated: of(true),
      });
      authorizationService = jasmine.createSpyObj('AuthorizationService', {
        isAuthorized: of(true),
      });
      fileService = jasmine.createSpyObj('FileService', {
        retrieveFileDownloadLink: null,
      });
      fileService.retrieveFileDownloadLink.and.callFake((url) => of(`${url}?authentication-token=fake`));

      TestBed.configureTestingModule({
        imports: [
          TranslateModule.forRoot(),
          ThumbnailComponent,
          SafeUrlPipe,
          MockTranslatePipe,
          VarDirective,
        ],
        providers: [
          { provide: AuthService, useValue: authService },
          { provide: AuthorizationDataService, useValue: authorizationService },
          { provide: FileService, useValue: fileService },
          { provide: ThemeService, useValue: getMockThemeService() },
          { provide: PLATFORM_ID, useValue: 'server' },
        ],
      }).overrideComponent(ThumbnailComponent, {
        add: {
          imports: [MockTranslatePipe],
        },
      })
        .compileComponents();
    }));

    beforeEach(() => {
      fixture = TestBed.createComponent(ThumbnailComponent);
      spyOn(fixture.componentInstance, 'setSrc').and.callThrough();
      fixture.detectChanges();

      authService = TestBed.inject(AuthService);

      comp = fixture.componentInstance; // ThumbnailComponent test instance
      de = fixture.debugElement.query(By.css('div.thumbnail'));
      el = de.nativeElement;
    });

    it('should start out with isLoading$ true', () => {
      expect(comp.isLoading()).toBeTrue();
      expect(de.query(By.css('ds-loading'))).toBeTruthy();
    });

    it('should not call setSrc', () => {
      expect(comp.setSrc).not.toHaveBeenCalled();
    });

  });
});
