/**
 * @license
 * Copyright Google LLC All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.io/license
 */

import {DOCUMENT, ɵgetDOM as getDOM} from '@angular/common';
import {ResourceLoader} from '@angular/compiler';
import {
  APP_BOOTSTRAP_LISTENER,
  APP_INITIALIZER,
  ChangeDetectionStrategy,
  Compiler,
  CompilerFactory,
  Component,
  EnvironmentInjector,
  InjectionToken,
  LOCALE_ID,
  NgModule,
  NgZone,
  PlatformRef,
  provideZoneChangeDetection,
  RendererFactory2,
  TemplateRef,
  Type,
  ViewChild,
  ViewContainerRef,
} from '@angular/core';
import {ErrorHandler} from '@angular/core/src/error_handler';
import {ComponentRef} from '@angular/core/src/linker/component_factory';
import {createEnvironmentInjector, getLocaleId} from '@angular/core/src/render3';
import {BrowserModule} from '@angular/platform-browser';
import {DomRendererFactory2} from '@angular/platform-browser/src/dom/dom_renderer';
import {
  createTemplate,
  dispatchEvent,
  getContent,
} from '@angular/platform-browser/testing/src/browser_util';
import {expect} from '@angular/platform-browser/testing/src/matchers';
import type {ServerModule} from '@angular/platform-server';

import {ApplicationRef} from '../src/application/application_ref';
import {NoopNgZone} from '../src/zone/ng_zone';
import {ComponentFixtureNoNgZone, inject, TestBed, waitForAsync, withModule} from '../testing';
import {take} from 'rxjs/operators';

let serverPlatformModule: Promise<Type<ServerModule>> | null = null;
if (isNode) {
  // Only when we are in Node, we load the platform-server module. It will
  // be required later in specs for declaring the platform module.
  serverPlatformModule = import('@angular/platform-server').then((m) => m.ServerModule);
}

@Component({selector: 'bootstrap-app', template: 'hello'})
class SomeComponent {}

