/**
 * @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.dev/license
 */

import {animate, style, transition, trigger} from '@angular/animations';
import {DOCUMENT, isPlatformBrowser, ɵgetDOM as getDOM} from '@angular/common';
import {
  ANIMATION_MODULE_TYPE,
  APP_INITIALIZER,
  Compiler,
  Component,
  createPlatformFactory,
  CUSTOM_ELEMENTS_SCHEMA,
  Directive,
  ErrorHandler,
  importProvidersFrom,
  Inject,
  inject as _inject,
  InjectionToken,
  Injector,
  LOCALE_ID,
  NgModule,
  NgModuleRef,
  NgZone,
  OnDestroy,
  PLATFORM_ID,
  PLATFORM_INITIALIZER,
  Provider,
  provideZoneChangeDetection,
  Sanitizer,
  StaticProvider,
  Testability,
  TestabilityRegistry,
  TransferState,
  Type,
  VERSION,
  EnvironmentProviders,
  ApplicationRef,
  ɵConsole as Console,
  ComponentRef,
  destroyPlatform,
  providePlatformInitializer,
  ɵcreateOrReusePlatformInjector as createOrReusePlatformInjector,
  APP_ID,
} from '@angular/core';
import {ɵLog as Log, inject, TestBed} from '@angular/core/testing';
import {BrowserModule} from '../../index';
import {provideAnimations, provideNoopAnimations} from '../../animations';
import {expect} from '@angular/private/testing/matchers';
import {isNode, withBody} from '@angular/private/testing';

import {bootstrapApplication, platformBrowser} from '../../src/browser';

@Component({
  selector: 'non-existent',
  template: '',
  standalone: false,
})
class NonExistentComp {}

@Component({
  selector: 'hello-app',
  template: '{{greeting}} world!',
  standalone: false,
})
class HelloRootCmp {
  greeting: string;

  constructor() {
    this.greeting = 'hello';
  }
}

@Component({
  selector: 'hello-app-2',
  template: '{{greeting}} world, again!',
  standalone: false,
})
class HelloRootCmp2 {
  greeting: string;

  constructor() {
    this.greeting = 'hello';
  }
}

@Component({
  selector: 'hello-app',
  template: '',
  standalone: false,
})
class HelloRootCmp3 {
  appBinding: string;

  constructor(@Inject('appBinding') appBinding: string) {
    this.appBinding = appBinding;
  }
}

@Component({
  selector: 'hello-app',
  template: '',
  standalone: false,
})
class HelloRootCmp4 {
  appRef: ApplicationRef;

  constructor(@Inject(ApplicationRef) appRef: ApplicationRef) {
    this.appRef = appRef;
  }
}

@Directive({
  selector: 'hello-app',
  standalone: false,
})
class HelloRootDirectiveIsNotCmp {}

@Component({
  selector: 'hello-app',
  template: '',
  standalone: false,
})
class HelloOnDestroyTickCmp implements OnDestroy {
  appRef: ApplicationRef;

  constructor(@Inject(ApplicationRef) appRef: ApplicationRef) {
    this.appRef = appRef;
  }

  ngOnDestroy(): void {
    this.appRef.tick();
  }
}

@Component({
  selector: 'hello-app',
  template: '<some-el [someProp]="true">hello world!</some-el>',
  standalone: false,
})
class HelloCmpUsingCustomElement {}

class MockConsole {
  res: any[][] = [];

  error(...s: any[]): void {
    this.res.push(s);
  }
}

class DummyConsole implements Console {
  public warnings: string[] = [];

  log(message: string) {}

  warn(message: string) {
    this.warnings.push(message);
  }
}

function bootstrap(
  cmpType: any,
  providers: Provider[] = [],
  platformProviders: StaticProvider[] = [],
  imports: Type<any>[] = [],
): Promise<any> {
  @NgModule({
    imports: [BrowserModule, ...imports],
    declarations: [cmpType],
    bootstrap: [cmpType],
    providers: [provideZoneChangeDetection(), ...providers],
    schemas: [CUSTOM_ELEMENTS_SCHEMA],
  })
  class TestModule {}
  return platformBrowser(platformProviders).bootstrapModule(TestModule);
}

