/**
 * @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 ts from 'typescript';

import {ErrorCode, ngErrorCode} from '../../src/ngtsc/diagnostics';
import {runInEachFileSystem} from '../../src/ngtsc/file_system/testing';
import {loadStandardTestFiles} from '../../src/ngtsc/testing';

import {NgtscTestEnvironment, TsConfigOptions} from './env';

const testFiles = loadStandardTestFiles();

runInEachFileSystem(() => {
  describe('local compilation', () => {
    let env!: NgtscTestEnvironment;

    function tsconfig(extraOpts: TsConfigOptions = {}) {
      const tsconfig: {[key: string]: any} = {
        extends: '../tsconfig-base.json',
        angularCompilerOptions: {
          compilationMode: 'experimental-local',
          ...extraOpts,
        },
      };
      env.write('tsconfig.json', JSON.stringify(tsconfig, null, 2));
    }

    beforeEach(() => {
      env = NgtscTestEnvironment.setup(testFiles);
      tsconfig();
    });

    it('should produce no TS semantic diagnostics', () => {
      env.write(
        'test.ts',
        `
        import {SubExternalStuff} from './some-where';
        `,
      );

      env.driveMain();
      const diags = env.driveDiagnostics();

      expect(diags).toEqual([]);
    });

    describe('extra imports generation', () => {
      beforeEach(() => {
        const tsconfig: {[key: string]: any} = {
          extends: '../tsconfig-base.json',
          angularCompilerOptions: {
            compilationMode: 'experimental-local',
            generateExtraImportsInLocalMode: true,
          },
        };
        env.write('tsconfig.json', JSON.stringify(tsconfig, null, 2));
      });

      it('should only include NgModule external import as global import', () => {
        env.write(
          'comp1.ts',
          `
        import {Component} from '@angular/core';

        @Component({template:''})
        export class Comp1 {
        }
        `,
        );
        env.write(
          'module1.ts',
          `
        import {NgModule} from '@angular/core';

        import {Comp1} from './comp1';

        @NgModule({declarations:[Comp1]})
        export class Module1 {
        }
        `,
        );
        env.write(
          'a.ts',
          `
        import {NgModule} from '@angular/core';
        import {SomeExternalStuff} from '/some_external_file';
        import {SomeExternalStuff2} from '/some_external_file2';

        import {BModule} from './b';

        @NgModule({imports: [SomeExternalStuff, BModule]})
        export class AModule {
        }
        `,
        );
        env.write(
          'b.ts',
          `
        import {NgModule} from '@angular/core';

        @NgModule({})
        export class BModule {
        }
        `,
        );

        env.driveMain();
        const Comp1Contents = env.getContents('comp1.js');

        expect(Comp1Contents)
          .withContext('NgModule external imports should be included in the global imports')
          .toContain('import "/some_external_file"');
        expect(Comp1Contents)
          .withContext(
            'External import which is not an NgModule import should not be included in the global import',
          )
          .not.toContain('import "/some_external_file2"');
        expect(Comp1Contents)
          .withContext('NgModule internal import should not be included in the global import')
          .not.toContain('import "./b"');
      });

      it('should include global imports only in the eligible files', () => {
        env.write(
          'module_and_comp.ts',
          `
        import {NgModule, Component} from '@angular/core';

        @Component({template: ''})
        export class Comp3 {
        }

        @NgModule({declarations:[Comp3]})
        export class Module3 {
        }
        `,
        );
        env.write(
          'standalone_comp.ts',
          `
        import {Component} from '@angular/core';

        @Component({template: ''})
        export class Comp2 {
        }
        `,
        );
        env.write(
          'comp1.ts',
          `
        import {Component} from '@angular/core';

        @Component({template:''})
        export class Comp1 {
        }
        `,
        );
        env.write(
          'module1.ts',
          `
        import {NgModule} from '@angular/core';

        import {Comp1} from './comp1';

        @NgModule({declarations:[Comp1]})
        export class Module1 {
        }
        `,
        );
        env.write(
          'a.ts',
          `
        import {NgModule} from '@angular/core';
        import {SomeExternalStuff} from '/some_external_file';
        import {SomeExternalStuff2} from '/some_external_file2';

        import {BModule} from './b';

        @NgModule({imports: [SomeExternalStuff, BModule]})
        export class AModule {
        }
        `,
        );
        env.write(
          'b.ts',
          `
        import {NgModule} from '@angular/core';

        @NgModule({})
        export class BModule {
        }
        `,
        );
        env.write(
          'c.ts',
          `
        // Some code
        `,
        );

        env.driveMain();

        expect(env.getContents('comp1.js'))
          .withContext(
            'Global imports should be generated when a component has its NgModule in a different file',
          )
          .toContain('import "/some_external_file"');
        expect(env.getContents('standalone_comp.js'))
          .withContext('Global imports should not be generated when all components are standalone')
          .not.toContain('import "/some_external_file"');
        expect(env.getContents('module_and_comp.js'))
          .withContext(
            'Global imports should not be generated when components and their NgModules are in the same file',
          )
          .not.toContain('import "/some_external_file"');
        expect(env.getContents('a.js'))
          .withContext('Global imports should not be generated when the file has no component')
          .not.toContain('import "/some_external_file"');
        expect(env.getContents('c.js'))
          .withContext('Global imports should not be generated for non-Angular files')
          .not.toContain('import "/some_external_file"');
      });

      it('should include NgModule namespace external import as global import', () => {
        env.write(
          'comp1.ts',
          `
        import {Component} from '@angular/core';

        @Component({template:''})
        export class Comp1 {
        }
        `,
        );
        env.write(
          'module1.ts',
          `
        import {NgModule} from '@angular/core';

        import {Comp1} from './comp1';

        @NgModule({declarations:[Comp1]})
        export class Module1 {
        }
        `,
        );
        env.write(
          'a.ts',
          `
        import {NgModule} from '@angular/core';
        import * as n from '/some_external_file';

        import {BModule} from './b';

        @NgModule({imports: [n.SomeExternalStuff]})
        export class AModule {
        }
        `,
        );
        env.write(
          'test.ts',
          `
        // Some code
        `,
        );

        env.driveMain();

        expect(env.getContents('comp1.js')).toContain('import "/some_external_file"');
      });

      it('should include nested NgModule external import as global import - case of named import', () => {
        env.write(
          'comp1.ts',
          `
        import {Component} from '@angular/core';

        @Component({template:''})
        export class Comp1 {
        }
        `,
        );
        env.write(
          'module1.ts',
          `
        import {NgModule} from '@angular/core';

        import {Comp1} from './comp1';

        @NgModule({declarations:[Comp1]})
        export class Module1 {
        }
        `,
        );
        env.write(
          'a.ts',
          `
        import {NgModule} from '@angular/core';
        import {SomeExternalStuff} from '/some_external_file';

        import {BModule} from './b';

        @NgModule({imports: [[[SomeExternalStuff]]]})
        export class AModule {
        }
        `,
        );

        env.driveMain();

        expect(env.getContents('comp1.js')).toContain('import "/some_external_file"');
      });

      it('should include nested NgModule external import as global import - case of namespace import', () => {
        env.write(
          'comp1.ts',
          `
        import {Component} from '@angular/core';

        @Component({template:''})
        export class Comp1 {
        }
        `,
        );
        env.write(
          'module1.ts',
          `
        import {NgModule} from '@angular/core';

        import {Comp1} from './comp1';

        @NgModule({declarations:[Comp1]})
        export class Module1 {
        }
        `,
        );
        env.write(
          'a.ts',
          `
        import {NgModule} from '@angular/core';
        import * as n from '/some_external_file';

        import {BModule} from './b';

        @NgModule({imports: [[[n.SomeExternalStuff]]]})
        export class AModule {
        }
        `,
        );

        env.driveMain();

        expect(env.getContents('comp1.js')).toContain('import "/some_external_file"');
      });

      it('should include NgModule external imports as global imports - case of multiple nested imports including named and namespace imports', () => {
        env.write(
          'comp1.ts',
          `
        import {Component} from '@angular/core';

        @Component({template:''})
        export class Comp1 {
        }
        `,
        );
        env.write(
          'module1.ts',
          `
        import {NgModule} from '@angular/core';

        import {Comp1} from './comp1';

        @NgModule({declarations:[Comp1]})
        export class Module1 {
        }
        `,
        );
        env.write(
          'a.ts',
          `
        import {NgModule} from '@angular/core';
        import {SomeExternalStuff} from '/some_external_file';
        import * as n from '/some_external_file2';

        import {BModule} from './b';

        @NgModule({imports: [[SomeExternalStuff], [n.SomeExternalStuff]]})
        export class AModule {
        }
        `,
        );

        env.driveMain();

        expect(env.getContents('comp1.js')).toContain('import "/some_external_file"');
        expect(env.getContents('comp1.js')).toContain('import "/some_external_file2"');
      });

      it('should include extra import for the local component dependencies (component, directive and pipe)', () => {
        env.write(
          'internal_comp.ts',
          `
        import {Component} from '@angular/core';

        @Component({
          template: '...',
          selector: 'internal-comp',
          standalone: false,
        })
        export class InternalComp {
        }
        `,
        );
        env.write(
          'internal_dir.ts',
          `
        import {Directive} from '@angular/core';

        @Directive({
          selector: '[internal-dir]',
          standalone: false,
        })
        export class InternalDir {
        }
        `,
        );
        env.write(
          'internal_pipe.ts',
          `
        import {Pipe, PipeTransform} from '@angular/core';

        @Pipe({
          name: 'internalPipe',
          standalone: false,
        })
        export class InternalPipe implements PipeTransform {
          transform(value: number): number {
            return value*2;
          }
        }
        `,
        );
        env.write(
          'internal_module.ts',
          `
        import {NgModule} from '@angular/core';

        import {InternalComp} from './internal_comp';
        import {InternalDir} from './internal_dir';
        import {InternalPipe} from './internal_pipe';

        @NgModule({declarations: [InternalComp, InternalDir, InternalPipe], exports: [InternalComp, InternalDir, InternalPipe]})
        export class InternalModule {
        }
        `,
        );
        env.write(
          'main_comp.ts',
          `
        import {Component} from '@angular/core';

        @Component({template: '<internal-comp></internal-comp> <span internal-dir></span> <span>{{2 | internalPipe}}</span>'})
        export class MainComp {
        }
        `,
        );
        env.write(
          'main_module.ts',
          `
        import {NgModule} from '@angular/core';

        import {MainComp} from './main_comp';
        import {InternalModule} from './internal_module';

        @NgModule({declarations: [MainComp], imports: [InternalModule]})
        export class MainModule {
        }
        `,
        );

        env.driveMain();

        expect(env.getContents('main_comp.js')).toContain('import "./internal_comp"');
        expect(env.getContents('main_comp.js')).toContain('import "./internal_dir"');
        expect(env.getContents('main_comp.js')).toContain('import "./internal_pipe"');
      });

      it('should not include extra import and remote scope runtime for the local component dependencies when cycle is produced', () => {
        env.write(
          'internal_comp.ts',
          `
        import {Component} from '@angular/core';
        import {cycleCreatingDep} from './main_comp';

        @Component({template: '...', selector: 'internal-comp'})
        export class InternalComp {
        }
        `,
        );
        env.write(
          'internal_module.ts',
          `
        import {NgModule} from '@angular/core';

        import {InternalComp} from 'internal_comp';

        @NgModule({declarations: [InternalComp], exports: [InternalComp]})
        export class InternalModule {
        }
        `,
        );
        env.write(
          'main_comp.ts',
          `
        import {Component} from '@angular/core';

        @Component({template: '<internal-comp></internal-comp>'})
        export class MainComp {
        }
        `,
        );
        env.write(
          'main_module.ts',
          `
        import {NgModule} from '@angular/core';

        import {MainComp} from 'main_comp';
        import {InternalModule} from 'internal_module';

        @NgModule({declarations: [MainComp], imports: [InternalModule]})
        export class MainModule {
        }
        `,
        );

        env.driveMain();

        expect(env.getContents('main_comp.js')).not.toContain('import "internal_comp"');
        expect(env.getContents('main_module.js')).not.toContain('ɵɵsetComponentScope');
      });
    });

    describe('ng module injector def', () => {
      it('should produce empty injector def imports when module has no imports/exports', () => {
        env.write(
          'test.ts',
          `
        import {NgModule} from '@angular/core';

        @NgModule({})
        export class MainModule {
        }
        `,
        );

        env.driveMain();
        const jsContents = env.getContents('test.js');

        expect(jsContents).toContain('MainModule.ɵinj = /*@__PURE__*/ i0.ɵɵdefineInjector({})');
      });

      it('should include raw module imports array elements (including forward refs) in the injector def imports', () => {
        env.write(
          'test.ts',
          `
        import {NgModule, forwardRef} from '@angular/core';
        import {SubModule1} from './some-where';
        import {SubModule2} from './another-where';

        @NgModule({})
        class LocalModule1 {}

        @NgModule({
          imports: [SubModule1, forwardRef(() => SubModule2), LocalModule1, forwardRef(() => LocalModule2)],
        })
        export class MainModule {
        }

        @NgModule({})
        class LocalModule2 {}
        `,
        );

        env.driveMain();
        const jsContents = env.getContents('test.js');

        expect(jsContents).toContain(
          'MainModule.ɵinj = /*@__PURE__*/ i0.ɵɵdefineInjector({ imports: [SubModule1, forwardRef(() => SubModule2), LocalModule1, forwardRef(() => LocalModule2)] })',
        );
      });

      it('should include non-array raw module imports as it is in the injector def imports', () => {
        env.write(
          'test.ts',
          `
        import {NgModule, forwardRef} from '@angular/core';
        import {SubModule1} from './some-where';
        import {SubModule2} from './another-where';

        const NG_IMPORTS = [SubModule1, forwardRef(() => SubModule2), LocalModule1, forwardRef(() => LocalModule2)];

        @NgModule({})
        class LocalModule1 {}

        @NgModule({
          imports: NG_IMPORTS,
        })
        export class MainModule {
        }

        @NgModule({})
        class LocalModule2 {}
        `,
        );

        env.driveMain();
        const jsContents = env.getContents('test.js');

        expect(jsContents).toContain(
          'MainModule.ɵinj = /*@__PURE__*/ i0.ɵɵdefineInjector({ imports: [NG_IMPORTS] })',
        );
      });

      it('should include raw module exports array elements (including forward refs) in the injector def imports', () => {
        env.write(
          'test.ts',
          `
        import {NgModule, forwardRef} from '@angular/core';
        import {SubModule1} from './some-where';
        import {SubModule2} from './another-where';

        @NgModule({})
        class LocalModule1 {}

        @NgModule({
          exports: [SubModule1, forwardRef(() => SubModule2), LocalModule1, forwardRef(() => LocalModule2)],
        })
        export class MainModule {
        }

        @NgModule({})
        class LocalModule2 {}
        `,
        );

        env.driveMain();
        const jsContents = env.getContents('test.js');

        expect(jsContents).toContain(
          'MainModule.ɵinj = /*@__PURE__*/ i0.ɵɵdefineInjector({ imports: [SubModule1, forwardRef(() => SubModule2), LocalModule1, forwardRef(() => LocalModule2)] })',
        );
      });

      it('should include non-array raw module exports (including forward refs) in the injector def imports', () => {
        env.write(
          'test.ts',
          `
        import {NgModule, forwardRef} from '@angular/core';
        import {SubModule1} from './some-where';
        import {SubModule2} from './another-where';

        const NG_EXPORTS = [SubModule1, forwardRef(() => SubModule2), LocalModule1, forwardRef(() => LocalModule2)];

        @NgModule({})
        class LocalModule1 {}

        @NgModule({
          exports: NG_EXPORTS,
        })
        export class MainModule {
        }

        @NgModule({})
        class LocalModule2 {}
        `,
        );

        env.driveMain();
        const jsContents = env.getContents('test.js');

        expect(jsContents).toContain(
          'MainModule.ɵinj = /*@__PURE__*/ i0.ɵɵdefineInjector({ imports: [NG_EXPORTS] })',
        );
      });

      it('should concat raw module imports and exports arrays (including forward refs) in the injector def imports', () => {
        env.write(
          'test.ts',
          `
        import {NgModule, forwardRef} from '@angular/core';
        import {SubModule1, SubModule2} from './some-where';
        import {SubModule3, SubModule4} from './another-where';

        @NgModule({
          imports: [SubModule1, forwardRef(() => SubModule2)],
          exports: [SubModule3, forwardRef(() => SubModule4)],
        })
        export class MainModule {
        }
        `,
        );

        env.driveMain();
        const jsContents = env.getContents('test.js');

        expect(jsContents).toContain(
          'MainModule.ɵinj = /*@__PURE__*/ i0.ɵɵdefineInjector({ imports: [SubModule1, forwardRef(() => SubModule2), SubModule3, forwardRef(() => SubModule4)] })',
        );
      });

      it('should combines non-array raw module imports and exports (including forward refs) in the injector def imports', () => {
        env.write(
          'test.ts',
          `
        import {NgModule, forwardRef} from '@angular/core';
        import {NG_IMPORTS} from './some-where';
        import {NG_EXPORTS} from './another-where';

        @NgModule({
          imports: NG_IMPORTS,
          exports: NG_EXPORTS,
        })
        export class MainModule {
        }
        `,
        );

        env.driveMain();
        const jsContents = env.getContents('test.js');

        expect(jsContents).toContain(
          'MainModule.ɵinj = /*@__PURE__*/ i0.ɵɵdefineInjector({ imports: [NG_IMPORTS, NG_EXPORTS] })',
        );
      });
    });

    describe('component dependencies', () => {
      it('should generate ɵɵgetComponentDepsFactory for component def dependencies - for non-standalone component ', () => {
        env.write(
          'test.ts',
          `
          import {NgModule, Component} from '@angular/core';

          @Component({
            selector: 'test-main',
            template: '<span>Hello world!</span>',
            standalone: false,
          })
          export class MainComponent {
          }

          @NgModule({
            declarations: [MainComponent],
          })
          export class MainModule {
          }
          `,
        );

        env.driveMain();
        const jsContents = env.getContents('test.js');

        expect(jsContents).toContain('dependencies: i0.ɵɵgetComponentDepsFactory(MainComponent)');
      });

      it('should generate ɵɵgetComponentDepsFactory with raw imports as second param for component def dependencies - for standalone component with non-empty imports', () => {
        env.write(
          'test.ts',
          `
          import {Component, forwardRef} from '@angular/core';
          import {SomeThing} from 'some-where';
          import {SomeThing2} from 'some-where2';

          @Component({
            imports: [SomeThing, forwardRef(()=>SomeThing2)],
            selector: 'test-main',
            template: '<span>Hello world!</span>',
          })
          export class MainComponent {
          }
          `,
        );

        env.driveMain();
        const jsContents = env.getContents('test.js');

        expect(jsContents).toContain(
          'dependencies: i0.ɵɵgetComponentDepsFactory(MainComponent, [SomeThing, forwardRef(() => SomeThing2)])',
        );
      });

      it('should generate ɵɵgetComponentDepsFactory with raw non-array imports as second param for component def dependencies - for standalone component with non-empty imports', () => {
        env.write(
          'test.ts',
          `
          import {Component, forwardRef} from '@angular/core';
          import {SomeThing} from 'some-where';
          import {SomeThing2} from 'some-where2';

          const NG_IMPORTS = [SomeThing, forwardRef(()=>SomeThing2)];

          @Component({
            imports: NG_IMPORTS,
            selector: 'test-main',
            template: '<span>Hello world!</span>',
          })
          export class MainComponent {
          }
          `,
        );

        env.driveMain();
        const jsContents = env.getContents('test.js');

        expect(jsContents).toContain(
          'dependencies: i0.ɵɵgetComponentDepsFactory(MainComponent, NG_IMPORTS)',
        );
      });

      it('should generate ɵɵgetComponentDepsFactory with empty array as secon d arg for standalone component with empty imports', () => {
        env.write(
          'test.ts',
          `
      import {Component} from '@angular/core';

      @Component({
        imports: [],
        selector: 'test-main',
        template: '<span>Hello world!</span>',
      })
      export class MainComponent {
      }
      `,
        );

        env.driveMain();
        const jsContents = env.getContents('test.js');

        expect(jsContents).toContain(
          'dependencies: i0.ɵɵgetComponentDepsFactory(MainComponent, [])',
        );
      });

      it('should not generate ɵɵgetComponentDepsFactory for standalone component with no imports', () => {
        env.write(
          'test.ts',
          `
          import {Component} from '@angular/core';

          @Component({
            selector: 'test-main',
            template: '<span>Hello world!</span>',
          })
          export class MainComponent {
          }
          `,
        );

        env.driveMain();
        const jsContents = env.getContents('test.js');

        expect(jsContents).not.toContain('i0.ɵɵgetComponentDepsFactory');
      });
    });

    describe('component fields', () => {
      it('should place the changeDetection as it is into the component def', () => {
        env.write(
          'test.ts',
          `
          import {Component} from '@angular/core';
          import {SomeWeirdThing} from 'some-where';

          @Component({
            changeDetection: SomeWeirdThing,
            template: '<span>Hello world!</span>',
          })
          export class MainComponent {
          }
          `,
        );

        env.driveMain();
        const jsContents = env.getContents('test.js');

        expect(jsContents).toContain('changeDetection: SomeWeirdThing');
      });

      it('should place the correct value of encapsulation into the component def - case of ViewEncapsulation.Emulated with no styles', () => {
        env.write(
          'test.ts',
          `
          import {Component, ViewEncapsulation} from '@angular/core';

          @Component({
            encapsulation: ViewEncapsulation.Emulated,
            template: '<span>Hello world!</span>',
          })
          export class MainComponent {
          }
          `,
        );

        env.driveMain();
        const jsContents = env.getContents('test.js');

        // If there is no style, don't generate css selectors on elements by setting
        // encapsulation to none (=2)
        expect(jsContents).toContain('encapsulation: 2');
      });

      it('should place the correct value of encapsulation into the component def - case of ViewEncapsulation.Emulated with styles', () => {
        env.write(
          'test.ts',
          `
          import {Component, ViewEncapsulation} from '@angular/core';

          @Component({
            encapsulation: ViewEncapsulation.Emulated,
            styles: ['color: blue'],
            template: '<span>Hello world!</span>',
          })
          export class MainComponent {
          }
          `,
        );

        env.driveMain();
        const jsContents = env.getContents('test.js');

        // encapsulation is set only for non-default value
        expect(jsContents).not.toContain('encapsulation: 0');
        expect(jsContents).toContain('styles: ["color: blue"]');
      });

      it('should place the correct value of encapsulation into the component def - case of ViewEncapsulation.ShadowDom', () => {
        env.write(
          'test.ts',
          `
          import {Component, ViewEncapsulation} from '@angular/core';

          @Component({
            encapsulation: ViewEncapsulation.ShadowDom,
            template: '<span>Hello world!</span>',
          })
          export class MainComponent {
          }
          `,
        );

        env.driveMain();
        const jsContents = env.getContents('test.js');

        expect(jsContents).toContain('encapsulation: 3');
      });

      it('should place the correct value of encapsulation into the component def - case of ViewEncapsulation.None', () => {
        env.write(
          'test.ts',
          `
          import {Component, ViewEncapsulation} from '@angular/core';

          @Component({
            encapsulation: ViewEncapsulation.None,
            template: '<span>Hello world!</span>',
          })
          export class MainComponent {
          }
          `,
        );

        env.driveMain();
        const jsContents = env.getContents('test.js');

        expect(jsContents).toContain('encapsulation: 2');
      });

      it('should default encapsulation to Emulated', () => {
        env.write(
          'test.ts',
          `
          import {Component, ViewEncapsulation} from '@angular/core';

          @Component({
            template: '<span>Hello world!</span>',
          })
          export class MainComponent {
          }
          `,
        );

        env.driveMain();
        const jsContents = env.getContents('test.js');

        // If there is no style, don't generate css selectors on elements by setting
        // encapsulation to none (=2)
        expect(jsContents).toContain('encapsulation: 2');
      });
    });

    describe('constructor injection', () => {
      it('should include injector types with all possible import/injection styles into component factory', () => {
        env.write(
          'test.ts',
          `
          import {Component, NgModule, Attribute, Inject} from '@angular/core';
          import {SomeClass} from './some-where'
          import {SomeService1} from './some-where1'
          import SomeService2 from './some-where2'
          import * as SomeWhere3 from './some-where3'
          import * as SomeWhere4 from './some-where4'

          @Component({
            selector: 'test-main',
            template: '<span>Hello world</span>',
          })
          export class MainComponent {
            constructor(
              private someService1: SomeService1,
              private someService2: SomeService2,
              private someService3: SomeWhere3.SomeService3,
              private someService4: SomeWhere4.nested.SomeService4,
              @Attribute('title') title: string,
              @Inject(MESSAGE_TOKEN) tokenMessage: SomeClass,
              ) {}
          }

          @NgModule({
            declarations: [MainComponent],
          })
          export class MainModule {
          }
          `,
        );

        env.driveMain();
        const jsContents = env.getContents('test.js');

        expect(jsContents).toContain(
          `MainComponent.ɵfac = function MainComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || MainComponent)(i0.ɵɵdirectiveInject(i1.SomeService1), i0.ɵɵdirectiveInject(SomeService2), i0.ɵɵdirectiveInject(i2.SomeService3), i0.ɵɵdirectiveInject(i3.nested.SomeService4), i0.ɵɵinjectAttribute('title'), i0.ɵɵdirectiveInject(MESSAGE_TOKEN)); };`,
        );
      });

      it('should include injector types with all possible import/injection styles into standalone component factory', () => {
        env.write(
          'test.ts',
          `
          import {Component, NgModule, Attribute, Inject} from '@angular/core';
          import {SomeClass} from './some-where'
          import {SomeService1} from './some-where1'
          import SomeService2 from './some-where2'
          import * as SomeWhere3 from './some-where3'
          import * as SomeWhere4 from './some-where4'

          @Component({
            selector: 'test-main',
            template: '<span>Hello world</span>',
          })
          export class MainComponent {
            constructor(
              private someService1: SomeService1,
              private someService2: SomeService2,
              private someService3: SomeWhere3.SomeService3,
              private someService4: SomeWhere4.nested.SomeService4,
              @Attribute('title') title: string,
              @Inject(MESSAGE_TOKEN) tokenMessage: SomeClass,
              ) {}
          }
          `,
        );

        env.driveMain();
        const jsContents = env.getContents('test.js');

        expect(jsContents).toContain(
          `MainComponent.ɵfac = function MainComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || MainComponent)(i0.ɵɵdirectiveInject(i1.SomeService1), i0.ɵɵdirectiveInject(SomeService2), i0.ɵɵdirectiveInject(i2.SomeService3), i0.ɵɵdirectiveInject(i3.nested.SomeService4), i0.ɵɵinjectAttribute('title'), i0.ɵɵdirectiveInject(MESSAGE_TOKEN)); };`,
        );
      });

      it('should include injector types with all possible import/injection styles into directive factory', () => {
        env.write(
          'test.ts',
          `
          import {Directive, NgModule, Attribute, Inject} from '@angular/core';
          import {SomeClass} from './some-where'
          import {SomeService1} from './some-where1'
          import SomeService2 from './some-where2'
          import * as SomeWhere3 from './some-where3'
          import * as SomeWhere4 from './some-where4'

          @Directive({
          })
          export class MainDirective {
            constructor(
              private someService1: SomeService1,
              private someService2: SomeService2,
              private someService3: SomeWhere3.SomeService3,
              private someService4: SomeWhere4.nested.SomeService4,
              @Attribute('title') title: string,
              @Inject(MESSAGE_TOKEN) tokenMessage: SomeClass,
              ) {}
          }

          @NgModule({
            declarations: [MainDirective],
          })
          export class MainModule {
          }
          `,
        );

        env.driveMain();
        const jsContents = env.getContents('test.js');

        expect(jsContents).toContain(
          `MainDirective.ɵfac = function MainDirective_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || MainDirective)(i0.ɵɵdirectiveInject(i1.SomeService1), i0.ɵɵdirectiveInject(SomeService2), i0.ɵɵdirectiveInject(i2.SomeService3), i0.ɵɵdirectiveInject(i3.nested.SomeService4), i0.ɵɵinjectAttribute('title'), i0.ɵɵdirectiveInject(MESSAGE_TOKEN)); };`,
        );
      });

      it('should include injector types with all possible import/injection styles into standalone directive factory', () => {
        env.write(
          'test.ts',
          `
          import {Directive, Attribute, Inject} from '@angular/core';
          import {SomeClass} from './some-where'
          import {SomeService1} from './some-where1'
          import SomeService2 from './some-where2'
          import * as SomeWhere3 from './some-where3'
          import * as SomeWhere4 from './some-where4'

          @Directive()
          export class MainDirective {
            constructor(
              private someService1: SomeService1,
              private someService2: SomeService2,
              private someService3: SomeWhere3.SomeService3,
              private someService4: SomeWhere4.nested.SomeService4,
              @Attribute('title') title: string,
              @Inject(MESSAGE_TOKEN) tokenMessage: SomeClass,
              ) {}
          }
          `,
        );

        env.driveMain();
        const jsContents = env.getContents('test.js');

        expect(jsContents).toContain(
          `MainDirective.ɵfac = function MainDirective_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || MainDirective)(i0.ɵɵdirectiveInject(i1.SomeService1), i0.ɵɵdirectiveInject(SomeService2), i0.ɵɵdirectiveInject(i2.SomeService3), i0.ɵɵdirectiveInject(i3.nested.SomeService4), i0.ɵɵinjectAttribute('title'), i0.ɵɵdirectiveInject(MESSAGE_TOKEN)); };`,
        );
      });

      it('should include injector types with all possible import/injection styles into pipe factory', () => {
        env.write(
          'test.ts',
          `
          import {Pipe, NgModule, Attribute, Inject} from '@angular/core';
          import {SomeClass} from './some-where'
          import {SomeService1} from './some-where1'
          import SomeService2 from './some-where2'
          import * as SomeWhere3 from './some-where3'
          import * as SomeWhere4 from './some-where4'

          @Pipe({name: 'pipe'})
          export class MainPipe {
            constructor(
              private someService1: SomeService1,
              private someService2: SomeService2,
              private someService3: SomeWhere3.SomeService3,
              private someService4: SomeWhere4.nested.SomeService4,
              @Attribute('title') title: string,
              @Inject(MESSAGE_TOKEN) tokenMessage: SomeClass,
              ) {}
          }

          @NgModule({
            declarations: [MainPipe],
          })
          export class MainModule {
          }
          `,
        );

        env.driveMain();
        const jsContents = env.getContents('test.js');

        expect(jsContents).toContain(
          `MainPipe.ɵfac = function MainPipe_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || MainPipe)(i0.ɵɵdirectiveInject(i1.SomeService1, 16), i0.ɵɵdirectiveInject(SomeService2, 16), i0.ɵɵdirectiveInject(i2.SomeService3, 16), i0.ɵɵdirectiveInject(i3.nested.SomeService4, 16), i0.ɵɵinjectAttribute('title'), i0.ɵɵdirectiveInject(MESSAGE_TOKEN, 16)); };`,
        );
      });

      it('should include injector types with all possible import/injection styles into standalone pipe factory', () => {
        env.write(
          'test.ts',
          `
          import {Pipe, Attribute, Inject} from '@angular/core';
          import {SomeClass} from './some-where'
          import {SomeService1} from './some-where1'
          import SomeService2 from './some-where2'
          import * as SomeWhere3 from './some-where3'
          import * as SomeWhere4 from './some-where4'

          @Pipe({name: 'pipe'})
          export class MainPipe {
            constructor(
              private someService1: SomeService1,
              private someService2: SomeService2,
              private someService3: SomeWhere3.SomeService3,
              private someService4: SomeWhere4.nested.SomeService4,
              @Attribute('title') title: string,
              @Inject(MESSAGE_TOKEN) tokenMessage: SomeClass,
              ) {}
          }
          `,
        );

        env.driveMain();
        const jsContents = env.getContents('test.js');

        expect(jsContents).toContain(
          `MainPipe.ɵfac = function MainPipe_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || MainPipe)(i0.ɵɵdirectiveInject(i1.SomeService1, 16), i0.ɵɵdirectiveInject(SomeService2, 16), i0.ɵɵdirectiveInject(i2.SomeService3, 16), i0.ɵɵdirectiveInject(i3.nested.SomeService4, 16), i0.ɵɵinjectAttribute('title'), i0.ɵɵdirectiveInject(MESSAGE_TOKEN, 16)); };`,
        );
      });

      it('should include injector types with all possible import/injection styles into injectable factory', () => {
        env.write(
          'test.ts',
          `
          import {Injectable, Attribute, Inject} from '@angular/core';
          import {SomeClass} from './some-where'
          import {SomeService1} from './some-where1'
          import SomeService2 from './some-where2'
          import * as SomeWhere3 from './some-where3'
          import * as SomeWhere4 from './some-where4'

          @Injectable({
            providedIn: 'root',
          })
          export class MainService {
            constructor(
              private someService1: SomeService1,
              private someService2: SomeService2,
              private someService3: SomeWhere3.SomeService3,
              private someService4: SomeWhere4.nested.SomeService4,
              @Attribute('title') title: string,
              @Inject(MESSAGE_TOKEN) tokenMessage: SomeClass,
              ) {}
          }
          `,
        );

        env.driveMain();
        const jsContents = env.getContents('test.js');

        expect(jsContents).toContain(
          `MainService.ɵfac = function MainService_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || MainService)(i0.ɵɵinject(i1.SomeService1), i0.ɵɵinject(SomeService2), i0.ɵɵinject(i2.SomeService3), i0.ɵɵinject(i3.nested.SomeService4), i0.ɵɵinjectAttribute('title'), i0.ɵɵinject(MESSAGE_TOKEN)); };`,
        );
      });

      it('should include injector types with all possible import/injection styles into ng module factory', () => {
        env.write(
          'test.ts',
          `
          import {Component, NgModule, Attribute, Inject} from '@angular/core';
          import {SomeClass} from './some-where'
          import {SomeService1} from './some-where1'
          import SomeService2 from './some-where2'
          import * as SomeWhere3 from './some-where3'
          import * as SomeWhere4 from './some-where4'

          @NgModule({
          })
          export class MainModule {
            constructor(
              private someService1: SomeService1,
              private someService2: SomeService2,
              private someService3: SomeWhere3.SomeService3,
              private someService4: SomeWhere4.nested.SomeService4,
              @Attribute('title') title: string,
              @Inject(MESSAGE_TOKEN) tokenMessage: SomeClass,
              ) {}
          }
          `,
        );

        env.driveMain();
        const jsContents = env.getContents('test.js');

        expect(jsContents).toContain(
          `MainModule.ɵfac = function MainModule_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || MainModule)(i0.ɵɵinject(i1.SomeService1), i0.ɵɵinject(SomeService2), i0.ɵɵinject(i2.SomeService3), i0.ɵɵinject(i3.nested.SomeService4), i0.ɵɵinjectAttribute('title'), i0.ɵɵinject(MESSAGE_TOKEN)); };`,
        );
      });

      it('should generate invalid factory for injectable when type parameter types are used as token', () => {
        env.write(
          'test.ts',
          `
          import {Injectable} from '@angular/core';

          @Injectable()
          export class MainService<S> {
            constructor(
              private someService1: S,
              ) {}
          }
          `,
        );

        env.driveMain();
        const jsContents = env.getContents('test.js');

        expect(jsContents).toContain(
          `MainService.ɵfac = function MainService_Factory(__ngFactoryType__) { i0.ɵɵinvalidFactory(); };`,
        );
      });

      it('should generate invalid factory for injectable when when token is imported as type', () => {
        env.write(
          'test.ts',
          `
          import {Injectable} from '@angular/core';
          import {type MyService} from './somewhere';

          @Injectable()
          export class MainService {
            constructor(
              private myService: MyService,
              ) {}
          }
          `,
        );

        env.driveMain();
        const jsContents = env.getContents('test.js');

        expect(jsContents).toContain(
          `MainService.ɵfac = function MainService_Factory(__ngFactoryType__) { i0.ɵɵinvalidFactory(); };`,
        );
      });

      it('should generate invalid factory for injectable when when token is a type', () => {
        env.write(
          'test.ts',
          `
          import {Injectable} from '@angular/core';

          type MyService = {a:string};

          @Injectable()
          export class MainService {
            constructor(
              private myService: MyService,
              ) {}
          }
          `,
        );

        env.driveMain();
        const jsContents = env.getContents('test.js');

        expect(jsContents).toContain(
          `MainService.ɵfac = function MainService_Factory(__ngFactoryType__) { i0.ɵɵinvalidFactory(); };`,
        );
      });

      it('should generate invalid factory for injectable when when token is an interface', () => {
        env.write(
          'test.ts',
          `
          import {Injectable} from '@angular/core';

          interface MyService {a:string}

          @Injectable()
          export class MainService {
            constructor(
              private myService: MyService,
              ) {}
          }
          `,
        );

        env.driveMain();
        const jsContents = env.getContents('test.js');

        expect(jsContents).toContain(
          `MainService.ɵfac = function MainService_Factory(__ngFactoryType__) { i0.ɵɵinvalidFactory(); };`,
        );
      });

      it('should generate invalid factory for directive without selector type parameter types are used as token', () => {
        env.write(
          'test.ts',
          `
          import {Directive} from '@angular/core';

          @Directive()
          export class MyDirective<S> {
            constructor(
              private myService: S,
              ) {}
          }
          `,
        );

        env.driveMain();
        const jsContents = env.getContents('test.js');

        expect(jsContents).toContain(
          `MyDirective.ɵfac = function MyDirective_Factory(__ngFactoryType__) { i0.ɵɵinvalidFactory(); };`,
        );
      });

      it('should generate invalid factory for directive without selector when token is imported as type', () => {
        env.write(
          'test.ts',
          `
          import {Directive} from '@angular/core';
          import {type MyService} from './somewhere';

          @Directive()
          export class MyDirective {
            constructor(
              private myService: MyService,
              ) {}
          }
          `,
        );

        env.driveMain();
        const jsContents = env.getContents('test.js');

        expect(jsContents).toContain(
          `MyDirective.ɵfac = function MyDirective_Factory(__ngFactoryType__) { i0.ɵɵinvalidFactory(); };`,
        );
      });

      it('should generate invalid factory for directive without selector when token is a type', () => {
        env.write(
          'test.ts',
          `
          import {Directive} from '@angular/core';

          type MyService = {a:string};

          @Directive()
          export class MyDirective {
            constructor(
              private myService: MyService,
              ) {}
          }
          `,
        );

        env.driveMain();
        const jsContents = env.getContents('test.js');

        expect(jsContents).toContain(
          `MyDirective.ɵfac = function MyDirective_Factory(__ngFactoryType__) { i0.ɵɵinvalidFactory(); };`,
        );
      });

      it('should generate invalid factory for directive without selector when token is an interface', () => {
        env.write(
          'test.ts',
          `
          import {Directive} from '@angular/core';

          interface MyService {a:string}

          @Directive()
          export class MyDirective {
            constructor(
              private myService: MyService,
              ) {}
          }
          `,
        );

        env.driveMain();
        const jsContents = env.getContents('test.js');

        expect(jsContents).toContain(
          `MyDirective.ɵfac = function MyDirective_Factory(__ngFactoryType__) { i0.ɵɵinvalidFactory(); };`,
        );
      });
    });

    describe('LOCAL_COMPILATION_UNRESOLVED_CONST errors', () => {
      it('should show correct error message when using an external symbol for component template', () => {
        env.write(
          'test.ts',
          `
          import {Component} from '@angular/core';
          import {ExternalString} from './some-where';

          @Component({
            template: ExternalString,
          })
          export class Main {
          }
          `,
        );

        const errors = env.driveDiagnostics();

        expect(errors.length).toBe(1);

        const {code, messageText, length, relatedInformation} = errors[0];

        expect(code).toBe(ngErrorCode(ErrorCode.LOCAL_COMPILATION_UNRESOLVED_CONST));
        expect(length).toBe(14);
        expect(relatedInformation).toBeUndefined();

        const text = ts.flattenDiagnosticMessageText(messageText, '\n');

        expect(text).toEqual(
          'Unresolved identifier found for @Component.template field! Did you import this identifier from a file outside of the compilation unit? This is not allowed when Angular compiler runs in local mode. Possible solutions: 1) Move the declaration into a file within the compilation unit, 2) Inline the template, 3) Move the template into a separate .html file and include it using @Component.templateUrl',
        );
      });

      it('should show correct error message when using an external symbol for component styles array', () => {
        env.write(
          'test.ts',
          `
          import {Component} from '@angular/core';
          import {ExternalString} from './some-where';

          const InternalStyle = "p{color:blue}";

          @Component({
            styles: [InternalStyle, ExternalString],
            template: '',

          })
          export class Main {
          }
          `,
        );

        const errors = env.driveDiagnostics();

        expect(errors.length).toBe(1);

        const {code, messageText, relatedInformation, length} = errors[0];

        expect(code).toBe(ngErrorCode(ErrorCode.LOCAL_COMPILATION_UNRESOLVED_CONST));
        expect(length).toBe(14);
        expect(relatedInformation).toBeUndefined();

        const text = ts.flattenDiagnosticMessageText(messageText, '\n');

        expect(text).toEqual(
          'Unresolved identifier found for @Component.styles field! Did you import this identifier from a file outside of the compilation unit? This is not allowed when Angular compiler runs in local mode. Possible solutions: 1) Move the declarations into a file within the compilation unit, 2) Inline the styles, 3) Move the styles into separate files and include it using @Component.styleUrls',
        );
      });

      it('should show correct error message when using an external symbol for component styles', () => {
        env.write(
          'test.ts',
          `
          import {Component} from '@angular/core';
          import {ExternalString} from './some-where';

          @Component({
            styles: ExternalString,
            template: '',

          })
          export class Main {
          }
          `,
        );

        const errors = env.driveDiagnostics();

        expect(errors.length).toBe(1);

        const {code, messageText, relatedInformation, length} = errors[0];

        expect(code).toBe(ngErrorCode(ErrorCode.LOCAL_COMPILATION_UNRESOLVED_CONST));
        expect(length).toBe(14);
        expect(relatedInformation).toBeUndefined();

        const text = ts.flattenDiagnosticMessageText(messageText, '\n');

        expect(text).toEqual(
          'Unresolved identifier found for @Component.styles field! Did you import this identifier from a file outside of the compilation unit? This is not allowed when Angular compiler runs in local mode. Possible solutions: 1) Move the declarations into a file within the compilation unit, 2) Inline the styles, 3) Move the styles into separate files and include it using @Component.styleUrls',
        );
      });

      it('should show correct error message when using an external symbol for component selector', () => {
        env.write(
          'test.ts',
          `
          import {Component} from '@angular/core';
          import {ExternalString} from './some-where';

          @Component({
            selector: ExternalString,
            template: '',

          })
          export class Main {
          }
          `,
        );

        const errors = env.driveDiagnostics();

        expect(errors.length).toBe(1);

        const {code, messageText, relatedInformation, length} = errors[0];

        expect(code).toBe(ngErrorCode(ErrorCode.LOCAL_COMPILATION_UNRESOLVED_CONST));
        expect(length).toBe(14);
        expect(relatedInformation).toBeUndefined();

        const text = ts.flattenDiagnosticMessageText(messageText, '\n');

        expect(text).toEqual(
          'Unresolved identifier found for @Component.selector field! Did you import this identifier from a file outside of the compilation unit? This is not allowed when Angular compiler runs in local mode. Possible solutions: 1) Move the declarations into a file within the compilation unit, 2) Inline the selector',
        );
      });

      it("should show correct error message when using an external symbol for component @HostListener's event name argument", () => {
        env.write(
          'test.ts',
          `
          import {Component, HostListener} from '@angular/core';
          import {ExternalString} from './some-where';

          @Component({
            template: '',
          })
          export class Main {
            @HostListener(ExternalString, ['$event'])
            handle() {}
          }
          `,
        );

        const errors = env.driveDiagnostics();

        expect(errors.length).toBe(1);

        const {code, messageText, relatedInformation, length} = errors[0];

        expect(code).toBe(ngErrorCode(ErrorCode.LOCAL_COMPILATION_UNRESOLVED_CONST));
        expect(length).toBe(14);
        expect(relatedInformation).toBeUndefined();

        const text = ts.flattenDiagnosticMessageText(messageText, '\n');

        expect(text).toEqual(
          `Unresolved identifier found for @HostListener's event name argument! Did you import this identifier from a file outside of the compilation unit? This is not allowed when Angular compiler runs in local mode. Possible solutions: 1) Move the declaration into a file within the compilation unit, 2) Inline the argument`,
        );
      });

      it("should show correct error message when using an external symbol for directive @HostListener's event name argument", () => {
        env.write(
          'test.ts',
          `
          import {Directive, HostListener} from '@angular/core';
          import {ExternalString} from './some-where';

          @Directive({selector: '[test]'})
          export class Main {
            @HostListener(ExternalString, ['$event'])
            handle() {}
          }
          `,
        );

        const errors = env.driveDiagnostics();

        expect(errors.length).toBe(1);

        const {code, messageText, relatedInformation, length} = errors[0];

        expect(code).toBe(ngErrorCode(ErrorCode.LOCAL_COMPILATION_UNRESOLVED_CONST));
        expect(length).toBe(14);
        expect(relatedInformation).toBeUndefined();

        const text = ts.flattenDiagnosticMessageText(messageText, '\n');

        expect(text).toEqual(
          `Unresolved identifier found for @HostListener's event name argument! Did you import this identifier from a file outside of the compilation unit? This is not allowed when Angular compiler runs in local mode. Possible solutions: 1) Move the declaration into a file within the compilation unit, 2) Inline the argument`,
        );
      });

      it("should show correct error message when using an external symbol for component @HostBinding's argument", () => {
        env.write(
          'test.ts',
          `
          import {Component, HostBinding} from '@angular/core';
          import {ExternalString} from './some-where';

          @Component({template: ''})
          export class Main {
            @HostBinding(ExternalString) id = 'some thing';
          }
          `,
        );

        const errors = env.driveDiagnostics();

        expect(errors.length).toBe(1);

        const {code, messageText, relatedInformation, length} = errors[0];

        expect(code).toBe(ngErrorCode(ErrorCode.LOCAL_COMPILATION_UNRESOLVED_CONST));
        expect(length).toBe(14);
        expect(relatedInformation).toBeUndefined();

        const text = ts.flattenDiagnosticMessageText(messageText, '\n');

        expect(text).toEqual(
          `Unresolved identifier found for @HostBinding's argument! Did you import this identifier from a file outside of the compilation unit? This is not allowed when Angular compiler runs in local mode. Possible solutions: 1) Move the declaration into a file within the compilation unit, 2) Inline the argument`,
        );
      });

      it("should show correct error message when using an external symbol for directive @HostBinding's argument", () => {
        env.write(
          'test.ts',
          `
          import {Directive, HostBinding} from '@angular/core';
          import {ExternalString} from './some-where';

          @Directive({selector: '[test]'})
          export class Main {
            @HostBinding(ExternalString) id = 'some thing';
          }
          `,
        );

        const errors = env.driveDiagnostics();

        expect(errors.length).toBe(1);

        const {code, messageText, relatedInformation, length} = errors[0];

        expect(code).toBe(ngErrorCode(ErrorCode.LOCAL_COMPILATION_UNRESOLVED_CONST));
        expect(length).toBe(14);
        expect(relatedInformation).toBeUndefined();

        const text = ts.flattenDiagnosticMessageText(messageText, '\n');

        expect(text).toEqual(
          `Unresolved identifier found for @HostBinding's argument! Did you import this identifier from a file outside of the compilation unit? This is not allowed when Angular compiler runs in local mode. Possible solutions: 1) Move the declaration into a file within the compilation unit, 2) Inline the argument`,
        );
      });

      it('should show correct error message when using an external symbol for @Directive.exportAs argument', () => {
        env.write(
          'test.ts',
          `
          import {Directive} from '@angular/core';
          import {ExternalString} from './some-where';

          @Directive({selector: '[test]', exportAs: ExternalString})
          export class Main {}
          `,
        );

        const errors = env.driveDiagnostics();

        expect(errors.length).toBe(1);

        const {code, messageText, relatedInformation, length} = errors[0];

        expect(code).toBe(ngErrorCode(ErrorCode.LOCAL_COMPILATION_UNRESOLVED_CONST));
        expect(length).toBe(14);
        expect(relatedInformation).toBeUndefined();

        const text = ts.flattenDiagnosticMessageText(messageText, '\n');

        expect(text).toEqual(
          `Unresolved identifier found for exportAs field! Did you import this identifier from a file outside of the compilation unit? This is not allowed when Angular compiler runs in local mode. Possible solutions: 1) Move the declarations into a file within the compilation unit, 2) Inline the selector`,
        );
      });
    });

    describe('ng module bootstrap def', () => {
      it('should include the bootstrap definition in ɵɵsetNgModuleScope instead of ɵɵdefineNgModule', () => {
        env.write(
          'test.ts',
          `
        import {NgModule} from '@angular/core';
        import {App} from './some-where';

        @NgModule({
          declarations: [App],
          bootstrap: [App],
        })
        export class AppModule {
        }
        `,
        );

        env.driveMain();
        const jsContents = env.getContents('test.js');

        expect(jsContents).toContain(
          'AppModule.ɵmod = /*@__PURE__*/ i0.ɵɵdefineNgModule({ type: AppModule });',
        );
        expect(jsContents).toContain(
          'ɵɵsetNgModuleScope(AppModule, { declarations: [App], bootstrap: [App] }); })();',
        );
      });

      it('should include no bootstrap definition in ɵɵsetNgModuleScope if the NgModule has no bootstrap field', () => {
        env.write(
          'test.ts',
          `
        import {NgModule} from '@angular/core';
        import {App} from './some-where';

        @NgModule({
          declarations: [App],
        })
        export class AppModule {
        }
        `,
        );

        env.driveMain();
        const jsContents = env.getContents('test.js');

        expect(jsContents).toContain(
          'AppModule.ɵmod = /*@__PURE__*/ i0.ɵɵdefineNgModule({ type: AppModule });',
        );
        expect(jsContents).toContain(
          'ɵɵsetNgModuleScope(AppModule, { declarations: [App] }); })();',
        );
      });
    });

    describe('host directives', () => {
      it('should generate component hostDirectives definition without input and output', () => {
        env.write(
          'test.ts',
          `
          import {Directive, Component} from '@angular/core';
          import {ExternalDirective} from 'some_where';
          import * as n from 'some_where2';

          @Directive({standalone: true})
          export class LocalDirective {
          }

          @Component({
            selector: 'my-comp',
            template: '',
            hostDirectives: [ExternalDirective, n.ExternalDirective, LocalDirective],
            standalone: false,
          })
          export class MyComp {}
        `,
        );

        env.driveMain();
        const jsContents = env.getContents('test.js');

        expect(jsContents).toContain(
          'features: [i0.ɵɵHostDirectivesFeature([ExternalDirective, n.ExternalDirective, LocalDirective])]',
        );
      });

      it('should generate component hostDirectives definition for externally imported directives with input and output', () => {
        env.write(
          'test.ts',
          `
          import {Directive, Component} from '@angular/core';
          import {HostDir} from 'some_where';

          @Component({
            selector: 'my-comp',
            template: '',
            hostDirectives: [{
              directive: HostDir,
              inputs: ['value', 'color: colorAlias'],
              outputs: ['opened', 'closed: closedAlias'],
            }],
            standalone: false,
          })
          export class MyComp {}
        `,
        );

        env.driveMain();
        const jsContents = env.getContents('test.js');

        expect(jsContents).toContain(
          'features: [i0.ɵɵHostDirectivesFeature([{ directive: HostDir, ' +
            'inputs: ["value", "value", "color", "colorAlias"], ' +
            'outputs: ["opened", "opened", "closed", "closedAlias"] }])]',
        );
      });

      it('should generate component hostDirectives definition for local directives with input and output', () => {
        env.write(
          'test.ts',
          `
          import {Directive, Component} from '@angular/core';

          @Directive()
          export class LocalDirective {
          }

          @Component({
            selector: 'my-comp',
            template: '',
            hostDirectives: [{
              directive: LocalDirective,
              inputs: ['value', 'color: colorAlias'],
              outputs: ['opened', 'closed: closedAlias'],
            }],
            standalone: false,
          })
          export class MyComp {}
        `,
        );

        env.driveMain();
        const jsContents = env.getContents('test.js');

        expect(jsContents).toContain(
          'features: [i0.ɵɵHostDirectivesFeature([{ directive: LocalDirective, ' +
            'inputs: ["value", "value", "color", "colorAlias"], ' +
            'outputs: ["opened", "opened", "closed", "closedAlias"] }])]',
        );
      });

      it('should generate directive hostDirectives definitions', () => {
        env.write(
          'test.ts',
          `
          import {Directive, Component} from '@angular/core';
          import {ExternalDirective} from 'some_where';

          @Directive({hostDirectives: [ExternalDirective]})
          export class LocalDirective {}

          @Directive({hostDirectives: [LocalDirective]})
          export class LocalDirective2 {}
        `,
        );

        env.driveMain();
        const jsContents = env.getContents('test.js');

        expect(jsContents).toContain(
          'ɵɵdefineDirective({ type: LocalDirective, ' +
            'features: [i0.ɵɵHostDirectivesFeature([ExternalDirective])] });',
        );
        expect(jsContents).toContain(
          'ɵɵdefineDirective({ type: LocalDirective2, ' +
            'features: [i0.ɵɵHostDirectivesFeature([LocalDirective])] });',
        );
      });

      it('should generate hostDirectives definition with forward references of local directives', () => {
        env.write(
          'test.ts',
          `
          import {Component, Directive, forwardRef, Input} from '@angular/core';

          @Component({
            selector: 'my-component',
            template: '',
            hostDirectives: [forwardRef(() => DirectiveB)],
            standalone: false,
          })
          export class MyComponent {
          }

          @Directive({
            hostDirectives: [{directive: forwardRef(() => DirectiveA), inputs: ['value']}],
          })
          export class DirectiveB {
          }

          @Directive({standalone: true})
          export class DirectiveA {
            @Input() value: any;
          }
        `,
        );

        env.driveMain();
        const jsContents = env.getContents('test.js');

        expect(jsContents).toContain(
          'features: [i0.ɵɵHostDirectivesFeature(function () { return [DirectiveB]; })]',
        );
        expect(jsContents).toContain(
          'features: [i0.ɵɵHostDirectivesFeature(function () { return [{ directive: DirectiveA, inputs: ["value", "value"] }]; })]',
        );
      });

      it('should produce fatal diagnostics for host directives with forward references of externally imported directive', () => {
        env.write(
          'test.ts',
          `
          import {Component, Directive, forwardRef} from '@angular/core';
          import {ExternalDirective} from 'some_where';

          @Component({
            selector: 'my-component',
            template: '',
            hostDirectives: [forwardRef(() => ExternalDirective)]
            //hostDirectives: [ExternalDirective]
          })
          export class MyComponent {
          }
        `,
        );

        const messages = env.driveDiagnostics();

        expect(messages[0].code).toBe(
          ngErrorCode(ErrorCode.LOCAL_COMPILATION_UNSUPPORTED_EXPRESSION),
        );
        expect(messages[0].messageText).toEqual(
          'In local compilation mode, host directive cannot be an expression. Use an identifier instead',
        );
      });
    });

    describe('input transform', () => {
      it('should generate input info for transform function imported externally using name', () => {
        env.write(
          'test.ts',
          `
        import {Component, NgModule, Input} from '@angular/core';
        import {externalFunc} from './some_where';

        @Component({
          template: '<span>{{x}}</span>',
        })
        export class Main {
          @Input({transform: externalFunc})
          x: string = '';
        }
     `,
        );

        env.driveMain();
        const jsContents = env.getContents('test.js');

        expect(jsContents).toContain('inputs: { x: [2, "x", "x", externalFunc] }');
      });

      it('should generate input info for transform function imported externally using namespace', () => {
        env.write(
          'test.ts',
          `
        import {Component, NgModule, Input} from '@angular/core';
        import * as n from './some_where';

        @Component({
          template: '<span>{{x}}</span>',
        })
        export class Main {
          @Input({transform: n.externalFunc})
          x: string = '';
        }
     `,
        );

        env.driveMain();
        const jsContents = env.getContents('test.js');

        expect(jsContents).toContain('inputs: { x: [2, "x", "x", n.externalFunc] }');
      });

      it('should generate input info for transform function defined locally', () => {
        env.write(
          'test.ts',
          `
        import {Component, NgModule, Input} from '@angular/core';

        @Component({
          template: '<span>{{x}}</span>',
        })
        export class Main {
          @Input({transform: localFunc})
          x: string = '';
        }

        function localFunc(value: string) {
          return value;
        }
     `,
        );

        env.driveMain();
        const jsContents = env.getContents('test.js');

        expect(jsContents).toContain('inputs: { x: [2, "x", "x", localFunc] }');
      });

      it('should generate input info for inline transform function', () => {
        env.write(
          'test.ts',
          `
        import {Component, NgModule, Input} from '@angular/core';

        @Component({
          template: '<span>{{x}}</span>',
        })
        export class Main {
          @Input({transform: (v: string) => v + 'TRANSFORMED!'})
          x: string = '';
        }
     `,
        );

        env.driveMain();
        const jsContents = env.getContents('test.js');

        expect(jsContents).toContain('inputs: { x: [2, "x", "x", (v) => v + \'TRANSFORMED!\'] }');
      });

      it('should not check inline function param type', () => {
        env.write(
          'test.ts',
          `
        import {Component, NgModule, Input} from '@angular/core';

        @Component({
          template: '<span>{{x}}</span>',
        })
        export class Main {
          @Input({transform: v => v + 'TRANSFORMED!'})
          x: string = '';
        }
     `,
        );

        env.driveMain();
        const jsContents = env.getContents('test.js');

        expect(jsContents).toContain('inputs: { x: [2, "x", "x", v => v + \'TRANSFORMED!\'] }');
      });
    });

    describe('@defer', () => {
      beforeEach(() => {
        tsconfig({onlyExplicitDeferDependencyImports: true});
      });

      it('should handle `@Component.deferredImports` field', () => {
        env.write(
          'deferred-a.ts',
          `
          import {Component} from '@angular/core';
          @Component({
            selector: 'deferred-cmp-a',
            template: 'DeferredCmpA contents',
          })
          export class DeferredCmpA {
          }
        `,
        );

        env.write(
          'deferred-b.ts',
          `
          import {Component} from '@angular/core';
          @Component({
                        selector: 'deferred-cmp-b',
            template: 'DeferredCmpB contents',
          })
          export class DeferredCmpB {
          }
        `,
        );

        env.write(
          'test.ts',
          `
          import {Component} from '@angular/core';
          import {DeferredCmpA} from './deferred-a';
          import {DeferredCmpB} from './deferred-b';
          @Component({
            deferredImports: [DeferredCmpA, DeferredCmpB],
            template: \`
              @defer {
                <deferred-cmp-a />
              }
              @defer {
                <deferred-cmp-b />
              }
            \`,
          })
          export class AppCmp {
          }
        `,
        );

        env.driveMain();
        const jsContents = env.getContents('test.js');

        // Expect that all deferrableImports in local compilation mode
        // are located in a single function (since we can't detect in
        // the local mode which components belong to which block).
        expect(jsContents).toContain(
          'const AppCmp_DeferFn = () => [' +
            'import("./deferred-a").then(m => m.DeferredCmpA), ' +
            'import("./deferred-b").then(m => m.DeferredCmpB)];',
        );

        // Make sure there are no eager imports present in the output.
        expect(jsContents).not.toContain(`from './deferred-a'`);
        expect(jsContents).not.toContain(`from './deferred-b'`);

        // All defer instructions use the same dependency function.
        expect(jsContents).toContain('ɵɵdefer(1, 0, AppCmp_DeferFn);');
        expect(jsContents).toContain('ɵɵdefer(4, 3, AppCmp_DeferFn);');

        // Expect `ɵsetClassMetadataAsync` to contain dynamic imports too.
        expect(jsContents).toContain(
          'ɵsetClassMetadataAsync(AppCmp, () => [' +
            'import("./deferred-a").then(m => m.DeferredCmpA), ' +
            'import("./deferred-b").then(m => m.DeferredCmpB)], ' +
            '(DeferredCmpA, DeferredCmpB) => {',
        );
      });

      it('should handle `@Component.imports` field', () => {
        env.write(
          'deferred-a.ts',
          `
          import {Component} from '@angular/core';
          @Component({
            selector: 'deferred-cmp-a',
            template: 'DeferredCmpA contents',
          })
          export class DeferredCmpA {
          }
        `,
        );

        env.write(
          'test.ts',
          `
          import {Component} from '@angular/core';
          import {DeferredCmpA} from './deferred-a';
          @Component({
            imports: [DeferredCmpA],
            template: \`
              @defer {
                <deferred-cmp-a />
              }
            \`,
          })
          export class AppCmp {
          }
        `,
        );

        env.driveMain();
        const jsContents = env.getContents('test.js');

        // In local compilation mode we can't detect which components
        // belong to `@defer` blocks, thus can't determine whether corresponding
        // classes can be defer-loaded. In this case we retain eager imports
        // and do not generate defer dependency functions for `@defer` instructions.

        // Eager imports are retained in the output.
        expect(jsContents).toContain(`from './deferred-a'`);

        // Defer instructions do not have a dependency function,
        // since all dependencies were defined in `@Component.imports`.
        expect(jsContents).toContain('ɵɵdefer(1, 0);');

        // Expect `ɵsetClassMetadata` (sync) to be generated.
        expect(jsContents).toContain('ɵsetClassMetadata(AppCmp,');
      });

      it('should handle defer blocks that rely on deps from `deferredImports` and `imports`', () => {
        env.write(
          'eager-a.ts',
          `
              import {Component} from '@angular/core';
              @Component({
                selector: 'eager-cmp-a',
                template: 'EagerCmpA contents',
              })
              export class EagerCmpA {
              }
            `,
        );

        env.write(
          'deferred-a.ts',
          `
              import {Component} from '@angular/core';
              @Component({
                selector: 'deferred-cmp-a',
                template: 'DeferredCmpA contents',
              })
              export class DeferredCmpA {
              }
            `,
        );

        env.write(
          'deferred-b.ts',
          `
              import {Component} from '@angular/core';
              @Component({
                                selector: 'deferred-cmp-b',
                template: 'DeferredCmpB contents',
              })
              export class DeferredCmpB {
              }
            `,
        );

        env.write(
          'test.ts',
          `
              import {Component} from '@angular/core';
              import {DeferredCmpA} from './deferred-a';
              import {DeferredCmpB} from './deferred-b';
              import {EagerCmpA} from './eager-a';
              @Component({
                imports: [EagerCmpA],
                deferredImports: [DeferredCmpA, DeferredCmpB],
                template: \`
                  @defer {
                    <eager-cmp-a />
                    <deferred-cmp-a />
                  }
                  @defer {
                    <eager-cmp-a />
                    <deferred-cmp-b />
                  }
                \`,
              })
              export class AppCmp {
              }
            `,
        );

        env.driveMain();
        const jsContents = env.getContents('test.js');

        // Expect that all deferrableImports in local compilation mode
        // are located in a single function (since we can't detect in
        // the local mode which components belong to which block).
        // Eager dependencies are **not* included here.
        expect(jsContents).toContain(
          'const AppCmp_DeferFn = () => [' +
            'import("./deferred-a").then(m => m.DeferredCmpA), ' +
            'import("./deferred-b").then(m => m.DeferredCmpB)];',
        );

        // Make sure there are no eager imports present in the output.
        expect(jsContents).not.toContain(`from './deferred-a'`);
        expect(jsContents).not.toContain(`from './deferred-b'`);

        // Eager dependencies retain their imports.
        expect(jsContents).toContain(`from './eager-a';`);

        // All defer instructions use the same dependency function.
        expect(jsContents).toContain('ɵɵdefer(1, 0, AppCmp_DeferFn);');
        expect(jsContents).toContain('ɵɵdefer(4, 3, AppCmp_DeferFn);');

        // Expect `ɵsetClassMetadataAsync` to contain dynamic imports too.
        expect(jsContents).toContain(
          'ɵsetClassMetadataAsync(AppCmp, () => [' +
            'import("./deferred-a").then(m => m.DeferredCmpA), ' +
            'import("./deferred-b").then(m => m.DeferredCmpB)], ' +
            '(DeferredCmpA, DeferredCmpB) => {',
        );
      });

      it(
        'should support importing multiple deferrable deps from a single file ' +
          'and use them within `@Component.deferrableImports` field',
        () => {
          env.write(
            'deferred-deps.ts',
            `
              import {Component} from '@angular/core';

              @Component({
                selector: 'deferred-cmp-a',
                template: 'DeferredCmpA contents',
              })
              export class DeferredCmpA {
              }

              @Component({
                selector: 'deferred-cmp-b',
                template: 'DeferredCmpB contents',
              })
              export class DeferredCmpB {
              }
            `,
          );

          env.write(
            'test.ts',
            `
              import {Component} from '@angular/core';

              // This import brings multiple symbols, but all of them are
              // used within @Component.deferredImports, thus this import
              // can be removed in favor of dynamic imports.
              import {DeferredCmpA, DeferredCmpB} from './deferred-deps';

              @Component({
                deferredImports: [DeferredCmpA],
                template: \`
                  @defer {
                    <deferred-cmp-a />
                  }
                \`,
              })
              export class AppCmpA {}

              @Component({
                deferredImports: [DeferredCmpB],
                template: \`
                  @defer {
                    <deferred-cmp-b />
                  }
                \`,
              })
              export class AppCmpB {}
            `,
          );

          env.driveMain();
          const jsContents = env.getContents('test.js');

          // Expect that we generate 2 different defer functions
          // (one for each component).
          expect(jsContents).toContain(
            'const AppCmpA_DeferFn = () => [' +
              'import("./deferred-deps").then(m => m.DeferredCmpA)]',
          );
          expect(jsContents).toContain(
            'const AppCmpB_DeferFn = () => [' +
              'import("./deferred-deps").then(m => m.DeferredCmpB)]',
          );

          // Make sure there are no eager imports present in the output.
          expect(jsContents).not.toContain(`from './deferred-deps'`);

          // Defer instructions use per-component dependency function.
          expect(jsContents).toContain('ɵɵdefer(1, 0, AppCmpA_DeferFn)');
          expect(jsContents).toContain('ɵɵdefer(1, 0, AppCmpB_DeferFn)');

          // Expect `ɵsetClassMetadataAsync` to contain dynamic imports too.
          expect(jsContents).toContain(
            'ɵsetClassMetadataAsync(AppCmpA, () => [' +
              'import("./deferred-deps").then(m => m.DeferredCmpA)]',
          );
          expect(jsContents).toContain(
            'ɵsetClassMetadataAsync(AppCmpB, () => [' +
              'import("./deferred-deps").then(m => m.DeferredCmpB)]',
          );
        },
      );

      it(
        'should produce a diagnostic in case imports with symbols used ' +
          'in `deferredImports` can not be removed',
        () => {
          env.write(
            'deferred-deps.ts',
            `
              import {Component} from '@angular/core';

              @Component({
                selector: 'deferred-cmp-a',
                template: 'DeferredCmpA contents',
              })
              export class DeferredCmpA {
              }

              @Component({
                selector: 'deferred-cmp-b',
                template: 'DeferredCmpB contents',
              })
              export class DeferredCmpB {
              }

              export function utilityFn() {}
            `,
          );

          env.write(
            'test.ts',
            `
              import {Component} from '@angular/core';

              // This import can not be removed, since it'd contain
              // 'utilityFn' symbol once we remove 'DeferredCmpA' and
              // 'DeferredCmpB' and generate a dynamic import for it.
              // In this situation compiler produces a diagnostic to
              // indicate that.
              import {DeferredCmpA, DeferredCmpB, utilityFn} from './deferred-deps';

              @Component({
                deferredImports: [DeferredCmpA],
                template: \`
                  @defer {
                    <deferred-cmp-a />
                  }
                \`,
              })
              export class AppCmpA {
                ngOnInit() {
                  utilityFn();
                }
              }

              @Component({
                deferredImports: [DeferredCmpB],
                template: \`
                  @defer {
                    <deferred-cmp-b />
                  }
                \`,
              })
              export class AppCmpB {}

              @Component({
                template: 'Component without any dependencies'
              })
              export class ComponentWithoutDeps {}
            `,
          );

          const diags = env.driveDiagnostics();

          // Expect 2 diagnostics: one for each component `AppCmpA` and `AppCmpB`,
          // since both of them refer to symbols from an import declaration that
          // can not be removed.
          expect(diags.length).toBe(2);

          for (let i = 0; i < 2; i++) {
            const {code, messageText} = diags[i];
            expect(code).toBe(ngErrorCode(ErrorCode.DEFERRED_DEPENDENCY_IMPORTED_EAGERLY));
            expect(messageText).toContain(
              'This import contains symbols that are used both inside and outside ' +
                'of the `@Component.deferredImports` fields in the file.',
            );
          }
        },
      );
    });

    describe('custom decorator', () => {
      it('should produce diagnostic for each custom decorator', () => {
        env.write(
          'test.ts',
          `
          import {Component} from '@angular/core';
          import {customDecorator1, customDecorator2} from './some-where';

          @customDecorator1('hello')
          @Component({
            template: ExternalString,
          })
          @customDecorator2
          export class Main {
          }
          `,
        );

        const errors = env.driveDiagnostics();

        expect(errors.length).toBe(2);

        const text1 = ts.flattenDiagnosticMessageText(errors[0].messageText, '\n');
        const text2 = ts.flattenDiagnosticMessageText(errors[1].messageText, '\n');

        expect(errors[0].code).toBe(ngErrorCode(ErrorCode.DECORATOR_UNEXPECTED));
        expect(errors[1].code).toBe(ngErrorCode(ErrorCode.DECORATOR_UNEXPECTED));
        expect(text1).toContain(
          'In local compilation mode, Angular does not support custom decorators',
        );
        expect(text2).toContain(
          'In local compilation mode, Angular does not support custom decorators',
        );
      });
    });

    describe('template diagnostics', () => {
      it('should show correct error message for syntatic template errors - case of inline template', () => {
        env.write(
          'test.ts',
          `
          import {Component} from '@angular/core';

          @Component({
            template: '<span Hello! </span>',
          })
          export class Main {
          }
          `,
        );

        const errors = env.driveDiagnostics();

        expect(errors.length).toBeGreaterThanOrEqual(1);

        const {code, messageText} = errors[0];

        expect(code).toBe(ngErrorCode(ErrorCode.TEMPLATE_PARSE_ERROR));

        const text = ts.flattenDiagnosticMessageText(messageText, '\n');

        expect(text).toContain('Opening tag "span" not terminated');
      });

      it('should show correct error message for syntatic template errors - case of external template', () => {
        env.write(
          'test.ts',
          `
          import {Component} from '@angular/core';

          @Component({
            templateUrl: 'test.ng.html',
          })
          export class Main {
          }
          `,
        );
        env.write(
          'test.ng.html',
          `
          <span Hello! </span>
          `,
        );

        const errors = env.driveDiagnostics();

        expect(errors.length).toBeGreaterThanOrEqual(1);

        const {code, messageText} = errors[0];

        expect(code).toBe(ngErrorCode(ErrorCode.TEMPLATE_PARSE_ERROR));

        const text = ts.flattenDiagnosticMessageText(messageText, '\n');

        expect(text).toContain('Opening tag "span" not terminated');
      });
    });
  });
});
