import { Component, EventEmitter, Input, NgModule, Output, Type } from '@angular/core';

import { TestBed } from '@angular/core/testing';
import { BehaviorSubject } from 'rxjs';
import { ICollection } from '../types';
import { getStorybookModuleMetadata } from './StorybookModule';

describe('StorybookModule', () => {
  describe('getStorybookModuleMetadata', () => {
    describe('with simple component', () => {
      @Component({
        selector: 'foo',
        template: `
          <p id="input">{{ input }}</p>
          <p id="inputBindingPropertyName">{{ localPropertyName }}</p>
          <p id="localProperty">{{ localProperty }}</p>
          <p id="localFunction">{{ localFunction() }}</p>
          <p id="output" (click)="output.emit('outputEmitted')"></p>
          <p id="outputBindingPropertyName" (click)="localOutput.emit('outputEmitted')"></p>
        `,
      })
      class FooComponent {
        @Input()
        public input: string;

        @Input('inputBindingPropertyName')
        public localPropertyName: string;

        @Output()
        public output = new EventEmitter<string>();

        @Output('outputBindingPropertyName')
        public localOutput = new EventEmitter<string>();

        public localProperty: string;

        public localFunction = () => '';
      }

      it('should initialize inputs', async () => {
        const props = {
          input: 'input',
          inputBindingPropertyName: 'inputBindingPropertyName',
          localProperty: 'localProperty',
          localFunction: () => 'localFunction',
        };

        const ngModule = getStorybookModuleMetadata(
          { storyFnAngular: { props }, parameters: { component: FooComponent } },
          new BehaviorSubject(props)
        );

        const { fixture } = await configureTestingModule(ngModule);
        fixture.detectChanges();

        expect(fixture.nativeElement.querySelector('p#input').innerHTML).toEqual(props.input);
        expect(fixture.nativeElement.querySelector('p#inputBindingPropertyName').innerHTML).toEqual(
          props.inputBindingPropertyName
        );
        expect(fixture.nativeElement.querySelector('p#localProperty').innerHTML).toEqual(
          props.localProperty
        );
        expect(fixture.nativeElement.querySelector('p#localFunction').innerHTML).toEqual(
          props.localFunction()
        );
      });

      it('should initialize outputs', async () => {
        let expectedOutputValue: string;
        let expectedOutputBindingValue: string;
        const props = {
          output: (value: string) => {
            expectedOutputValue = value;
          },
          outputBindingPropertyName: (value: string) => {
            expectedOutputBindingValue = value;
          },
        };

        const ngModule = getStorybookModuleMetadata(
          { storyFnAngular: { props }, parameters: { component: FooComponent } },
          new BehaviorSubject(props)
        );

        const { fixture } = await configureTestingModule(ngModule);
        fixture.detectChanges();

        fixture.nativeElement.querySelector('p#output').click();
        fixture.nativeElement.querySelector('p#outputBindingPropertyName').click();

        expect(expectedOutputValue).toEqual('outputEmitted');
        expect(expectedOutputBindingValue).toEqual('outputEmitted');
      });

      it('should change inputs if storyProps$ Subject emit', async () => {
        const initialProps = {
          input: 'input',
        };
        const storyProps$ = new BehaviorSubject(initialProps);

        const ngModule = getStorybookModuleMetadata(
          { storyFnAngular: { props: initialProps }, parameters: { component: FooComponent } },
          storyProps$
        );
        const { fixture } = await configureTestingModule(ngModule);
        fixture.detectChanges();

        expect(fixture.nativeElement.querySelector('p#input').innerHTML).toEqual(
          initialProps.input
        );
        expect(fixture.nativeElement.querySelector('p#inputBindingPropertyName').innerHTML).toEqual(
          ''
        );

        const newProps = {
          input: 'new input',
          inputBindingPropertyName: 'new inputBindingPropertyName',
          localProperty: 'new localProperty',
          localFunction: () => 'new localFunction',
        };
        storyProps$.next(newProps);
        fixture.detectChanges();

        expect(fixture.nativeElement.querySelector('p#input').innerHTML).toEqual(newProps.input);
        expect(fixture.nativeElement.querySelector('p#inputBindingPropertyName').innerHTML).toEqual(
          newProps.inputBindingPropertyName
        );
        expect(fixture.nativeElement.querySelector('p#localProperty').innerHTML).toEqual(
          newProps.localProperty
        );
        expect(fixture.nativeElement.querySelector('p#localFunction').innerHTML).toEqual(
          newProps.localFunction()
        );
      });

      it('should override outputs if storyProps$ Subject emit', async () => {
        let expectedOutputValue;
        let expectedOutputBindingValue;
        const initialProps = {
          output: (value: string) => {
            expectedOutputValue = value;
          },
          outputBindingPropertyName: (value: string) => {
            expectedOutputBindingValue = value;
          },
        };
        const storyProps$ = new BehaviorSubject(initialProps);

        const ngModule = getStorybookModuleMetadata(
          { storyFnAngular: { props: initialProps }, parameters: { component: FooComponent } },
          storyProps$
        );
        const { fixture } = await configureTestingModule(ngModule);
        fixture.detectChanges();

        const newProps = {
          input: 'new input',
          output: () => {
            expectedOutputValue = 'should be called';
          },
          outputBindingPropertyName: () => {
            expectedOutputBindingValue = 'should be called';
          },
        };
        storyProps$.next(newProps);
        fixture.detectChanges();

        fixture.nativeElement.querySelector('p#output').click();
        fixture.nativeElement.querySelector('p#outputBindingPropertyName').click();

        expect(fixture.nativeElement.querySelector('p#input').innerHTML).toEqual(newProps.input);
        expect(expectedOutputValue).toEqual('should be called');
        expect(expectedOutputBindingValue).toEqual('should be called');
      });

      it('should change template inputs if storyProps$ Subject emit', async () => {
        const initialProps = {
          color: 'red',
          input: 'input',
        };
        const storyProps$ = new BehaviorSubject<ICollection>(initialProps);

        const ngModule = getStorybookModuleMetadata(
          {
            storyFnAngular: {
              props: initialProps,
              template: '<p [style.color]="color"><foo [input]="input"></foo></p>',
            },
            parameters: { component: FooComponent },
          },
          storyProps$
        );
        const { fixture } = await configureTestingModule(ngModule);
        fixture.detectChanges();
        expect(fixture.nativeElement.querySelector('p').style.color).toEqual('red');
        expect(fixture.nativeElement.querySelector('p#input').innerHTML).toEqual(
          initialProps.input
        );

        const newProps = {
          color: 'black',
          input: 'new input',
        };
        storyProps$.next(newProps);
        fixture.detectChanges();

        expect(fixture.nativeElement.querySelector('p').style.color).toEqual('black');
        expect(fixture.nativeElement.querySelector('p#input').innerHTML).toEqual(newProps.input);
      });
    });
  });

  async function configureTestingModule(ngModule: NgModule) {
    await TestBed.configureTestingModule({
      declarations: ngModule.declarations,
      providers: ngModule.providers,
    }).compileComponents();
    const fixture = TestBed.createComponent(ngModule.bootstrap[0] as Type<unknown>);

    return {
      fixture,
    };
  }
});
