import {
  Component,
  DebugElement,
  Input,
} from '@angular/core';
import {
  ComponentFixture,
  TestBed,
  waitForAsync,
} from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
import { PlacementArray } from '@ng-bootstrap/ng-bootstrap/util/positioning';
import { TranslateService } from '@ngx-translate/core';
import {
  BehaviorSubject,
  of as observableOf,
} from 'rxjs';

import { ContextHelp } from '../context-help.model';
import { ContextHelpService } from '../context-help.service';
import { ContextHelpWrapperComponent } from './context-help-wrapper.component';
import { PlacementDir } from './placement-dir.model';

@Component({
  template: `
    <ng-template #div>template</ng-template>
    <ds-context-help-wrapper
      #chwrapper
      [templateRef]="div"
      [content]="content"
      [id]="id"
      [tooltipPlacement]="tooltipPlacement"
      [iconPlacement]="iconPlacement"
      [dontParseLinks]="dontParseLinks"
    >
    </ds-context-help-wrapper>
  `,
  standalone: true,
  imports: [NgbTooltipModule, ContextHelpWrapperComponent],
})
class TemplateComponent {
  @Input() content: string;
  @Input() id: string;
  @Input() tooltipPlacement?: PlacementArray;
  @Input() iconPlacement?: PlacementDir;
  @Input() dontParseLinks?: boolean;
}

const messages = {
  lorem: 'lorem ipsum dolor sit amet',
  linkTest: 'This is text, [this](https://dspace.lyrasis.org/) is a link, and [so is this](https://google.com/)',
};
const exampleContextHelp: ContextHelp = {
  id: 'test-tooltip',
  isTooltipVisible: false,
};