describe('bootstrap factory method', () => {
  let el: HTMLElement;
  let el2: HTMLElement;
  let testProviders: Provider[];
  let lightDom: HTMLElement;

  if (isNode) {
    // Jasmine will throw if there are no tests.
    it('should pass', () => {});
    return;
  }

  let compilerConsole: DummyConsole;

  beforeEach(() => {
    TestBed.configureTestingModule({providers: [Log]});
  });

  beforeEach(inject([DOCUMENT], (doc: any) => {
    destroyPlatform();
    compilerConsole = new DummyConsole();
    testProviders = [{provide: Console, useValue: compilerConsole}];

    const oldRoots = doc.querySelectorAll('hello-app,hello-app-2,light-dom-el');
    for (let i = 0; i < oldRoots.length; i++) {
      getDOM().remove(oldRoots[i]);
    }

    el = getDOM().createElement('hello-app', doc);
    el2 = getDOM().createElement('hello-app-2', doc);
    lightDom = getDOM().createElement('light-dom-el', doc);
    doc.body.appendChild(el);
    doc.body.appendChild(el2);
    el.appendChild(lightDom);
    lightDom.textContent = 'loading';
  }));

  afterEach(destroyPlatform);

  describe('bootstrapApplication', () => {
    const NAME = new InjectionToken<string>('name');

    @Component({
      selector: 'hello-app',
      template: 'Hello from {{ name }}!',
    })
    class SimpleComp {
      name = 'SimpleComp';
    }

    @Component({
      selector: 'hello-app-2',
      template: 'Hello from {{ name }}!',
    })
    class SimpleComp2 {
      name = 'SimpleComp2';
    }

    @Component({
      selector: 'hello-app',
      template: 'Hello from {{ name }}!',
    })
    class ComponentWithDeps {
      constructor(@Inject(NAME) public name: string) {}
    }

    @Component({
      selector: 'hello-app-2',
      template: 'Hello from {{ name }}!',
      standalone: false,
    })
    class NonStandaloneComp {
      name = 'NonStandaloneComp';
    }

    @NgModule({
      declarations: [NonStandaloneComp],
    })
    class NonStandaloneCompModule {}

    it('should work for simple standalone components', async () => {
      await bootstrapApplication(SimpleComp);
      expect(el.innerText).toBe('Hello from SimpleComp!');
    });

    it('should allow passing providers during the bootstrap', async () => {
      const providers = [{provide: NAME, useValue: 'Name via DI'}];
      await bootstrapApplication(ComponentWithDeps, {providers});
      expect(el.innerText).toBe('Hello from Name via DI!');
    });

    it('should reuse existing platform', async () => {
      const platformProviders = [{provide: NAME, useValue: 'Name via DI (Platform level)'}];
      platformBrowser(platformProviders);

      await bootstrapApplication(ComponentWithDeps);
      expect(el.innerText).toBe('Hello from Name via DI (Platform level)!');
    });

    it('should allow bootstrapping multiple apps', async () => {
      await bootstrapApplication(SimpleComp);
      await bootstrapApplication(SimpleComp2);

      expect(el.innerText).toBe('Hello from SimpleComp!');
      expect(el2.innerText).toBe('Hello from SimpleComp2!');
    });

    it('should keep change detection isolated for separately bootstrapped apps', async () => {
      const appRef1 = await bootstrapApplication(SimpleComp, {
        providers: [provideZoneChangeDetection()],
      });
      const appRef2 = await bootstrapApplication(SimpleComp2, {
        providers: [provideZoneChangeDetection()],
      });

      expect(el.innerText).toBe('Hello from SimpleComp!');
      expect(el2.innerText).toBe('Hello from SimpleComp2!');

      // Update name in both components, but trigger change detection only in the first one.
      appRef1.components[0].instance.name = 'Updated SimpleComp';
      appRef2.components[0].instance.name = 'Updated SimpleComp2';

      // Trigger change detection for the first app.
      appRef1.tick();

      // Expect that the first component content is updated, but the second one remains the same.
      expect(el.innerText).toBe('Hello from Updated SimpleComp!');
      expect(el2.innerText).toBe('Hello from SimpleComp2!');

      // Trigger change detection for the second app.
      appRef2.tick();

      // Now the second component should be updated as well.
      expect(el.innerText).toBe('Hello from Updated SimpleComp!');
      expect(el2.innerText).toBe('Hello from Updated SimpleComp2!');
    });

    it('should allow bootstrapping multiple standalone components within the same app', async () => {
      const appRef = await bootstrapApplication(SimpleComp, {
        providers: [provideZoneChangeDetection()],
      });
      appRef.bootstrap(SimpleComp2);

      expect(el.innerText).toBe('Hello from SimpleComp!');
      expect(el2.innerText).toBe('Hello from SimpleComp2!');

      // Update name in both components.
      appRef.components[0].instance.name = 'Updated SimpleComp';
      appRef.components[1].instance.name = 'Updated SimpleComp2';

      // Run change detection for the app.
      appRef.tick();

      // Expect both components to be updated, since they belong to the same app.
      expect(el.innerText).toBe('Hello from Updated SimpleComp!');
      expect(el2.innerText).toBe('Hello from Updated SimpleComp2!');
    });

    it('should allow bootstrapping non-standalone components within the same app', async () => {
      const appRef = await bootstrapApplication(SimpleComp, {
        providers: [provideZoneChangeDetection()],
      });

      // ApplicationRef should still allow bootstrapping non-standalone
      // components into the same application.
      appRef.bootstrap(NonStandaloneComp);

      expect(el.innerText).toBe('Hello from SimpleComp!');
      expect(el2.innerText).toBe('Hello from NonStandaloneComp!');

      // Update name in both components.
      appRef.components[0].instance.name = 'Updated SimpleComp';
      appRef.components[1].instance.name = 'Updated NonStandaloneComp';

      // Run change detection for the app.
      appRef.tick();

      // Expect both components to be updated, since they belong to the same app.
      expect(el.innerText).toBe('Hello from Updated SimpleComp!');
      expect(el2.innerText).toBe('Hello from Updated NonStandaloneComp!');
    });

    it('should throw when trying to bootstrap a non-standalone component', async () => {
      const msg =
        'NG0907: The NonStandaloneComp component is not marked as standalone, ' +
        'but Angular expects to have a standalone component here. Please make sure the ' +
        'NonStandaloneComp component does not have the `standalone: false` flag in the decorator.';
      let bootstrapError: string | null = null;

      try {
        await bootstrapApplication(NonStandaloneComp);
      } catch (e) {
        bootstrapError = (e as Error).message;
      }

      expect(bootstrapError).toBe(msg);
    });

    it('should throw when trying to bootstrap a standalone directive', async () => {
      @Directive({
        selector: '[dir]',
      })
      class StandaloneDirective {}

      const msg = //
        'NG0906: The StandaloneDirective is not an Angular component, ' +
        'make sure it has the `@Component` decorator.';
      let bootstrapError: string | null = null;

      try {
        await bootstrapApplication(StandaloneDirective);
      } catch (e) {
        bootstrapError = (e as Error).message;
      }

      expect(bootstrapError).toBe(msg);
    });

    it('should throw when trying to bootstrap a non-annotated class', async () => {
      class NonAnnotatedClass {}
      const msg = //
        'NG0906: The NonAnnotatedClass is not an Angular component, ' +
        'make sure it has the `@Component` decorator.';
      let bootstrapError: string | null = null;

      try {
        await bootstrapApplication(NonAnnotatedClass);
      } catch (e) {
        bootstrapError = (e as Error).message;
      }

      expect(bootstrapError).toBe(msg);
    });

    it('should have the TransferState token available', async () => {
      let state: TransferState | undefined;
      @Component({
        selector: 'hello-app',
        template: '...',
      })
      class StandaloneComponent {
        constructor() {
          state = _inject(TransferState);
        }
      }

      await bootstrapApplication(StandaloneComponent);
      expect(state).toBeInstanceOf(TransferState);
    });

    it('should reject the bootstrapApplication promise if an imported module throws', (done) => {
      @NgModule()
      class ErrorModule {
        constructor() {
          throw new Error('This error should be in the promise rejection');
        }
      }

      bootstrapApplication(SimpleComp, {
        providers: [importProvidersFrom(ErrorModule)],
      }).then(
        () => done.fail('Expected bootstrap promised to be rejected'),
        () => done(),
      );
    });

    describe('with animations', () => {
      @Component({
        selector: 'hello-app',
        template:
          '<div @myAnimation (@myAnimation.start)="onStart($event)">Hello from AnimationCmp!</div>',
        animations: [
          trigger('myAnimation', [transition('void => *', [style({opacity: 1}), animate(5)])]),
        ],
      })
      class AnimationCmp {
        renderer = _inject(ANIMATION_MODULE_TYPE, {optional: true}) ?? 'not found';
        startEvent?: {};

        onStart(event: {}) {
          this.startEvent = event;
        }
      }

      it('should enable animations when using provideAnimations()', async () => {
        const appRef = await bootstrapApplication(AnimationCmp, {
          providers: [provideAnimations()],
        });
        const cmp = appRef.components[0].instance;

        // Wait until animation is completed.
        await new Promise((resolve) => setTimeout(resolve, 10));

        expect(cmp.renderer).toBe('BrowserAnimations');
        expect(cmp.startEvent.triggerName).toEqual('myAnimation');
        expect(cmp.startEvent.phaseName).toEqual('start');

        expect(el.innerText).toBe('Hello from AnimationCmp!');
      });

      it('should use noop animations renderer when using provideNoopAnimations()', async () => {
        const appRef = await bootstrapApplication(AnimationCmp, {
          providers: [provideNoopAnimations()],
        });
        const cmp = appRef.components[0].instance;

        // Wait until animation is completed.
        await new Promise((resolve) => setTimeout(resolve, 10));

        expect(cmp.renderer).toBe('NoopAnimations');
        expect(cmp.startEvent.triggerName).toEqual('myAnimation');
        expect(cmp.startEvent.phaseName).toEqual('start');

        expect(el.innerText).toBe('Hello from AnimationCmp!');
      });
    });

    it('initializes modules inside the NgZone when using `provideZoneChangeDetection`', async () => {
      let moduleInitialized = false;
      @NgModule({})
      class SomeModule {
        constructor() {
          expect(NgZone.isInAngularZone()).toBe(true);
          moduleInitialized = true;
        }
      }
      @Component({
        template: '',
        selector: 'hello-app',
        imports: [SomeModule],
      })
      class AnimationCmp {}

      await bootstrapApplication(AnimationCmp, {
        providers: [provideZoneChangeDetection({eventCoalescing: true})],
      });
      expect(moduleInitialized).toBe(true);
    });
  });

  it('should throw if bootstrapped Directive is not a Component', (done) => {
    const logger = new MockConsole();
    const errorHandler = new ErrorHandler();
    (errorHandler as any)._console = logger as any;
    bootstrap(HelloRootDirectiveIsNotCmp, [{provide: ErrorHandler, useValue: errorHandler}]).catch(
      (error: Error) => {
        expect(error).toEqual(
          new Error(`HelloRootDirectiveIsNotCmp cannot be used as an entry component.`),
        );
        done();
      },
    );
  });

  it('should have the TransferState token available in NgModule bootstrap', async () => {
    let state: TransferState | undefined;
    @Component({
      selector: 'hello-app',
      template: '...',
      standalone: false,
    })
    class NonStandaloneComponent {
      constructor() {
        state = _inject(TransferState);
      }
    }

    await bootstrap(NonStandaloneComponent);
    expect(state).toBeInstanceOf(TransferState);
  });

  it('should retrieve sanitizer', inject([Injector], (injector: Injector) => {
    const sanitizer: Sanitizer | null = injector.get(Sanitizer, null);
    // We don't want to have sanitizer in DI. We use DI only to overwrite the
    // sanitizer, but not for default one. The default one is pulled in by the Ivy
    // instructions as needed.
    expect(sanitizer).toBe(null);
  }));

  it('should throw if no element is found', (done) => {
    const logger = new MockConsole();
    const errorHandler = new ErrorHandler();
    (errorHandler as any)._console = logger as any;
    bootstrap(NonExistentComp, [{provide: ErrorHandler, useValue: errorHandler}]).then(
      null,
      (reason) => {
        expect(reason.message).toContain('The selector "non-existent" did not match any elements');
        done();
        return null;
      },
    );
  });

  it('should throw if no provider', async () => {
    const logger = new MockConsole();
    const errorHandler = new ErrorHandler();
    (errorHandler as any)._console = logger as any;

    class IDontExist {}

    @Component({
      selector: 'cmp',
      template: 'Cmp',
      standalone: false,
    })
    class CustomCmp {
      constructor(iDontExist: IDontExist) {}
    }

    @Component({
      selector: 'hello-app',
      template: '<cmp></cmp>',
      standalone: false,
    })
    class RootCmp {}

    @NgModule({declarations: [CustomCmp], exports: [CustomCmp]})
    class CustomModule {}

    await expectAsync(
      bootstrap(RootCmp, [{provide: ErrorHandler, useValue: errorHandler}], [], [CustomModule]),
    ).toBeRejected();
  });

  if (getDOM().supportsDOMEvents) {
    it('should forward the error to promise when bootstrap fails', (done) => {
      const logger = new MockConsole();
      const errorHandler = new ErrorHandler();
      (errorHandler as any)._console = logger as any;

      const refPromise = bootstrap(NonExistentComp, [
        {provide: ErrorHandler, useValue: errorHandler},
      ]);
      refPromise.then(null, (reason: any) => {
        expect(reason.message).toContain('The selector "non-existent" did not match any elements');
        done();
      });
    });

    it('should invoke the default exception handler when bootstrap fails', (done) => {
      const logger = new MockConsole();
      const errorHandler = new ErrorHandler();
      (errorHandler as any)._console = logger as any;

      const refPromise = bootstrap(NonExistentComp, [
        {provide: ErrorHandler, useValue: errorHandler},
      ]);
      refPromise.then(null, (reason) => {
        expect(logger.res[0].join('#')).toContain(
          'ERROR#Error: NG05104: The selector "non-existent" did not match any elements',
        );
        done();
        return null;
      });
    });
  }

  it('should create an injector promise', async () => {
    const refPromise = bootstrap(HelloRootCmp, testProviders);
    expect(refPromise).toEqual(jasmine.any(Promise));
    await refPromise; // complete component initialization before switching to the next test
  });

  it('should set platform name to browser', (done) => {
    const refPromise = bootstrap(HelloRootCmp, testProviders);
    refPromise.then((ref) => {
      expect(isPlatformBrowser(ref.injector.get(PLATFORM_ID))).toBe(true);
      done();
    }, done.fail);
  });

  it('should display hello world', (done) => {
    const refPromise = bootstrap(HelloRootCmp, testProviders);
    refPromise.then((ref) => {
      expect(el).toHaveText('hello world!');
      expect(el.getAttribute('ng-version')).toEqual(VERSION.full);
      done();
    }, done.fail);
  });

  it('should throw a descriptive error if BrowserModule is installed again via a lazily loaded module', (done) => {
    @NgModule({imports: [BrowserModule]})
    class AsyncModule {}
    bootstrap(HelloRootCmp, testProviders)
      .then((ref: ComponentRef<HelloRootCmp>) => {
        const compiler: Compiler = ref.injector.get(Compiler);
        return compiler.compileModuleAsync(AsyncModule).then((factory) => {
          expect(() => factory.create(ref.injector)).toThrowError(
            'NG05100: Providers from the `BrowserModule` have already been loaded. ' +
              'If you need access to common directives such as NgIf and NgFor, ' +
              'import the `CommonModule` instead.',
          );
        });
      })
      .then(
        () => done(),
        (err) => done.fail(err),
      );
  });

  it('should support multiple calls to bootstrap', (done) => {
    const refPromise1 = bootstrap(HelloRootCmp, testProviders);
    const refPromise2 = bootstrap(HelloRootCmp2, testProviders);
    Promise.all([refPromise1, refPromise2]).then((refs) => {
      expect(el).toHaveText('hello world!');
      expect(el2).toHaveText('hello world, again!');
      done();
    }, done.fail);
  });

  it('should not crash if change detection is invoked when the root component is disposed', (done) => {
    bootstrap(HelloOnDestroyTickCmp, testProviders).then((ref) => {
      expect(() => ref.destroy()).not.toThrow();
      done();
    });
  });

  it('should unregister change detectors when components are disposed', (done) => {
    bootstrap(HelloRootCmp, testProviders).then((ref) => {
      const appRef = ref.injector.get(ApplicationRef);
      ref.destroy();
      expect(() => appRef.tick()).not.toThrow();
      done();
    }, done.fail);
  });

  it('should make the provided bindings available to the application component', (done) => {
    const refPromise = bootstrap(HelloRootCmp3, [
      testProviders,
      {provide: 'appBinding', useValue: 'BoundValue'},
    ]);

    refPromise.then((ref) => {
      expect(ref.injector.get('appBinding')).toEqual('BoundValue');
      done();
    }, done.fail);
  });

  it('should not override locale provided during bootstrap', (done) => {
    const refPromise = bootstrap(
      HelloRootCmp,
      [testProviders],
      [{provide: LOCALE_ID, useValue: 'fr-FR'}],
    );

    refPromise.then((ref) => {
      expect(ref.injector.get(LOCALE_ID)).toEqual('fr-FR');
      done();
    }, done.fail);
  });

  it('should avoid cyclic dependencies when root component requires Lifecycle through DI', (done) => {
    const refPromise = bootstrap(HelloRootCmp4, testProviders);

    refPromise.then((ref) => {
      const appRef = ref.injector.get(ApplicationRef);
      expect(appRef).toBeDefined();
      done();
    }, done.fail);
  });

  it('should run platform initializers', (done) => {
    inject([Log], (log: Log) => {
      const p = createPlatformFactory(platformBrowser, 'someName', [
        {provide: PLATFORM_INITIALIZER, useValue: log.fn('platform_init1'), multi: true},
        {provide: PLATFORM_INITIALIZER, useValue: log.fn('platform_init2'), multi: true},
      ])();

      @NgModule({
        imports: [BrowserModule],
        providers: [
          {provide: APP_INITIALIZER, useValue: log.fn('app_init1'), multi: true},
          {provide: APP_INITIALIZER, useValue: log.fn('app_init2'), multi: true},
        ],
      })
      class SomeModule {
        ngDoBootstrap() {}
      }

      expect(log.result()).toEqual('platform_init1; platform_init2');
      log.clear();
      p.bootstrapModule(SomeModule).then(() => {
        expect(log.result()).toEqual('app_init1; app_init2');
        done();
      }, done.fail);
    })();
  });

  it('should allow provideZoneChangeDetection in bootstrapModule', async () => {
    @NgModule({imports: [BrowserModule], providers: [provideZoneChangeDetection()]})
    class SomeModule {
      ngDoBootstrap() {}
    }

    await expectAsync(platformBrowser().bootstrapModule(SomeModule)).toBeResolved();
  });

  it('should register each application with the testability registry', async () => {
    const ngModuleRef1: NgModuleRef<unknown> = await bootstrap(HelloRootCmp, testProviders);
    const ngModuleRef2: NgModuleRef<unknown> = await bootstrap(HelloRootCmp2, testProviders);

    // The `TestabilityRegistry` is provided in the "platform", so the same instance is available
    // to both `NgModuleRef`s and it can be retrieved from any ref (we use the first one).
    const registry = ngModuleRef1.injector.get(TestabilityRegistry);

    expect(registry.findTestabilityInTree(el)).toEqual(ngModuleRef1.injector.get(Testability));
    expect(registry.findTestabilityInTree(el2)).toEqual(ngModuleRef2.injector.get(Testability));
  });

  it('should allow to pass schemas', (done) => {
    bootstrap(HelloCmpUsingCustomElement, testProviders).then(() => {
      expect(el).toHaveText('hello world!');
      done();
    }, done.fail);
  });

  it('should throw an error if the provided APP_ID is invalid', (done) => {
    const logger = new MockConsole();
    const errorHandler = new ErrorHandler();
    (errorHandler as any)._console = logger as any;

    const refPromise = bootstrap(HelloRootCmp, [{provide: APP_ID, useValue: 'foo:bar'}]);
    refPromise.then(
      () => fail(),
      (reason) => {
        expect(reason.message).toContain(
          `NG0211: APP_ID value "foo:bar" is not alphanumeric. The APP_ID must be a string of alphanumeric characters.`,
        );
        done();
        return null;
      },
    );
  });

  describe('change detection', () => {
    const log: string[] = [];

    @Component({
      selector: 'hello-app',
      template: '<div id="button-a" (click)="onClick()">{{title}}</div>',
      standalone: false,
    })
    class CompA {
      title: string = '';

      ngDoCheck() {
        log.push('CompA:ngDoCheck');
      }

      onClick() {
        this.title = 'CompA';
        log.push('CompA:onClick');
      }
    }

    @Component({
      selector: 'hello-app-2',
      template: '<div id="button-b" (click)="onClick()">{{title}}</div>',
      standalone: false,
    })
    class CompB {
      title: string = '';

      ngDoCheck() {
        log.push('CompB:ngDoCheck');
      }

      onClick() {
        this.title = 'CompB';
        log.push('CompB:onClick');
      }
    }

    it('should be triggered for all bootstrapped components in case change happens in one of them', (done) => {
      @NgModule({
        imports: [BrowserModule],
        declarations: [CompA, CompB],
        bootstrap: [CompA, CompB],
        schemas: [CUSTOM_ELEMENTS_SCHEMA],
        providers: [provideZoneChangeDetection()],
      })
      class TestModuleA {}
      platformBrowser()
        .bootstrapModule(TestModuleA)
        .then((ref) => {
          log.length = 0;
          el.querySelectorAll<HTMLElement>('#button-a')[0].click();
          expect(log).toContain('CompA:onClick');
          expect(log).toContain('CompA:ngDoCheck');
          expect(log).toContain('CompB:ngDoCheck');

          log.length = 0;
          el2.querySelectorAll<HTMLElement>('#button-b')[0].click();
          expect(log).toContain('CompB:onClick');
          expect(log).toContain('CompA:ngDoCheck');
          expect(log).toContain('CompB:ngDoCheck');

          done();
        }, done.fail);
    });

    it('should work in isolation for each component bootstrapped individually', (done) => {
      const refPromise1 = bootstrap(CompA);
      const refPromise2 = bootstrap(CompB);
      Promise.all([refPromise1, refPromise2]).then((refs) => {
        log.length = 0;
        el.querySelectorAll<HTMLElement>('#button-a')[0].click();
        expect(log).toContain('CompA:onClick');
        expect(log).toContain('CompA:ngDoCheck');
        expect(log).not.toContain('CompB:ngDoCheck');

        log.length = 0;
        el2.querySelectorAll<HTMLElement>('#button-b')[0].click();
        expect(log).toContain('CompB:onClick');
        expect(log).toContain('CompB:ngDoCheck');
        expect(log).not.toContain('CompA:ngDoCheck');

        done();
      }, done.fail);
    });
  });
});