describe('bootstrap', () => {
  let mockConsole: MockConsole;

  beforeEach(() => {
    mockConsole = new MockConsole();
  });

  function createRootEl(selector = 'bootstrap-app') {
    const doc = TestBed.inject(DOCUMENT);
    const rootEl = <HTMLElement>(
      getContent(createTemplate(`<${selector}></${selector}>`)).firstChild
    );
    const oldRoots = doc.querySelectorAll(selector);
    for (let i = 0; i < oldRoots.length; i++) {
      getDOM().remove(oldRoots[i]);
    }
    doc.body.appendChild(rootEl);
  }

  type CreateModuleOptions = {
    providers?: any[];
    ngDoBootstrap?: any;
    bootstrap?: any[];
    component?: Type<any>;
  };

  async function createModule(providers?: any[]): Promise<Type<any>>;
  async function createModule(options: CreateModuleOptions): Promise<Type<any>>;
  async function createModule(
    providersOrOptions: any[] | CreateModuleOptions | undefined,
  ): Promise<Type<any>> {
    let options: CreateModuleOptions = {};
    if (Array.isArray(providersOrOptions)) {
      options = {providers: providersOrOptions};
    } else {
      options = providersOrOptions || {};
    }
    const errorHandler = new ErrorHandler();
    (errorHandler as any)._console = mockConsole as any;

    const platformModule = getDOM().supportsDOMEvents ? BrowserModule : await serverPlatformModule!;

    @NgModule({
      providers: [{provide: ErrorHandler, useValue: errorHandler}, options.providers || []],
      imports: [platformModule],
      declarations: [options.component || SomeComponent],
      bootstrap: options.bootstrap || [],
    })
    class MyModule {}
    if (options.ngDoBootstrap !== false) {
      (<any>MyModule.prototype).ngDoBootstrap = options.ngDoBootstrap || (() => {});
    }
    return MyModule;
  }

  it('should bootstrap a component from a child module', waitForAsync(
    inject([ApplicationRef, Compiler], (app: ApplicationRef, compiler: Compiler) => {
      @Component({
        selector: 'bootstrap-app',
        template: '',
      })
      class SomeComponent {}

      const helloToken = new InjectionToken<string>('hello');

      @NgModule({
        providers: [{provide: helloToken, useValue: 'component'}],
        declarations: [SomeComponent],
      })
      class SomeModule {}

      createRootEl();
      const modFactory = compiler.compileModuleSync(SomeModule);
      const module = modFactory.create(TestBed);
      const cmpFactory = module.componentFactoryResolver.resolveComponentFactory(SomeComponent);
      const component = app.bootstrap(cmpFactory);

      // The component should see the child module providers
      expect(component.injector.get(helloToken)).toEqual('component');
    }),
  ));

  it('should bootstrap a component with a custom selector', waitForAsync(
    inject([ApplicationRef, Compiler], (app: ApplicationRef, compiler: Compiler) => {
      @Component({
        selector: 'bootstrap-app',
        template: '',
      })
      class SomeComponent {}

      const helloToken = new InjectionToken<string>('hello');

      @NgModule({
        providers: [{provide: helloToken, useValue: 'component'}],
        declarations: [SomeComponent],
      })
      class SomeModule {}

      createRootEl('custom-selector');
      const modFactory = compiler.compileModuleSync(SomeModule);
      const module = modFactory.create(TestBed);
      const cmpFactory = module.componentFactoryResolver.resolveComponentFactory(SomeComponent);
      const component = app.bootstrap(cmpFactory, 'custom-selector');

      // The component should see the child module providers
      expect(component.injector.get(helloToken)).toEqual('component');
    }),
  ));

  describe('ApplicationRef', () => {
    beforeEach(async () => {
      TestBed.configureTestingModule({imports: [await createModule()]});
    });

    it('should throw when reentering tick', () => {
      @Component({template: '{{reenter()}}'})
      class ReenteringComponent {
        reenterCount = 1;
        reenterErr: any;

        constructor(private appRef: ApplicationRef) {}

        reenter() {
          if (this.reenterCount--) {
            try {
              this.appRef.tick();
            } catch (e) {
              this.reenterErr = e;
            }
          }
        }
      }

      const fixture = TestBed.configureTestingModule({
        declarations: [ReenteringComponent],
      }).createComponent(ReenteringComponent);
      const appRef = TestBed.inject(ApplicationRef);
      appRef.attachView(fixture.componentRef.hostView);
      appRef.tick();
      expect(fixture.componentInstance.reenterErr.message).toBe(
        'NG0101: ApplicationRef.tick is called recursively',
      );
    });

    describe('APP_BOOTSTRAP_LISTENER', () => {
      let capturedCompRefs: ComponentRef<any>[];
      beforeEach(() => {
        capturedCompRefs = [];
        TestBed.configureTestingModule({
          providers: [
            {
              provide: APP_BOOTSTRAP_LISTENER,
              multi: true,
              useValue: (compRef: any) => {
                capturedCompRefs.push(compRef);
              },
            },
          ],
        });
      });

      it('should be called when a component is bootstrapped', inject(
        [ApplicationRef],
        (ref: ApplicationRef) => {
          createRootEl();
          const compRef = ref.bootstrap(SomeComponent);
          expect(capturedCompRefs).toEqual([compRef]);
        },
      ));
    });

    describe('bootstrap', () => {
      it(
        'should throw if an APP_INITIIALIZER is not yet resolved',
        withModule(
          {
            providers: [
              {provide: APP_INITIALIZER, useValue: () => new Promise(() => {}), multi: true},
            ],
          },
          inject([ApplicationRef], (ref: ApplicationRef) => {
            createRootEl();
            expect(() => ref.bootstrap(SomeComponent)).toThrowError(
              'NG0405: Cannot bootstrap as there are still asynchronous initializers running. Bootstrap components in the `ngDoBootstrap` method of the root module.',
            );
          }),
        ),
      );
    });
  });

  describe('destroy', () => {
    const providers = [
      {provide: DOCUMENT, useFactory: () => document, deps: []},
      // Use the `DomRendererFactory2` as a renderer factory instead of the
      // `AnimationRendererFactory` one, which is configured as a part of the `ServerModule`, see
      // platform module setup above. This simplifies the tests (so they are sync vs async when
      // animations are in use) that verify that the DOM has been cleaned up after tests.
      {provide: RendererFactory2, useClass: DomRendererFactory2},
    ];
    // This function creates a new Injector instance with the `ApplicationRef` as a provider, so
    // that the instance of the `ApplicationRef` class is created on that injector (vs in the
    // app-level injector). It is needed to verify `ApplicationRef.destroy` scenarios, which
    // includes destroying an underlying injector.
    function createApplicationRefInjector(parentInjector: EnvironmentInjector) {
      const extraProviders = [{provide: ApplicationRef, useClass: ApplicationRef}];

      return createEnvironmentInjector(extraProviders, parentInjector);
    }

    function createApplicationRef(parentInjector: EnvironmentInjector) {
      const injector = createApplicationRefInjector(parentInjector);
      return injector.get(ApplicationRef);
    }

    it(
      'should cleanup the DOM',
      withModule(
        {providers},
        waitForAsync(
          inject(
            [EnvironmentInjector, DOCUMENT],
            (parentInjector: EnvironmentInjector, doc: Document) => {
              createRootEl();

              const appRef = createApplicationRef(parentInjector);
              appRef.bootstrap(SomeComponent);

              // The component template content (`hello`) is present in the document body.
              expect(doc.body.textContent!.indexOf('hello') > -1).toBeTrue();

              appRef.destroy();

              // The component template content (`hello`) is *not* present in the document
              // body, i.e. the DOM has been cleaned up.
              expect(doc.body.textContent!.indexOf('hello') === -1).toBeTrue();
            },
          ),
        ),
      ),
    );

    it(
      'should throw when trying to call `destroy` method on already destroyed ApplicationRef',
      withModule(
        {providers},
        waitForAsync(
          inject([EnvironmentInjector], (parentInjector: EnvironmentInjector) => {
            createRootEl();
            const appRef = createApplicationRef(parentInjector);
            appRef.bootstrap(SomeComponent);
            appRef.destroy();

            expect(() => appRef.destroy()).toThrowError(
              'NG0406: This instance of the `ApplicationRef` has already been destroyed.',
            );
          }),
        ),
      ),
    );

    it(
      'should invoke all registered `onDestroy` callbacks (internal API)',
      withModule(
        {providers},
        waitForAsync(
          inject([EnvironmentInjector], (parentInjector: EnvironmentInjector) => {
            const onDestroyA = jasmine.createSpy('onDestroyA');
            const onDestroyB = jasmine.createSpy('onDestroyB');
            createRootEl();

            const appRef = createApplicationRef(parentInjector) as unknown as ApplicationRef & {
              onDestroy: Function;
            };
            appRef.bootstrap(SomeComponent);
            appRef.onDestroy(onDestroyA);
            appRef.onDestroy(onDestroyB);
            appRef.destroy();

            expect(onDestroyA).toHaveBeenCalledTimes(1);
            expect(onDestroyB).toHaveBeenCalledTimes(1);
          }),
        ),
      ),
    );

    it(
      'should allow to unsubscribe a registered `onDestroy` callback (internal API)',
      withModule(
        {providers},
        waitForAsync(
          inject([EnvironmentInjector], (parentInjector: EnvironmentInjector) => {
            createRootEl();

            const appRef = createApplicationRef(parentInjector) as unknown as ApplicationRef & {
              onDestroy: Function;
            };
            appRef.bootstrap(SomeComponent);

            const onDestroyA = jasmine.createSpy('onDestroyA');
            const onDestroyB = jasmine.createSpy('onDestroyB');
            const unsubscribeOnDestroyA = appRef.onDestroy(onDestroyA);
            const unsubscribeOnDestroyB = appRef.onDestroy(onDestroyB);

            // Unsubscribe registered listeners.
            unsubscribeOnDestroyA();
            unsubscribeOnDestroyB();

            appRef.destroy();

            expect(onDestroyA).not.toHaveBeenCalled();
            expect(onDestroyB).not.toHaveBeenCalled();
          }),
        ),
      ),
    );

    it(
      'should correctly update the `destroyed` flag',
      withModule(
        {providers},
        waitForAsync(
          inject([EnvironmentInjector], (parentInjector: EnvironmentInjector) => {
            createRootEl();

            const appRef = createApplicationRef(parentInjector);
            appRef.bootstrap(SomeComponent);

            expect(appRef.destroyed).toBeFalse();

            appRef.destroy();

            expect(appRef.destroyed).toBeTrue();
          }),
        ),
      ),
    );

    it(
      'should also destroy underlying injector',
      withModule(
        {providers},
        waitForAsync(
          inject([EnvironmentInjector], (parentInjector: EnvironmentInjector) => {
            // This is a temporary type to represent an instance of an R3Injector, which
            // can be destroyed.
            // The type will be replaced with a different one once destroyable injector
            // type is available.
            type DestroyableInjector = EnvironmentInjector & {destroyed?: boolean};

            createRootEl();

            const injector = createApplicationRefInjector(parentInjector) as DestroyableInjector;

            const appRef = injector.get(ApplicationRef);
            appRef.bootstrap(SomeComponent);

            expect(appRef.destroyed).toBeFalse();
            expect(injector.destroyed).toBeFalse();

            appRef.destroy();

            expect(appRef.destroyed).toBeTrue();
            expect(injector.destroyed).toBeTrue();
          }),
        ),
      ),
    );
  });

  describe('bootstrapModule', () => {
    let defaultPlatform: PlatformRef;
    beforeEach(inject([PlatformRef], (_platform: PlatformRef) => {
      createRootEl();
      defaultPlatform = _platform;
    }));

    it('should wait for asynchronous app initializers', waitForAsync(async () => {
      let resolve: (result: any) => void;
      const promise: Promise<any> = new Promise((res) => {
        resolve = res;
      });
      let initializerDone = false;
      setTimeout(() => {
        resolve(true);
        initializerDone = true;
      }, 1);

      defaultPlatform
        .bootstrapModule(
          await createModule([{provide: APP_INITIALIZER, useValue: () => promise, multi: true}]),
        )
        .then((_) => {
          expect(initializerDone).toBe(true);
        });
    }));

    it('should rethrow sync errors even if the exceptionHandler is not rethrowing', waitForAsync(async () => {
      defaultPlatform
        .bootstrapModule(
          await createModule([
            {
              provide: APP_INITIALIZER,
              useValue: () => {
                throw 'Test';
              },
              multi: true,
            },
          ]),
        )
        .then(
          () => expect(false).toBe(true),
          (e) => {
            expect(e).toBe('Test');
            // Error rethrown will be seen by the exception handler since it's after
            // construction.
            expect(mockConsole.res[0].join('#')).toEqual('ERROR#Test');
          },
        );
    }));

    it('should rethrow promise errors even if the exceptionHandler is not rethrowing', waitForAsync(async () => {
      defaultPlatform
        .bootstrapModule(
          await createModule([
            {provide: APP_INITIALIZER, useValue: () => Promise.reject('Test'), multi: true},
          ]),
        )
        .then(
          () => expect(false).toBe(true),
          (e) => {
            expect(e).toBe('Test');
            expect(mockConsole.res[0].join('#')).toEqual('ERROR#Test');
          },
        );
    }));

    it('should throw useful error when ApplicationRef is not configured', waitForAsync(() => {
      @NgModule()
      class EmptyModule {}

      return defaultPlatform.bootstrapModule(EmptyModule).then(
        () => fail('expecting error'),
        (error) => {
          expect(error.message).toMatch(/NG0402/);
        },
      );
    }));

    it('should call the `ngDoBootstrap` method with `ApplicationRef` on the main module', waitForAsync(async () => {
      const ngDoBootstrap = jasmine.createSpy('ngDoBootstrap');
      defaultPlatform
        .bootstrapModule(await createModule({ngDoBootstrap: ngDoBootstrap}))
        .then((moduleRef) => {
          const appRef = moduleRef.injector.get(ApplicationRef);
          expect(ngDoBootstrap).toHaveBeenCalledWith(appRef);
        });
    }));

    it('should auto bootstrap components listed in @NgModule.bootstrap', waitForAsync(async () => {
      defaultPlatform
        .bootstrapModule(await createModule({bootstrap: [SomeComponent]}))
        .then((moduleRef) => {
          const appRef: ApplicationRef = moduleRef.injector.get(ApplicationRef);
          expect(appRef.componentTypes).toEqual([SomeComponent]);
        });
    }));

    it('should error if neither `ngDoBootstrap` nor @NgModule.bootstrap was specified', waitForAsync(async () => {
      defaultPlatform.bootstrapModule(await createModule({ngDoBootstrap: false})).then(
        () => expect(false).toBe(true),
        (e) => {
          const expectedErrMsg =
            `NG0403: The module MyModule was bootstrapped, ` +
            `but it does not declare "@NgModule.bootstrap" components nor a "ngDoBootstrap" method. ` +
            `Please define one of these. ` +
            `Find more at https://angular.dev/errors/NG0403`;
          expect(e.message).toEqual(expectedErrMsg);
          expect(mockConsole.res[0].join('#')).toEqual('ERROR#Error: ' + expectedErrMsg);
        },
      );
    }));

    it('should add bootstrapped module into platform modules list', waitForAsync(async () => {
      defaultPlatform
        .bootstrapModule(await createModule({bootstrap: [SomeComponent]}))
        .then((module) => expect((<any>defaultPlatform)._modules).toContain(module));
    }));

    it('should bootstrap with NoopNgZone', waitForAsync(async () => {
      defaultPlatform
        .bootstrapModule(await createModule({bootstrap: [SomeComponent]}), {ngZone: 'noop'})
        .then((module) => {
          const ngZone = module.injector.get(NgZone);
          expect(ngZone instanceof NoopNgZone).toBe(true);
        });
    }));

    it('should resolve component resources when creating module factory', async () => {
      @Component({
        selector: 'with-templates-app',
        templateUrl: '/test-template.html',
      })
      class WithTemplateUrlComponent {}

      const loadResourceSpy = jasmine.createSpy('load resource').and.returnValue('fakeContent');
      const testModule = await createModule({component: WithTemplateUrlComponent});

      await defaultPlatform.bootstrapModule(testModule, {
        providers: [{provide: ResourceLoader, useValue: {get: loadResourceSpy}}],
      });

      expect(loadResourceSpy).toHaveBeenCalledTimes(1);
      expect(loadResourceSpy).toHaveBeenCalledWith('/test-template.html');
    });

    it('should define `LOCALE_ID`', async () => {
      @Component({
        selector: 'i18n-app',
        templateUrl: '',
      })
      class I18nComponent {}

      const testModule = await createModule({
        component: I18nComponent,
        providers: [{provide: LOCALE_ID, useValue: 'ro'}],
      });
      await defaultPlatform.bootstrapModule(testModule);

      expect(getLocaleId()).toEqual('ro');
    });

    it('should wait for APP_INITIALIZER to set providers for `LOCALE_ID`', async () => {
      let locale: string = '';

      const testModule = await createModule({
        providers: [
          {provide: APP_INITIALIZER, useValue: () => (locale = 'fr-FR'), multi: true},
          {provide: LOCALE_ID, useFactory: () => locale},
        ],
      });
      const app = await defaultPlatform.bootstrapModule(testModule);
      expect(app.injector.get(LOCALE_ID)).toEqual('fr-FR');
    });
  });

  describe('bootstrapModuleFactory', () => {
    let defaultPlatform: PlatformRef;
    beforeEach(inject([PlatformRef], (_platform: PlatformRef) => {
      createRootEl();
      defaultPlatform = _platform;
    }));
    it('should wait for asynchronous app initializers', waitForAsync(async () => {
      let resolve: (result: any) => void;
      const promise: Promise<any> = new Promise((res) => {
        resolve = res;
      });
      let initializerDone = false;
      setTimeout(() => {
        resolve(true);
        initializerDone = true;
      }, 1);

      const compilerFactory: CompilerFactory = defaultPlatform.injector.get(CompilerFactory, null)!;
      const moduleFactory = compilerFactory
        .createCompiler()
        .compileModuleSync(
          await createModule([{provide: APP_INITIALIZER, useValue: () => promise, multi: true}]),
        );
      defaultPlatform.bootstrapModuleFactory(moduleFactory).then((_) => {
        expect(initializerDone).toBe(true);
      });
    }));

    it('should rethrow sync errors even if the exceptionHandler is not rethrowing', waitForAsync(async () => {
      const compilerFactory: CompilerFactory = defaultPlatform.injector.get(CompilerFactory, null)!;
      const moduleFactory = compilerFactory.createCompiler().compileModuleSync(
        await createModule([
          {
            provide: APP_INITIALIZER,
            useValue: () => {
              throw 'Test';
            },
            multi: true,
          },
        ]),
      );
      expect(() => defaultPlatform.bootstrapModuleFactory(moduleFactory)).toThrow('Test');
      // Error rethrown will be seen by the exception handler since it's after
      // construction.
      expect(mockConsole.res[0].join('#')).toEqual('ERROR#Test');
    }));

    it('should rethrow promise errors even if the exceptionHandler is not rethrowing', waitForAsync(async () => {
      const compilerFactory: CompilerFactory = defaultPlatform.injector.get(CompilerFactory, null)!;
      const moduleFactory = compilerFactory
        .createCompiler()
        .compileModuleSync(
          await createModule([
            {provide: APP_INITIALIZER, useValue: () => Promise.reject('Test'), multi: true},
          ]),
        );
      defaultPlatform.bootstrapModuleFactory(moduleFactory).then(
        () => expect(false).toBe(true),
        (e) => {
          expect(e).toBe('Test');
          expect(mockConsole.res[0].join('#')).toEqual('ERROR#Test');
        },
      );
    }));
  });

  describe('attachView / detachView', () => {
    @Component({template: '{{name}}'})
    class MyComp {
      name = 'Initial';
    }

    @Component({template: '<ng-container #vc></ng-container>'})
    class ContainerComp {
      @ViewChild('vc', {read: ViewContainerRef}) vc!: ViewContainerRef;
    }

    @Component({template: '<ng-template #t>Dynamic content</ng-template>'})
    class EmbeddedViewComp {
      @ViewChild(TemplateRef, {static: true}) tplRef!: TemplateRef<Object>;
    }

    beforeEach(() => {
      TestBed.configureTestingModule({
        declarations: [MyComp, ContainerComp, EmbeddedViewComp],
        providers: [{provide: ComponentFixtureNoNgZone, useValue: true}],
      });
    });

    it('should dirty check attached views', () => {
      const comp = TestBed.createComponent(MyComp);
      const appRef: ApplicationRef = TestBed.inject(ApplicationRef);
      expect(appRef.viewCount).toBe(0);

      appRef.tick();
      expect(comp.nativeElement).toHaveText('');

      appRef.attachView(comp.componentRef.hostView);
      appRef.tick();
      expect(appRef.viewCount).toBe(1);
      expect(comp.nativeElement).toHaveText('Initial');
    });

    it('should not dirty check detached views', () => {
      const comp = TestBed.createComponent(MyComp);
      const appRef: ApplicationRef = TestBed.inject(ApplicationRef);

      appRef.attachView(comp.componentRef.hostView);
      appRef.tick();
      expect(comp.nativeElement).toHaveText('Initial');

      appRef.detachView(comp.componentRef.hostView);
      comp.componentInstance.name = 'New';
      appRef.tick();
      expect(appRef.viewCount).toBe(0);
      expect(comp.nativeElement).toHaveText('Initial');
    });

    it('should not dirty host bindings of views not marked for check', () => {
      @Component({
        template: '',
        host: {'[class]': 'clazz'},
        standalone: true,
        changeDetection: ChangeDetectionStrategy.OnPush,
      })
      class HostBindingComp {
        clazz = 'initial';
      }
      const comp = TestBed.createComponent(HostBindingComp);
      const appRef = TestBed.inject(ApplicationRef);

      appRef.attachView(comp.componentRef.hostView);
      appRef.tick();
      expect(comp.nativeElement.outerHTML).toContain('initial');

      comp.componentInstance.clazz = 'new';
      appRef.tick();
      expect(comp.nativeElement.outerHTML).toContain('initial');

      comp.changeDetectorRef.markForCheck();
      appRef.tick();
      expect(comp.nativeElement.outerHTML).toContain('new');
    });

    it('should detach attached views if they are destroyed', () => {
      const comp = TestBed.createComponent(MyComp);
      const appRef: ApplicationRef = TestBed.inject(ApplicationRef);

      appRef.attachView(comp.componentRef.hostView);
      comp.destroy();

      expect(appRef.viewCount).toBe(0);
    });

    it('should detach attached embedded views if they are destroyed', () => {
      const comp = TestBed.createComponent(EmbeddedViewComp);
      const appRef: ApplicationRef = TestBed.inject(ApplicationRef);

      const embeddedViewRef = comp.componentInstance.tplRef.createEmbeddedView({});

      appRef.attachView(embeddedViewRef);
      embeddedViewRef.destroy();

      expect(appRef.viewCount).toBe(0);
    });

    it('should not allow to attach a view to both, a view container and the ApplicationRef', () => {
      const comp = TestBed.createComponent(MyComp);
      let hostView = comp.componentRef.hostView;
      const containerComp = TestBed.createComponent(ContainerComp);
      containerComp.detectChanges();
      const vc = containerComp.componentInstance.vc;
      const appRef: ApplicationRef = TestBed.inject(ApplicationRef);

      vc.insert(hostView);
      expect(() => appRef.attachView(hostView)).toThrowError(
        'NG0902: This view is already attached to a ViewContainer!',
      );
      hostView = vc.detach(0)!;

      appRef.attachView(hostView);
      expect(() => vc.insert(hostView)).toThrowError(
        'NG0902: This view is already attached directly to the ApplicationRef!',
      );
    });
  });
});