describe('ContextHelpWrapperComponent', () => {
  let templateComponent: TemplateComponent;
  let wrapperComponent: ContextHelpWrapperComponent;
  let fixture: ComponentFixture<TemplateComponent>;
  let el: DebugElement;
  let translateService: any;
  let contextHelpService: any;
  let getContextHelp$: BehaviorSubject<ContextHelp>;
  let shouldShowIcons$: BehaviorSubject<boolean>;

  function makeWrappedElement(): HTMLElement {
    const wrapped: HTMLElement = document.createElement('div');
    wrapped.innerHTML = 'example element';
    return wrapped;
  }

  beforeEach(waitForAsync( () => {
    translateService = jasmine.createSpyObj('translateService', ['get']);
    contextHelpService = jasmine.createSpyObj('contextHelpService', [
      'shouldShowIcons$',
      'getContextHelp$',
      'add',
      'remove',
      'toggleIcons',
      'toggleTooltip',
      'showTooltip',
      'hideTooltip',
    ]);

    TestBed.configureTestingModule({
      imports: [NgbTooltipModule, TemplateComponent, ContextHelpWrapperComponent],
      providers: [
        { provide: TranslateService, useValue: translateService },
        { provide: ContextHelpService, useValue: contextHelpService },
      ],
    }).compileComponents();
  }));

  beforeEach(() => {
    // Initializing services.
    getContextHelp$ = new BehaviorSubject<ContextHelp>(exampleContextHelp);
    shouldShowIcons$ = new BehaviorSubject<boolean>(false);
    contextHelpService.getContextHelp$.and.returnValue(getContextHelp$);
    contextHelpService.shouldShowIcons$.and.returnValue(shouldShowIcons$);
    translateService.get.and.callFake((content) => observableOf(messages[content]));

    getContextHelp$.next(exampleContextHelp);
    shouldShowIcons$.next(false);

    // Initializing components.
    fixture = TestBed.createComponent(TemplateComponent);
    el = fixture.debugElement;
    templateComponent = fixture.componentInstance;
    templateComponent.content = 'lorem';
    templateComponent.id = 'test-tooltip';
    templateComponent.tooltipPlacement = ['bottom'];
    templateComponent.iconPlacement = 'left';
    wrapperComponent = el.query(By.css('ds-context-help-wrapper')).componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(templateComponent).toBeDefined();
    expect(wrapperComponent).toBeDefined();
  });

  it('should not show the context help icon while icon visibility is not turned on', (done) => {
    fixture.whenStable().then(() => {
      const wrapper = el.query(By.css('ds-context-help-wrapper')).nativeElement;
      expect(wrapper.children.length).toBe(0);
      done();
    });
  });

  describe('when icon visibility is turned on', () => {
    beforeEach(() => {
      shouldShowIcons$.next(true);
      fixture.detectChanges();
      spyOn(wrapperComponent.tooltip, 'open').and.callThrough();
      spyOn(wrapperComponent.tooltip, 'close').and.callThrough();
    });

    it('should show the context help button', (done) => {
      fixture.whenStable().then(() => {
        const wrapper = el.query(By.css('ds-context-help-wrapper')).nativeElement;
        expect(wrapper.children.length).toBe(1);
        const [i] = wrapper.children;
        expect(i.tagName).toBe('I');
        done();
      });
    });

    describe('after the icon is clicked', () => {
      let i;
      beforeEach(() => {
        i = el.query(By.css('.ds-context-help-icon')).nativeElement;
        i.click();
        fixture.detectChanges();
      });

      it('should display the tooltip', () => {
        expect(contextHelpService.toggleTooltip).toHaveBeenCalledWith('test-tooltip');
        getContextHelp$.next({ ...exampleContextHelp, isTooltipVisible: true });
        fixture.detectChanges();
        expect(wrapperComponent.tooltip.open).toHaveBeenCalled();
        expect(wrapperComponent.tooltip.close).toHaveBeenCalledTimes(0);
        expect(fixture.debugElement.query(By.css('.ds-context-help-content')).nativeElement.textContent)
          .toMatch(/\s*lorem ipsum dolor sit amet\s*/);
      });

      it('should correctly display links', () => {
        templateComponent.content = 'linkTest';
        getContextHelp$.next({ ...exampleContextHelp, isTooltipVisible: true });
        fixture.detectChanges();
        const nodeList: NodeList = fixture.debugElement.query(By.css('.ds-context-help-content'))
          .nativeElement
          .childNodes;
        const relevantNodes = Array.from(nodeList).filter(node => node.nodeType !== Node.COMMENT_NODE);
        expect(relevantNodes.length).toBe(4);

        const [text1, link1, text2, link2] = relevantNodes;

        expect(text1.nodeType).toBe(Node.TEXT_NODE);
        expect(text1.nodeValue).toMatch(/\s* This is text, \s*/);

        expect(link1.nodeName).toBe('A');
        expect((link1 as any).href).toBe('https://dspace.lyrasis.org/');
        expect(link1.textContent).toBe('this');

        expect(text2.nodeType).toBe(Node.TEXT_NODE);
        expect(text2.nodeValue).toMatch(/\s* is a link, and \s*/);

        expect(link2.nodeName).toBe('A');
        expect((link2 as any).href).toBe('https://google.com/');
        expect(link2.textContent).toBe('so is this');
      });

      it('should not display links if specified not to', () => {
        templateComponent.dontParseLinks = true;
        templateComponent.content = 'linkTest';
        getContextHelp$.next({ ...exampleContextHelp, isTooltipVisible: true });
        fixture.detectChanges();


        const nodeList: NodeList = fixture.debugElement.query(By.css('.ds-context-help-content'))
          .nativeElement
          .childNodes;
        const relevantNodes = Array.from(nodeList).filter(node => node.nodeType !== Node.COMMENT_NODE);
        expect(relevantNodes.length).toBe(1);

        const [text] = relevantNodes;

        expect(text.nodeType).toBe(Node.TEXT_NODE);
        expect(text.nodeValue).toMatch(
          /\s* This is text, \[this\]\(https:\/\/dspace.lyrasis.org\/\) is a link, and \[so is this\]\(https:\/\/google.com\/\) \s*/);
      });

      describe('after the icon is clicked again', () => {
        beforeEach(() => {
          i.click();
          fixture.detectChanges();
          spyOn(wrapperComponent.tooltip, 'isOpen').and.returnValue(true);
        });

        it('should close the tooltip', () => {
          expect(contextHelpService.toggleTooltip).toHaveBeenCalledWith('test-tooltip');
          getContextHelp$.next({ ...exampleContextHelp, isTooltipVisible: false });
          fixture.detectChanges();
          expect(wrapperComponent.tooltip.close).toHaveBeenCalled();
        });
      });
    });
  });
});