describe('providePlatformInitializer', () => {
  beforeEach(() => destroyPlatform());
  afterEach(() => destroyPlatform());

  it('should call the provided function when platform is initialized', () => {
    let initialized = false;

    createPlatformInjector([providePlatformInitializer(() => (initialized = true))]);

    expect(initialized).toBe(true);
  });

  it('should be able to inject dependencies', () => {
    const TEST_TOKEN = new InjectionToken<string>('TEST_TOKEN');
    let injectedValue!: string;

    createPlatformInjector([
      {provide: TEST_TOKEN, useValue: 'test'},
      providePlatformInitializer(() => (injectedValue = _inject(TEST_TOKEN))),
    ]);

    expect(injectedValue).toBe('test');
  });

  function createPlatformInjector(providers: Array<StaticProvider>) {
    return createOrReusePlatformInjector(providers);
  }

  it('should bootstrap with platform initializers', async () => {
    return withBody('<app></app>', async () => {
      @Component({
        selector: 'app',
        template: '',
      })
      class App {}

      let platformInitializerCalls = 0;

      const platformRef = platformBrowser([
        providePlatformInitializer(() => {
          platformInitializerCalls++;
        }),
      ]);

      expect(platformInitializerCalls).toBe(0);
      await bootstrapApplication(App, undefined, {platformRef});
      expect(platformInitializerCalls).toBe(1);
      await bootstrapApplication(App, undefined, {platformRef});
      expect(platformInitializerCalls).toBe(1);
    });
  });
});