describe('AppRef', () => {
  @Component({selector: 'sync-comp', template: `<span>{{text}}</span>`})
  class SyncComp {
    text: string = '1';
  }

  @Component({selector: 'click-comp', template: `<span (click)="onClick()">{{text}}</span>`})
  class ClickComp {
    text: string = '1';

    onClick() {
      this.text += '1';
    }
  }

  @Component({selector: 'micro-task-comp', template: `<span>{{text}}</span>`})
  class MicroTaskComp {
    text: string = '1';

    ngOnInit() {
      Promise.resolve(null).then((_) => {
        this.text += '1';
      });
    }
  }

  @Component({selector: 'macro-task-comp', template: `<span>{{text}}</span>`})
  class MacroTaskComp {
    text: string = '1';

    ngOnInit() {
      setTimeout(() => {
        this.text += '1';
      }, 10);
    }
  }

  @Component({selector: 'micro-macro-task-comp', template: `<span>{{text}}</span>`})
  class MicroMacroTaskComp {
    text: string = '1';

    ngOnInit() {
      Promise.resolve(null).then((_) => {
        this.text += '1';
        setTimeout(() => {
          this.text += '1';
        }, 10);
      });
    }
  }

  @Component({selector: 'macro-micro-task-comp', template: `<span>{{text}}</span>`})
  class MacroMicroTaskComp {
    text: string = '1';

    ngOnInit() {
      setTimeout(() => {
        this.text += '1';
        Promise.resolve(null).then((_: any) => {
          this.text += '1';
        });
      }, 10);
    }
  }

  let stableCalled = false;

  beforeEach(() => {
    stableCalled = false;
    TestBed.configureTestingModule({
      providers: [provideZoneChangeDetection({ignoreChangesOutsideZone: true})],
      declarations: [
        SyncComp,
        MicroTaskComp,
        MacroTaskComp,
        MicroMacroTaskComp,
        MacroMicroTaskComp,
        ClickComp,
      ],
    });
  });

  afterEach(() => {
    expect(stableCalled).toBe(true, 'isStable did not emit true on stable');
  });

  function expectStableTexts(component: Type<any>, expected: string[]) {
    const fixture = TestBed.createComponent(component);
    const appRef: ApplicationRef = TestBed.inject(ApplicationRef);
    const zone: NgZone = TestBed.inject(NgZone);
    appRef.attachView(fixture.componentRef.hostView);
    zone.run(() => appRef.tick());

    let i = 0;
    appRef.isStable.subscribe({
      next: (stable: boolean) => {
        if (stable) {
          expect(i).toBeLessThan(expected.length);
          expect(fixture.nativeElement).toHaveText(expected[i++]);
          stableCalled = true;
        }
      },
    });
  }

  it('isStable should fire on synchronous component loading', waitForAsync(() => {
    expectStableTexts(SyncComp, ['1']);
  }));

  it('isStable should fire after a microtask on init is completed', waitForAsync(() => {
    expectStableTexts(MicroTaskComp, ['11']);
  }));

  it('isStable should fire after a macrotask on init is completed', waitForAsync(() => {
    expectStableTexts(MacroTaskComp, ['11']);
  }));

  it('isStable should fire only after chain of micro and macrotasks on init are completed', waitForAsync(() => {
    expectStableTexts(MicroMacroTaskComp, ['111']);
  }));

  it('isStable should fire only after chain of macro and microtasks on init are completed', waitForAsync(() => {
    expectStableTexts(MacroMicroTaskComp, ['111']);
  }));

  it('isStable can be subscribed to many times', async () => {
    const appRef: ApplicationRef = TestBed.inject(ApplicationRef);
    // Create stable subscription but do not unsubscribe before the second subscription is made
    appRef.isStable.subscribe();
    await expectAsync(appRef.isStable.pipe(take(1)).toPromise()).toBeResolved();
    stableCalled = true;
  });

  describe('unstable', () => {
    let unstableCalled = false;

    afterEach(() => {
      expect(unstableCalled).toBe(true, 'isStable did not emit false on unstable');
    });

    function expectUnstable(appRef: ApplicationRef) {
      appRef.isStable.subscribe({
        next: (stable: boolean) => {
          if (stable) {
            stableCalled = true;
          }
          if (!stable) {
            unstableCalled = true;
          }
        },
      });
    }

    it('should be fired after app becomes unstable', waitForAsync(() => {
      const fixture = TestBed.createComponent(ClickComp);
      const appRef: ApplicationRef = TestBed.inject(ApplicationRef);
      const zone: NgZone = TestBed.inject(NgZone);
      appRef.attachView(fixture.componentRef.hostView);
      zone.run(() => appRef.tick());

      fixture.whenStable().then(() => {
        expectUnstable(appRef);
        const element = fixture.debugElement.children[0];
        dispatchEvent(element.nativeElement, 'click');
      });
    }));
  });
});

describe('injector', () => {
  it('should expose an EnvironmentInjector', () => {
    @Component({})
    class TestCmp {
      constructor(readonly envInjector: EnvironmentInjector) {}
    }

    const fixture = TestBed.createComponent(TestCmp);
    const appRef = TestBed.inject(ApplicationRef);

    expect(appRef.injector).toBe(fixture.componentInstance.envInjector);
  });
});

class MockConsole {
  res: any[][] = [];
  log(...args: any[]): void {
    // Logging from ErrorHandler should run outside of the Angular Zone.
    NgZone.assertNotInAngularZone();
    this.res.push(args);
  }
  error(...args: any[]): void {
    // Logging from ErrorHandler should run outside of the Angular Zone.
    NgZone.assertNotInAngularZone();
    this.res.push(args);
  }
}
