/* eslint-disable max-classes-per-file */
import { ThemedComponent } from './themed.component';
import { Component, NO_ERRORS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { VarDirective } from '../utils/var.directive';
import { ThemeService } from './theme.service';
import { getMockThemeService } from '../mocks/theme-service.mock';
import { TestComponent } from './test/test.component.spec';
import { ThemeConfig } from '../../../config/theme.config';

@Component({
  selector: 'ds-test-themed-component',
  templateUrl: './themed.component.html'
})
class TestThemedComponent extends ThemedComponent<TestComponent> {
  protected inAndOutputNames: (keyof TestComponent & keyof this)[] = ['testInput'];

  testInput = 'unset';

  protected getComponentName(): string {
    return 'TestComponent';
  }
  protected importThemedComponent(themeName: string): Promise<any> {
    return import(`./test/${themeName}/themed-test.component.spec`);
  }
  protected importUnthemedComponent(): Promise<any> {
    return import('./test/test.component.spec');
  }
}

describe('ThemedComponent', () => {
  let component: TestThemedComponent;
  let fixture: ComponentFixture<TestThemedComponent>;
  let themeService: ThemeService;

  function setupTestingModuleForTheme(theme: string, themes?: ThemeConfig[]) {
    themeService = getMockThemeService(theme, themes);
    TestBed.configureTestingModule({
      imports: [],
      declarations: [TestThemedComponent, VarDirective],
      providers: [
        { provide: ThemeService, useValue: themeService },
      ],
      schemas: [NO_ERRORS_SCHEMA]
    }).compileComponents();
  }

  function initComponent() {
    fixture = TestBed.createComponent(TestThemedComponent);
    component = fixture.componentInstance;
    spyOn(component as any, 'importThemedComponent').and.callThrough();
    component.testInput = 'changed';
    fixture.detectChanges();
  }

  describe('when the current theme matches a themed component', () => {
    beforeEach(waitForAsync(() => {
      setupTestingModuleForTheme('custom');
    }));

    beforeEach(initComponent);

    it('should set compRef to the themed component', waitForAsync(() => {
      fixture.whenStable().then(() => {
        expect((component as any).compRef.instance.type).toEqual('themed');
      });
    }));

    it('should sync up this component\'s input with the themed component', waitForAsync(() => {
      fixture.whenStable().then(() => {
        expect((component as any).compRef.instance.testInput).toEqual('changed');
      });
    }));

    it(`should set usedTheme to the name of the matched theme`, waitForAsync(() => {
      fixture.whenStable().then(() => {
        expect(component.usedTheme).toEqual('custom');
      });
    }));
  });

  describe('when the current theme doesn\'t match a themed component', () => {
    describe('and it doesn\'t extend another theme', () => {
      beforeEach(waitForAsync(() => {
        setupTestingModuleForTheme('non-existing-theme');
      }));

      beforeEach(initComponent);

      it('should set compRef to the default component', waitForAsync(() => {
        fixture.whenStable().then(() => {
          expect((component as any).compRef.instance.type).toEqual('default');
        });
      }));

      it('should sync up this component\'s input with the default component', waitForAsync(() => {
        fixture.whenStable().then(() => {
          expect((component as any).compRef.instance.testInput).toEqual('changed');
        });
      }));

      it(`should set usedTheme to the name of the base theme`, waitForAsync(() => {
        fixture.whenStable().then(() => {
          expect(component.usedTheme).toEqual('base');
        });
      }));
    });

    describe('and it extends another theme', () => {
      describe('that doesn\'t match it either', () => {
        beforeEach(waitForAsync(() => {
          setupTestingModuleForTheme('current-theme', [
            { name: 'current-theme', extends: 'non-existing-theme' },
          ]);
        }));

        beforeEach(initComponent);

        it('should set compRef to the default component', waitForAsync(() => {
          fixture.whenStable().then(() => {
            expect((component as any).importThemedComponent).toHaveBeenCalledWith('current-theme');
            expect((component as any).importThemedComponent).toHaveBeenCalledWith('non-existing-theme');
            expect((component as any).compRef.instance.type).toEqual('default');
          });
        }));

        it('should sync up this component\'s input with the default component', waitForAsync(() => {
          fixture.whenStable().then(() => {
            expect((component as any).compRef.instance.testInput).toEqual('changed');
          });
        }));

        it(`should set usedTheme to the name of the base theme`, waitForAsync(() => {
          fixture.whenStable().then(() => {
            expect(component.usedTheme).toEqual('base');
          });
        }));
      });

      describe('that does match it', () => {
        beforeEach(waitForAsync(() => {
          setupTestingModuleForTheme('current-theme', [
            { name: 'current-theme', extends: 'custom' },
          ]);
        }));

        beforeEach(initComponent);

        it('should set compRef to the themed component', waitForAsync(() => {
          fixture.whenStable().then(() => {
            expect((component as any).importThemedComponent).toHaveBeenCalledWith('current-theme');
            expect((component as any).importThemedComponent).toHaveBeenCalledWith('custom');
            expect((component as any).compRef.instance.type).toEqual('themed');
          });
        }));

        it('should sync up this component\'s input with the themed component', waitForAsync(() => {
          fixture.whenStable().then(() => {
            expect((component as any).compRef.instance.testInput).toEqual('changed');
          });
        }));

        it(`should set usedTheme to the name of the matched theme`, waitForAsync(() => {
          fixture.whenStable().then(() => {
            expect(component.usedTheme).toEqual('custom');
          });
        }));
      });

      describe('that extends another theme that doesn\'t match it either', () => {
        beforeEach(waitForAsync(() => {
          setupTestingModuleForTheme('current-theme', [
            { name: 'current-theme', extends: 'parent-theme' },
            { name: 'parent-theme', extends: 'non-existing-theme' },
          ]);
        }));

        beforeEach(initComponent);

        it('should set compRef to the default component', waitForAsync(() => {
          fixture.whenStable().then(() => {
            expect((component as any).importThemedComponent).toHaveBeenCalledWith('current-theme');
            expect((component as any).importThemedComponent).toHaveBeenCalledWith('parent-theme');
            expect((component as any).importThemedComponent).toHaveBeenCalledWith('non-existing-theme');
            expect((component as any).compRef.instance.type).toEqual('default');
          });
        }));

        it('should sync up this component\'s input with the default component', waitForAsync(() => {
          fixture.whenStable().then(() => {
            expect((component as any).compRef.instance.testInput).toEqual('changed');
          });
        }));

        it(`should set usedTheme to the name of the base theme`, waitForAsync(() => {
          fixture.whenStable().then(() => {
            expect(component.usedTheme).toEqual('base');
          });
        }));
      });

      describe('that extends another theme that does match it', () => {
        beforeEach(waitForAsync(() => {
          setupTestingModuleForTheme('current-theme', [
            { name: 'current-theme', extends: 'parent-theme' },
            { name: 'parent-theme', extends: 'custom' },
          ]);
        }));

        beforeEach(initComponent);

        it('should set compRef to the themed component', waitForAsync(() => {
          fixture.whenStable().then(() => {
            expect((component as any).importThemedComponent).toHaveBeenCalledWith('current-theme');
            expect((component as any).importThemedComponent).toHaveBeenCalledWith('parent-theme');
            expect((component as any).importThemedComponent).toHaveBeenCalledWith('custom');
            expect((component as any).compRef.instance.type).toEqual('themed');
          });
        }));

        it('should sync up this component\'s input with the themed component', waitForAsync(() => {
          fixture.whenStable().then(() => {
            expect((component as any).compRef.instance.testInput).toEqual('changed');
          });
        }));

        it(`should set usedTheme to the name of the matched theme`, waitForAsync(() => {
          fixture.whenStable().then(() => {
            expect(component.usedTheme).toEqual('custom');
          });
        }));
      });
    });
  });
});
