/**
 * @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 {initMockFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing';
import ts from 'typescript';

import {FixIdForCodeFixesAll} from '../../src/codefixes/utils';
import {createModuleAndProjectWithDeclarations, LanguageServiceTestEnv} from '../../testing';

describe('code fixes', () => {
  let env: LanguageServiceTestEnv;
  beforeEach(() => {
    initMockFileSystem('Native');
    env = LanguageServiceTestEnv.setup();
  });

  it('should fix error when property does not exist on type', () => {
    const files = {
      'app.ts': `
       import {Component, NgModule} from '@angular/core';

       @Component({
         templateUrl: './app.html'
       })
       export class AppComponent {
         title1 = '';
       }
     `,
      'app.html': `{{title}}`,
    };

    const project = createModuleAndProjectWithDeclarations(env, 'test', files);
    const diags = project.getDiagnosticsForFile('app.html');
    const appFile = project.openFile('app.html');
    appFile.moveCursorToText('title¦');
    const codeActions = project.getCodeFixesAtPosition('app.html', appFile.cursor, appFile.cursor, [
      diags[0].code,
    ]);
    expectIncludeReplacementText({
      codeActions,
      content: appFile.contents,
      text: 'title',
      newText: 'title1',
      fileName: 'app.html',
    });

    const appTsFile = project.openFile('app.ts');
    appTsFile.moveCursorToText(`title1 = '';\n¦`);
    expectIncludeAddText({
      codeActions,
      position: appTsFile.cursor,
      text: 'title: any;\n',
      fileName: 'app.ts',
    });
  });

  it('should fix a missing method when property does not exist on type', () => {
    const files = {
      'app.ts': `
       import {Component, NgModule} from '@angular/core';

       @Component({
         templateUrl: './app.html'
       })
       export class AppComponent {
       }
     `,
      'app.html': `{{title('Angular')}}`,
    };

    const project = createModuleAndProjectWithDeclarations(env, 'test', files);
    const diags = project.getDiagnosticsForFile('app.html');
    const appFile = project.openFile('app.html');
    appFile.moveCursorToText('title¦');
    const codeActions = project.getCodeFixesAtPosition('app.html', appFile.cursor, appFile.cursor, [
      diags[0].code,
    ]);

    const appTsFile = project.openFile('app.ts');
    appTsFile.moveCursorToText(`class AppComponent {¦`);
    expectIncludeAddText({
      codeActions,
      position: appTsFile.cursor,
      text: `\ntitle(arg0: string) {\nthrow new Error('Method not implemented.');\n}`,
      fileName: 'app.ts',
    });
  });

  it('should not show fix all errors when there is only one diagnostic in the template but has two or more diagnostics in TCB', () => {
    const files = {
      'app.ts': `
         import {Component, NgModule} from '@angular/core';

         @Component({
           templateUrl: './app.html'
         })
         export class AppComponent {
           title1 = '';
         }
       `,
      'app.html': `<div *ngIf="title" />`,
    };

    const project = createModuleAndProjectWithDeclarations(env, 'test', files);
    const diags = project.getDiagnosticsForFile('app.html');
    const appFile = project.openFile('app.html');
    appFile.moveCursorToText('title¦');
    const codeActions = project.getCodeFixesAtPosition('app.html', appFile.cursor, appFile.cursor, [
      diags[0].code,
    ]);
    expectNotIncludeFixAllInfo(codeActions);
  });

  it('should fix all errors when property does not exist on type', () => {
    const files = {
      'app.ts': `
       import {Component, NgModule} from '@angular/core';

       @Component({
         template: '{{tite}}{{bannr}}',
       })
       export class AppComponent {
         title = '';
         banner = '';
       }
     `,
    };

    const project = createModuleAndProjectWithDeclarations(env, 'test', files);
    const appFile = project.openFile('app.ts');

    const fixesAllSpelling = project.getCombinedCodeFix('app.ts', 'fixSpelling' as string);
    expectIncludeReplacementTextForFileTextChange({
      fileTextChanges: fixesAllSpelling.changes,
      content: appFile.contents,
      text: 'tite',
      newText: 'title',
      fileName: 'app.ts',
    });
    expectIncludeReplacementTextForFileTextChange({
      fileTextChanges: fixesAllSpelling.changes,
      content: appFile.contents,
      text: 'bannr',
      newText: 'banner',
      fileName: 'app.ts',
    });

    const fixAllMissingMember = project.getCombinedCodeFix('app.ts', 'fixMissingMember' as string);
    appFile.moveCursorToText(`banner = '';\n¦`);
    expectIncludeAddTextForFileTextChange({
      fileTextChanges: fixAllMissingMember.changes,
      position: appFile.cursor,
      text: 'tite: any;\n',
      fileName: 'app.ts',
    });
    expectIncludeAddTextForFileTextChange({
      fileTextChanges: fixAllMissingMember.changes,
      position: appFile.cursor,
      text: 'bannr: any;\n',
      fileName: 'app.ts',
    });
  });

  it('should fix invalid banana-in-box error', () => {
    const files = {
      'app.ts': `
       import {Component, NgModule} from '@angular/core';

       @Component({
         templateUrl: './app.html'
       })
       export class AppComponent {
         title = '';
       }
     `,
      'app.html': `<input ([ngModel])="title">`,
    };

    const project = createModuleAndProjectWithDeclarations(env, 'test', files);
    const diags = project.getDiagnosticsForFile('app.html');
    const appFile = project.openFile('app.html');
    appFile.moveCursorToText('¦([ngModel');

    const codeActions = project.getCodeFixesAtPosition('app.html', appFile.cursor, appFile.cursor, [
      diags[0].code,
    ]);
    expectIncludeReplacementText({
      codeActions,
      content: appFile.contents,
      text: `([ngModel])="title"`,
      newText: `[(ngModel)]="title"`,
      fileName: 'app.html',
      description: `fix invalid banana-in-box for '([ngModel])="title"'`,
    });
  });

  it('should fix all invalid banana-in-box errors', () => {
    const files = {
      'app.ts': `
       import {Component, NgModule} from '@angular/core';

       @Component({
         template: '<input ([ngModel])="title"><input ([value])="title">',
       })
       export class AppComponent {
         title = '';
         banner = '';
       }
     `,
    };

    const project = createModuleAndProjectWithDeclarations(env, 'test', files);
    const appFile = project.openFile('app.ts');

    const fixesAllActions = project.getCombinedCodeFix(
      'app.ts',
      FixIdForCodeFixesAll.FIX_INVALID_BANANA_IN_BOX,
    );
    expectIncludeReplacementTextForFileTextChange({
      fileTextChanges: fixesAllActions.changes,
      content: appFile.contents,
      text: `([ngModel])="title"`,
      newText: `[(ngModel)]="title"`,
      fileName: 'app.ts',
    });
    expectIncludeReplacementTextForFileTextChange({
      fileTextChanges: fixesAllActions.changes,
      content: appFile.contents,
      text: `([value])="title"`,
      newText: `[(value)]="title"`,
      fileName: 'app.ts',
    });
  });

  describe('should fix missing required input', () => {
    it('for different type', () => {
      const files = {
        'app.ts': `
         import {Component} from '@angular/core';

         @Component({
           template: '<foo />',
           standalone: false,
         })
         export class AppComponent {
           title = '';
           banner = '';
         }
       `,
        'foo.ts': `
        import {Component, Input} from '@angular/core';

        @Component({
          selector: 'foo',
          template: '',
          standalone: false,
        })
        export class Foo {
          @Input({required: true}) name: string = "";
          @Input({required: true}) isShow = false;
          @Input({required: true}) product: {name?: string} = {};
        }
       `,
      };

      const project = createModuleAndProjectWithDeclarations(env, 'test', files);
      const appFile = project.openFile('app.ts');
      const diags = project.getDiagnosticsForFile('app.ts');
      appFile.moveCursorToText('<foo¦');

      const codeActions = project.getCodeFixesAtPosition('app.ts', appFile.cursor, appFile.cursor, [
        diags[0].code,
      ]);

      expectIncludeAddText({
        codeActions,
        position: appFile.cursor,
        text: ` [name]=""`,
        fileName: 'app.ts',
      });
      expectIncludeAddText({
        codeActions,
        position: appFile.cursor,
        text: ` [product]=""`,
        fileName: 'app.ts',
      });
      expectIncludeAddText({
        codeActions,
        position: appFile.cursor,
        text: ` [isShow]=""`,
        fileName: 'app.ts',
      });
    });
    it('for signal inputs', () => {
      const files = {
        'app.ts': `
         import {Component} from '@angular/core';

         @Component({
           template: '<foo />',
           standalone: false,
         })
         export class AppComponent {
           title = '';
           banner = '';
         }
       `,
        'foo.ts': `
        import {Component, input} from '@angular/core';

        @Component({
          selector: 'foo',
          template: '',
          standalone: false,
        })
        export class Foo {
          name = input.required<string>();
        }
       `,
      };

      const project = createModuleAndProjectWithDeclarations(env, 'test', files);
      const appFile = project.openFile('app.ts');
      const diags = project.getDiagnosticsForFile('app.ts');
      appFile.moveCursorToText('<foo¦');

      const codeActions = project.getCodeFixesAtPosition('app.ts', appFile.cursor, appFile.cursor, [
        diags[0].code,
      ]);

      expectIncludeAddText({
        codeActions,
        position: appFile.cursor,
        text: ` [name]=""`,
        fileName: 'app.ts',
      });
    });
    it('for the structural directives', () => {
      const files = {
        'app.ts': `
         import {Component} from '@angular/core';

         @Component({
           template: '<foo *ngFor="" />',
           standalone: false,
         })
         export class AppComponent {
           title = '';
           banner = '';
         }
       `,
        'foo.ts': `
        import {Component, Input} from '@angular/core';

        @Component({
          selector: 'foo',
          template: '',
          standalone: false,
        })
        export class Foo {
          @Input({required: true}) name: string = "";
        }
       `,
      };

      const project = createModuleAndProjectWithDeclarations(env, 'test', files);
      const appFile = project.openFile('app.ts');
      const diags = project.getDiagnosticsForFile('app.ts');
      appFile.moveCursorToText('<foo¦');

      const codeActions = project.getCodeFixesAtPosition('app.ts', appFile.cursor, appFile.cursor, [
        diags[0].code,
      ]);

      expectIncludeAddText({
        codeActions,
        position: appFile.cursor,
        /**
         * <foo [name]="" *ngFor="" />
         * TODO: This should be <foo *ngFor="" [name]="" />
         */
        text: ` [name]=""`,
        fileName: 'app.ts',
      });
    });
    it('for the alias input name', () => {
      const files = {
        'app.ts': `
         import {Component} from '@angular/core';

         @Component({
           template: '<foo />',
           standalone: false,
         })
         export class AppComponent {
           title = '';
           banner = '';
         }
       `,
        'foo.ts': `
        import {Component, Input} from '@angular/core';

        @Component({
          selector: 'foo',
          template: '',
          standalone: false,
        })
        export class Foo {
          @Input({required: true, alias: "name"}) _name: string = "";
        }
       `,
      };

      const project = createModuleAndProjectWithDeclarations(env, 'test', files);
      const appFile = project.openFile('app.ts');
      const diags = project.getDiagnosticsForFile('app.ts');
      appFile.moveCursorToText('<foo¦');

      const codeActions = project.getCodeFixesAtPosition('app.ts', appFile.cursor, appFile.cursor, [
        diags[0].code,
      ]);

      expectIncludeAddText({
        codeActions,
        position: appFile.cursor,
        text: ` [name]=""`,
        fileName: 'app.ts',
      });
    });
    it('and insert the attribute after the text interpolations', () => {
      const files = {
        'app.ts': `
         import {Component} from '@angular/core';

         @Component({
           template: '<foo class="selected" />',
           standalone: false,
         })
         export class AppComponent {
           title = '';
           banner = '';
         }
       `,
        'foo.ts': `
        import {Component, Input} from '@angular/core';

        @Component({
          selector: 'foo',
          template: '',
          standalone: false,
        })
        export class Foo {
          @Input({required: true}) name: string = "";
        }
       `,
      };

      const project = createModuleAndProjectWithDeclarations(env, 'test', files);
      const appFile = project.openFile('app.ts');
      const diags = project.getDiagnosticsForFile('app.ts');
      appFile.moveCursorToText('¦<foo');

      const codeActions = project.getCodeFixesAtPosition('app.ts', appFile.cursor, appFile.cursor, [
        diags[0].code,
      ]);

      appFile.moveCursorToText('class="selected"¦');
      expectIncludeAddText({
        codeActions,
        position: appFile.cursor,
        text: ` [name]=""`,
        fileName: 'app.ts',
      });
    });
    it('and insert the attribute after the property binding', () => {
      const files = {
        'app.ts': `
         import {Component} from '@angular/core';

         @Component({
           template: '<foo [isShow]="show" />',
           standalone: false,
         })
         export class AppComponent {
           show = true;
         }
       `,
        'foo.ts': `
        import {Component, Input} from '@angular/core';

        @Component({
          selector: 'foo',
          template: '',
          standalone: false,
        })
        export class Foo {
          @Input({required: true}) name: string = "";
          @Input({required: true}) isShow = true;
        }
       `,
      };

      const project = createModuleAndProjectWithDeclarations(env, 'test', files);
      const appFile = project.openFile('app.ts');
      const diags = project.getDiagnosticsForFile('app.ts');
      appFile.moveCursorToText('¦<foo');

      const codeActions = project.getCodeFixesAtPosition('app.ts', appFile.cursor, appFile.cursor, [
        diags[0].code,
      ]);

      appFile.moveCursorToText('[isShow]="show"¦');
      expectIncludeAddText({
        codeActions,
        position: appFile.cursor,
        text: ` [name]=""`,
        fileName: 'app.ts',
      });
    });
    it('and insert the attribute after the output', () => {
      const files = {
        'app.ts': `
         import {Component} from '@angular/core';

         @Component({
           template: '<foo (click)="" />',
           standalone: false,
         })
         export class AppComponent {
           title = '';
           banner = '';
         }
       `,
        'foo.ts': `
        import {Component, Input} from '@angular/core';

        @Component({
          selector: 'foo',
          template: '',
          standalone: false,
        })
        export class Foo {
          @Input({required: true}) name: string = "";
        }
       `,
      };

      const project = createModuleAndProjectWithDeclarations(env, 'test', files);
      const appFile = project.openFile('app.ts');
      const diags = project.getDiagnosticsForFile('app.ts');
      appFile.moveCursorToText('¦<foo');

      const codeActions = project.getCodeFixesAtPosition('app.ts', appFile.cursor, appFile.cursor, [
        diags[0].code,
      ]);

      appFile.moveCursorToText('(click)=""¦');
      expectIncludeAddText({
        codeActions,
        position: appFile.cursor,
        text: ` [name]=""`,
        fileName: 'app.ts',
      });
    });
  });

  describe('should fix missing selector imports', () => {
    it('for a new standalone component import', () => {
      const standaloneFiles = {
        'foo.ts': `
         import {Component} from '@angular/core';
         @Component({
           selector: 'foo',
           template: '<bar></bar>'
         })
         export class FooComponent {}
         `,
        'bar.ts': `
         import {Component} from '@angular/core';
         @Component({
           selector: 'bar',
           template: '<div>bar</div>'
         })
         export class BarComponent {}
         `,
      };

      const project = createModuleAndProjectWithDeclarations(env, 'test', {}, {}, standaloneFiles);
      const diags = project.getDiagnosticsForFile('foo.ts');
      const fixFile = project.openFile('foo.ts');
      fixFile.moveCursorToText('<¦bar>');

      const codeActions = project.getCodeFixesAtPosition('foo.ts', fixFile.cursor, fixFile.cursor, [
        diags[0].code,
      ]);
      const actionChanges = allChangesForCodeActions(fixFile.contents, codeActions);
      actionChangesMatch(actionChanges, `Import BarComponent from './bar' on FooComponent`, [
        [``, `import { BarComponent } from "./bar";`],
        [``, `, imports: [BarComponent]`],
      ]);
    });

    it('for a new NgModule-based component import', () => {
      const standaloneFiles = {
        'foo.ts': `
         import {Component} from '@angular/core';
         @Component({
           selector: 'foo',
           template: '<bar></bar>'
         })
         export class FooComponent {}
         `,
        'bar.ts': `
         import {Component, NgModule} from '@angular/core';
         @Component({
           selector: 'bar',
           template: '<div>bar</div>',
           standalone: false,
         })
         export class BarComponent {}
         @NgModule({
           declarations: [BarComponent],
           exports: [BarComponent],
           imports: []
         })
         export class BarModule {}
         `,
      };

      const project = createModuleAndProjectWithDeclarations(env, 'test', {}, {}, standaloneFiles);
      const diags = project.getDiagnosticsForFile('foo.ts');
      const fixFile = project.openFile('foo.ts');
      fixFile.moveCursorToText('<¦bar>');

      const codeActions = project.getCodeFixesAtPosition('foo.ts', fixFile.cursor, fixFile.cursor, [
        diags[0].code,
      ]);
      const actionChanges = allChangesForCodeActions(fixFile.contents, codeActions);
      actionChangesMatch(actionChanges, `Import BarModule from './bar' on FooComponent`, [
        [``, `import { BarModule } from "./bar";`],
        [``, `, imports: [BarModule]`],
      ]);
    });

    it('for an import of a component onto an ngModule', () => {
      const standaloneFiles = {
        'foo.ts': `
         import {Component, NgModule} from '@angular/core';
         @Component({
           selector: 'foo',
           template: '<bar></bar>',
           standalone: false,
         })
         export class FooComponent {}
         @NgModule({
           declarations: [FooComponent],
           exports: [],
           imports: []
         })
         export class FooModule {}
         `,
        'bar.ts': `
         import {Component} from '@angular/core';
         @Component({
           selector: 'bar',
           template: '<div>bar</div>',
          })
         export class BarComponent {}
         `,
      };

      const project = createModuleAndProjectWithDeclarations(env, 'test', {}, {}, standaloneFiles);
      const diags = project.getDiagnosticsForFile('foo.ts');
      const fixFile = project.openFile('foo.ts');
      fixFile.moveCursorToText('<¦bar>');

      const codeActions = project.getCodeFixesAtPosition('foo.ts', fixFile.cursor, fixFile.cursor, [
        diags[0].code,
      ]);
      const actionChanges = allChangesForCodeActions(fixFile.contents, codeActions);
      actionChangesMatch(actionChanges, `Import BarComponent from './bar' on FooModule`, [
        [``, `import { BarComponent } from "./bar";`],
        [`imports: []`, `imports: [BarComponent]`],
      ]);
    });

    it('for a new standalone pipe import', () => {
      const standaloneFiles = {
        'foo.ts': `
        import {Component} from '@angular/core';
        @Component({
          selector: 'foo',
          template: '{{"hello"|bar}}'
        })
        export class FooComponent {}
        `,
        'bar.ts': `
        import {Pipe} from '@angular/core';
        @Pipe({
          name: 'bar'
        })
        export class BarPipe implements PipeTransform {
          transform(value: unknown, ...args: unknown[]): unknown {
            return null;
          }
        }
        `,
      };

      const project = createModuleAndProjectWithDeclarations(env, 'test', {}, {}, standaloneFiles);
      const diags = project.getDiagnosticsForFile('foo.ts');
      const fixFile = project.openFile('foo.ts');
      fixFile.moveCursorToText('"hello"|b¦ar');

      const codeActions = project.getCodeFixesAtPosition('foo.ts', fixFile.cursor, fixFile.cursor, [
        diags[0].code,
      ]);
      const actionChanges = allChangesForCodeActions(fixFile.contents, codeActions);

      actionChangesMatch(actionChanges, `Import BarPipe from './bar' on FooComponent`, [
        [``, `import { BarPipe } from "./bar";`],
        ['', `, imports: [BarPipe]`],
      ]);
    });

    it('for a transitive NgModule-based reexport', () => {
      const standaloneFiles = {
        'foo.ts': `
         import {Component} from '@angular/core';
         @Component({
           selector: 'foo',
           template: '<bar></bar>'
         })
         export class FooComponent {}
         `,
        'bar.ts': `
         import {Component, NgModule} from '@angular/core';
         @Component({
           selector: 'bar',
           template: '<div>bar</div>',
         })
         export class BarComponent {}
         @NgModule({
           declarations: [BarComponent],
           exports: [BarComponent],
           imports: []
         })
         export class BarModule {}
         @NgModule({
          declarations: [],
          exports: [BarModule],
          imports: []
        })
        export class Bar2Module {}
        `,
      };

      const project = createModuleAndProjectWithDeclarations(env, 'test', {}, {}, standaloneFiles);
      const diags = project.getDiagnosticsForFile('foo.ts');
      const fixFile = project.openFile('foo.ts');
      fixFile.moveCursorToText('<¦bar>');

      const codeActions = project.getCodeFixesAtPosition('foo.ts', fixFile.cursor, fixFile.cursor, [
        diags[0].code,
      ]);
      const actionChanges = allChangesForCodeActions(fixFile.contents, codeActions);
      actionChangesMatch(actionChanges, `Import BarModule from './bar' on FooComponent`, [
        [``, `import { BarModule } from "./bar";`],
        [``, `, imports: [BarModule]`],
      ]);
      actionChangesMatch(actionChanges, `Import Bar2Module from './bar' on FooComponent`, [
        [``, `import { Bar2Module } from "./bar";`],
        [``, `, imports: [Bar2Module]`],
      ]);
    });

    it('for a default exported component', () => {
      const standaloneFiles = {
        'foo.ts': `
         import {Component} from '@angular/core';
         @Component({
           selector: 'foo',
           template: '<bar></bar>'
         })
         export class FooComponent {}
         `,
        'bar.ts': `
         import {Component} from '@angular/core';
         @Component({
           selector: 'bar',
           template: '<div>bar</div>'
         })
         class BarComponent {}
         export default BarComponent;
         `,
      };

      const project = createModuleAndProjectWithDeclarations(env, 'test', {}, {}, standaloneFiles);
      const diags = project.getDiagnosticsForFile('foo.ts');
      const fixFile = project.openFile('foo.ts');
      fixFile.moveCursorToText('<¦bar>');

      const codeActions = project.getCodeFixesAtPosition('foo.ts', fixFile.cursor, fixFile.cursor, [
        diags[0].code,
      ]);
      const actionChanges = allChangesForCodeActions(fixFile.contents, codeActions);
      actionChangesMatch(actionChanges, `Import BarComponent from './bar' on FooComponent`, [
        [``, `import BarComponent from "./bar";`],
        [``, `, imports: [BarComponent]`],
      ]);
    });

    it('for a default exported component and reuse the existing import declarations', () => {
      const standaloneFiles = {
        'foo.ts': `
         import {Component} from '@angular/core';
         import {test} from './bar';
         @Component({
           selector: 'foo',
           template: '<bar></bar>'
         })
         export class FooComponent {}
         `,
        'bar.ts': `
         import {Component} from '@angular/core';
         @Component({
           selector: 'bar',
           template: '<div>bar</div>'
         })
         class BarComponent {}
         export default BarComponent;
         export const test = 1;
         `,
      };

      const project = createModuleAndProjectWithDeclarations(env, 'test', {}, {}, standaloneFiles);
      const diags = project.getDiagnosticsForFile('foo.ts');
      const fixFile = project.openFile('foo.ts');
      fixFile.moveCursorToText('<¦bar>');

      const codeActions = project.getCodeFixesAtPosition('foo.ts', fixFile.cursor, fixFile.cursor, [
        diags[0].code,
      ]);
      const actionChanges = allChangesForCodeActions(fixFile.contents, codeActions);
      actionChangesMatch(actionChanges, `Import BarComponent from './bar' on FooComponent`, [
        [`{test}`, `BarComponent, { test }`],
        [``, `, imports: [BarComponent]`],
      ]);
    });

    it('for a default exported component and reuse the existing imported component name', () => {
      const standaloneFiles = {
        'foo.ts': `
         import {Component} from '@angular/core';
         import NewBarComponent, {test} from './bar';
         @Component({
           selector: 'foo',
           template: '<bar></bar>'
         })
         export class FooComponent {}
         `,
        'bar.ts': `
         import {Component} from '@angular/core';
         @Component({
           selector: 'bar',
           template: '<div>bar</div>'
         })
         class BarComponent {}
         export default BarComponent;
         export const test = 1;
         `,
      };

      const project = createModuleAndProjectWithDeclarations(env, 'test', {}, {}, standaloneFiles);
      const diags = project.getDiagnosticsForFile('foo.ts');
      const fixFile = project.openFile('foo.ts');
      fixFile.moveCursorToText('<¦bar>');

      const codeActions = project.getCodeFixesAtPosition('foo.ts', fixFile.cursor, fixFile.cursor, [
        diags[0].code,
      ]);
      const actionChanges = allChangesForCodeActions(fixFile.contents, codeActions);
      actionChangesMatch(actionChanges, `Import NewBarComponent from './bar' on FooComponent`, [
        [``, `, imports: [NewBarComponent]`],
      ]);
    });

    it('for forward references in the same file', () => {
      const standaloneFiles = {
        'foo.ts': `
          import {Component} from '@angular/core';

          @Component({
                        selector: 'one-cmp',
            template: '<two-cmp></two-cmp>',
          })
          export class OneCmp {}

          @Component({
                        selector: 'two-cmp',
            template: '<div></div>',
          })
          export class TwoCmp {}
         `,
      };

      const project = createModuleAndProjectWithDeclarations(env, 'test', {}, {}, standaloneFiles);
      const diags = project.getDiagnosticsForFile('foo.ts');
      const fixFile = project.openFile('foo.ts');
      fixFile.moveCursorToText('<¦two-cmp>');

      const codeActions = project.getCodeFixesAtPosition('foo.ts', fixFile.cursor, fixFile.cursor, [
        diags[0].code,
      ]);
      const actionChanges = allChangesForCodeActions(fixFile.contents, codeActions);
      actionChangesMatch(actionChanges, `Import TwoCmp`, [
        [`{Component}`, `{ Component, forwardRef }`],
        [``, `, imports: [forwardRef(() => TwoCmp)]`],
      ]);
    });

    it('for an exported component from the node_modules', () => {
      const standaloneFiles = {
        'foo.ts': `
         import {Component} from '@angular/core';
         @Component({
           selector: 'foo',
           template: '<mat-card></mat-card>'
         })
         export class FooComponent {}
         `,
        'bar.ts': `
         // make sure the @angular/common is found by the project
         import {} from '@angular/common';
         `,
      };

      const project = createModuleAndProjectWithDeclarations(env, 'test', {}, {}, standaloneFiles);
      const diags = project.getDiagnosticsForFile('foo.ts');
      const fixFile = project.openFile('foo.ts');
      fixFile.moveCursorToText('<¦mat-card>');

      const codeActions = project.getCodeFixesAtPosition('foo.ts', fixFile.cursor, fixFile.cursor, [
        diags[0].code,
      ]);
      const actionChanges = allChangesForCodeActions(fixFile.contents, codeActions);
      actionChangesMatch(actionChanges, `Import MatCard from '@angular/common' on FooComponent`, [
        [``, `import { MatCard } from "@angular/common";`],
        [``, `, imports: [MatCard]`],
      ]);
    });

    it('for a path from the tsconfig', () => {
      const standaloneFiles = {
        'src/foo.ts': `
           import {Component} from '@angular/core';
           @Component({
             selector: 'foo',
             template: '<bar></bar>'
           })
           export class FooComponent {}
           `,
        'component/share/bar.ts': `
           import {Component} from '@angular/core';
           @Component({
             selector: 'bar',
             template: '<div>bar</div>'
           })
           export class BarComponent {}
           `,
      };

      const project = createModuleAndProjectWithDeclarations(env, 'test', {}, {}, standaloneFiles, {
        paths: {'@app/*': ['./component/share/*.ts']},
      });
      const diags = project.getDiagnosticsForFile('src/foo.ts');
      const fixFile = project.openFile('src/foo.ts');
      fixFile.moveCursorToText('<¦bar>');

      const codeActions = project.getCodeFixesAtPosition(
        'src/foo.ts',
        fixFile.cursor,
        fixFile.cursor,
        [diags[0].code],
      );
      const actionChanges = allChangesForCodeActions(fixFile.contents, codeActions);
      actionChangesMatch(actionChanges, `Import BarComponent from '@app/bar' on FooComponent`, [
        [``, `import { BarComponent } from "@app/bar";`],
        [``, `, imports: [BarComponent]`],
      ]);
    });

    it('for a re-export symbol from the tsconfig path', () => {
      const standaloneFiles = {
        'src/foo.ts': `
           import {Component} from '@angular/core';
           @Component({
             selector: 'foo',
             template: '<bar></bar>'
           })
           export class FooComponent {}
           `,
        'component/share/bar.ts': `
           import {Component} from '@angular/core';
           @Component({
             selector: 'bar',
             template: '<div>bar</div>'
           })
           export class BarComponent {}
           `,
        'component/share/re_export.ts': `
            import {BarComponent as NewBarComponent1} from "./bar"
            export {NewBarComponent1}
           `,
        'component/share/public_api.ts': `
            export {NewBarComponent1 as NewBarComponent2} from "./re_export"
           `,
        'component/share/index.ts': `
            export {NewBarComponent2 as NewBarComponent3} from "./public_api"
           `,
      };

      const project = createModuleAndProjectWithDeclarations(env, 'test', {}, {}, standaloneFiles, {
        paths: {'@app/*': ['./component/share/*.ts']},
      });
      const diags = project.getDiagnosticsForFile('src/foo.ts');
      const fixFile = project.openFile('src/foo.ts');
      fixFile.moveCursorToText('<¦bar>');

      const codeActions = project.getCodeFixesAtPosition(
        'src/foo.ts',
        fixFile.cursor,
        fixFile.cursor,
        [diags[0].code],
      );
      const actionChanges = allChangesForCodeActions(fixFile.contents, codeActions);
      actionChangesMatch(
        actionChanges,
        `Import NewBarComponent3 from '@app/index' on FooComponent`,
        [
          [``, `import { NewBarComponent3 } from "@app/index";`],
          [``, `, imports: [NewBarComponent3]`],
        ],
      );
    });

    it('for a reusable path from the tsconfig', () => {
      const standaloneFiles = {
        'src/foo.ts': `
           import {Component} from '@angular/core';
           import {BazComponent} from '@app/bar';
           @Component({
             selector: 'foo',
             template: '<bar></bar><baz/>',
             imports: [BazComponent]
           })
           export class FooComponent {}
           `,
        'component/share/bar.ts': `
           import {Component} from '@angular/core';
           @Component({
             selector: 'bar',
             template: '<div>bar</div>',
           })
           export class BarComponent {}

           @Component({
             selector: 'baz',
             template: '<div>baz</div>',
           })
           export class BazComponent {}
           `,
      };

      const project = createModuleAndProjectWithDeclarations(env, 'test', {}, {}, standaloneFiles, {
        paths: {'@app/*': ['./component/share/*.ts']},
      });
      const diags = project.getDiagnosticsForFile('src/foo.ts');
      const fixFile = project.openFile('src/foo.ts');
      fixFile.moveCursorToText('<¦bar>');

      const codeActions = project.getCodeFixesAtPosition(
        'src/foo.ts',
        fixFile.cursor,
        fixFile.cursor,
        [diags[0].code],
      );
      const actionChanges = allChangesForCodeActions(fixFile.contents, codeActions);
      actionChangesMatch(actionChanges, `Import BarComponent from '@app/bar' on FooComponent`, [
        [`{BazComponent}`, `{ BazComponent, BarComponent }`],
        [`imports: [BazComponent]`, `imports: [BazComponent, BarComponent]`],
      ]);
    });

    it('for a reusable path with name export from the tsconfig', () => {
      const standaloneFiles = {
        'src/foo.ts': `
           import {Component} from '@angular/core';
           import {BazComponent} from '@app/bar';
           @Component({
             selector: 'foo',
             template: '<bar></bar><baz/>',
             imports: [BazComponent]
           })
           export class FooComponent {}
           `,
        'component/share/bar.ts': `
           import {Component} from '@angular/core';
           @Component({
             selector: 'bar',
             template: '<div>bar</div>',
           })
           class BarComponent {}

           @Component({
             selector: 'baz',
             template: '<div>baz</div>',
           })
           class BazComponent {}

           export {BarComponent, BazComponent};
           `,
      };

      const project = createModuleAndProjectWithDeclarations(env, 'test', {}, {}, standaloneFiles, {
        paths: {'@app/*': ['./component/share/*.ts']},
      });
      const diags = project.getDiagnosticsForFile('src/foo.ts');
      const fixFile = project.openFile('src/foo.ts');
      fixFile.moveCursorToText('<¦bar>');

      const codeActions = project.getCodeFixesAtPosition(
        'src/foo.ts',
        fixFile.cursor,
        fixFile.cursor,
        [diags[0].code],
      );
      const actionChanges = allChangesForCodeActions(fixFile.contents, codeActions);
      actionChangesMatch(actionChanges, `Import BarComponent from '@app/bar' on FooComponent`, [
        [`{BazComponent}`, `{ BazComponent, BarComponent }`],
        [`imports: [BazComponent]`, `imports: [BazComponent, BarComponent]`],
      ]);
    });

    it('for module specifier existing in the file', () => {
      const standaloneFiles = {
        'src/foo.ts': `
           import {Component} from '@angular/core';
           import { } from "../component/share/bar";

           @Component({
             selector: 'foo',
             template: '<bar></bar>'
           })
           export class FooComponent {}
           `,
        'component/share/bar.ts': `
           import {Component} from '@angular/core';

           @Component({
             selector: 'bar',
             template: '<div>bar</div>',
             standalone: false
           })
           export class BarComponent {}
           `,
        'component/share/bar.module.ts': `
            import {NgModule} from '@angular/core';
            import {BarComponent} from './bar';

            @NgModule({
              declarations: [BarComponent],
              exports: [BarComponent],
              imports: []
            })
            export class BarModule {}
            `,
      };

      const project = createModuleAndProjectWithDeclarations(env, 'test', {}, {}, standaloneFiles);
      const diags = project.getDiagnosticsForFile('src/foo.ts');
      const fixFile = project.openFile('src/foo.ts');
      fixFile.moveCursorToText('<¦bar>');

      const codeActions = project.getCodeFixesAtPosition(
        'src/foo.ts',
        fixFile.cursor,
        fixFile.cursor,
        [diags[0].code],
      );

      const actionChanges = allChangesForCodeActions(fixFile.contents, codeActions);
      actionChangesMatch(
        actionChanges,
        `Import BarModule from '../component/share/bar.module' on FooComponent`,
        [
          [``, `import { BarModule } from "../component/share/bar.module";`],
          [``, `, imports: [BarModule]`],
        ],
      );
    });

    it('for NgModule and Component existing in the different file', () => {
      const standaloneFiles = {
        'src/foo/foo.ts': `
           import {Component} from '@angular/core';

           @Component({
             selector: 'foo',
             template: '<bar></bar>',
             standalone: false
           })
           export class FooComponent {}
           `,

        'src/bar/index.ts': `
           import {Component} from '@angular/core';

           @Component({
             selector: 'bar',
             template: '<bar></bar>',
           })
           export class BarComponent {}
           `,

        'src/app.ts': `
           import {NgModule} from '@angular/core';
           import {FooComponent} from "./foo/foo";

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

      const project = createModuleAndProjectWithDeclarations(env, 'test', {}, {}, standaloneFiles);
      const diags = project.getDiagnosticsForFile('src/foo/foo.ts');
      const fixFile = project.openFile('src/foo/foo.ts');
      fixFile.moveCursorToText('<¦bar>');

      const codeActions = project.getCodeFixesAtPosition(
        'src/foo/foo.ts',
        fixFile.cursor,
        fixFile.cursor,
        [diags[0].code],
      );

      const appModuleContents = project.openFile('src/app.ts').contents;

      const actionChanges = allChangesForCodeActions(appModuleContents, codeActions);
      actionChangesMatch(actionChanges, `Import BarComponent from './bar' on AppModule`, [
        [``, `import { BarComponent } from "./bar";`],
        [`imports: []`, `imports: [BarComponent]`],
      ]);
    });
  });

  describe('unused standalone imports', () => {
    it('should fix single diagnostic about individual imports that are not used', () => {
      const files = {
        'app.ts': `
         import {Component, Directive} from '@angular/core';

         @Directive({selector: '[used]'})
         export class UsedDirective {}

         @Directive({selector: '[unused]'})
         export class UnusedDirective {}

         @Component({
           template: '<span used></span>',
                      imports: [UnusedDirective, UsedDirective],
         })
         export class AppComponent {}
       `,
      };

      const project = createModuleAndProjectWithDeclarations(env, 'test', files);
      const fixFile = project.openFile('app.ts');
      fixFile.moveCursorToText('Unused¦Directive,');

      const diags = project.getDiagnosticsForFile('app.ts');
      const codeActions = project.getCodeFixesAtPosition('app.ts', fixFile.cursor, fixFile.cursor, [
        diags[0].code,
      ]);
      const actionChanges = allChangesForCodeActions(fixFile.contents, codeActions);

      actionChangesMatch(actionChanges, 'Remove unused import UnusedDirective', [
        ['[UnusedDirective, UsedDirective]', '[UsedDirective]'],
      ]);
    });

    it('should fix single diagnostic about all imports that are not used', () => {
      const files = {
        'app.ts': `
         import {Component, Directive, Pipe} from '@angular/core';

         @Directive({selector: '[unused]'})
         export class UnusedDirective {}

         @Pipe({name: 'unused'})
         export class UnusedPipe {}

         @Component({
           template: '',
                      imports: [UnusedDirective, UnusedPipe],
         })
         export class AppComponent {}
       `,
      };

      const project = createModuleAndProjectWithDeclarations(env, 'test', files);
      const fixFile = project.openFile('app.ts');
      fixFile.moveCursorToText('impo¦rts:');

      const diags = project.getDiagnosticsForFile('app.ts');
      const codeActions = project.getCodeFixesAtPosition('app.ts', fixFile.cursor, fixFile.cursor, [
        diags[0].code,
      ]);
      const actionChanges = allChangesForCodeActions(fixFile.contents, codeActions);

      actionChangesMatch(actionChanges, 'Remove all unused imports', [
        ['[UnusedDirective, UnusedPipe]', '[]'],
      ]);
    });

    it('should fix all imports arrays where some imports are not used', () => {
      const files = {
        'app.ts': `
         import {Component, Directive, Pipe} from '@angular/core';

         @Directive({selector: '[used]'})
         export class UsedDirective {}

         @Directive({selector: '[unused]'})
         export class UnusedDirective {}

         @Pipe({name: 'unused'})
         export class UnusedPipe {}

         @Component({
          selector: 'used-cmp',
                    template: '',
         })
         export class UsedComponent {}

         @Component({
           template: \`
            <section>
              <div></div>
              <span used></span>
              <div>
                <used-cmp/>
              </div>
            </section>
           \`,
                      imports: [UnusedDirective, UsedDirective, UnusedPipe, UsedComponent],
         })
         export class AppComponent {}
       `,
      };

      const project = createModuleAndProjectWithDeclarations(env, 'test', files);
      const appFile = project.openFile('app.ts');

      const fixesAllActions = project.getCombinedCodeFix(
        'app.ts',
        FixIdForCodeFixesAll.FIX_UNUSED_STANDALONE_IMPORTS,
      );
      expectIncludeReplacementTextForFileTextChange({
        fileTextChanges: fixesAllActions.changes,
        content: appFile.contents,
        text: '[UnusedDirective, UsedDirective, UnusedPipe, UsedComponent]',
        newText: '[UsedDirective, UsedComponent]',
        fileName: 'app.ts',
      });
    });

    it('should fix all imports arrays where all imports are not used', () => {
      const files = {
        'app.ts': `
         import {Component, Directive, Pipe} from '@angular/core';

         @Directive({selector: '[unused]'})
         export class UnusedDirective {}

         @Pipe({name: 'unused'})
         export class UnusedPipe {}

         @Component({
           template: '',
                      imports: [UnusedDirective, UnusedPipe],
         })
         export class AppComponent {}
       `,
      };

      const project = createModuleAndProjectWithDeclarations(env, 'test', files);
      const appFile = project.openFile('app.ts');

      const fixesAllActions = project.getCombinedCodeFix(
        'app.ts',
        FixIdForCodeFixesAll.FIX_UNUSED_STANDALONE_IMPORTS,
      );
      expectIncludeReplacementTextForFileTextChange({
        fileTextChanges: fixesAllActions.changes,
        content: appFile.contents,
        text: '[UnusedDirective, UnusedPipe]',
        newText: '[]',
        fileName: 'app.ts',
      });
    });
  });
});

type ActionChanges = {
  [description: string]: Array<readonly [string, string]>;
};

function actionChangesMatch(
  actionChanges: ActionChanges,
  description: string,
  substitutions: Array<readonly [string, string]>,
) {
  expect(Object.keys(actionChanges)).toContain(description);
  for (const substitution of substitutions) {
    expect(actionChanges[description]).toContain([substitution[0], substitution[1]]);
  }
}

// Returns the ActionChanges for all changes in the given code actions, collapsing whitespace into a
// single space and trimming at the ends.
function allChangesForCodeActions(
  fileContents: string,
  codeActions: readonly ts.CodeAction[],
): ActionChanges {
  // Replace all whitespace characters with a single space, then deduplicate spaces and trim.
  const collapse = (s: string) =>
    s
      .replace(/\s/g, ' ')
      .replace(/\s{2,}/g, ' ')
      .trim();
  let allActionChanges: ActionChanges = {};
  // For all code actions, construct a map from descriptions to [oldText, newText] pairs.
  for (const action of codeActions) {
    const actionChanges = action.changes.flatMap((change) => {
      return change.textChanges.map((tc) => {
        const oldText = collapse(fileContents.slice(tc.span.start, tc.span.start + tc.span.length));
        const newText = collapse(tc.newText);
        return [oldText, newText] as const;
      });
    });
    allActionChanges[collapse(action.description)] = actionChanges;
  }
  return allActionChanges;
}

function expectNotIncludeFixAllInfo(codeActions: readonly ts.CodeFixAction[]) {
  for (const codeAction of codeActions) {
    expect(codeAction.fixId).toBeUndefined();
    expect(codeAction.fixAllDescription).toBeUndefined();
  }
}

/**
 * The `description` is optional because if the description comes from the ts server, no need to
 * check it.
 */
function expectIncludeReplacementText({
  codeActions,
  content,
  text,
  newText,
  fileName,
  description,
}: {
  codeActions: readonly ts.CodeAction[];
  content: string;
  text: string | null;
  newText: string;
  fileName: string;
  description?: string;
}) {
  let includeReplacementText = false;
  for (const codeAction of codeActions) {
    includeReplacementText =
      includeReplacementTextInChanges({
        fileTextChanges: codeAction.changes,
        content,
        text,
        newText,
        fileName,
      }) && (description === undefined ? true : description === codeAction.description);
    if (includeReplacementText) {
      return;
    }
  }
  expect(includeReplacementText).toBeTruthy();
}

function expectIncludeAddText({
  codeActions,
  position,
  text,
  fileName,
}: {
  codeActions: readonly ts.CodeAction[];
  position: number | null;
  text: string;
  fileName: string;
}) {
  let includeAddText = false;
  for (const codeAction of codeActions) {
    includeAddText = includeAddTextInChanges({
      fileTextChanges: codeAction.changes,
      position,
      text,
      fileName,
    });
    if (includeAddText) {
      return;
    }
  }
  expect(includeAddText).toBeTruthy();
}

function expectIncludeReplacementTextForFileTextChange({
  fileTextChanges,
  content,
  text,
  newText,
  fileName,
}: {
  fileTextChanges: readonly ts.FileTextChanges[];
  content: string;
  text: string;
  newText: string;
  fileName: string;
}) {
  expect(
    includeReplacementTextInChanges({fileTextChanges, content, text, newText, fileName}),
  ).toBeTruthy();
}

function expectIncludeAddTextForFileTextChange({
  fileTextChanges,
  position,
  text,
  fileName,
}: {
  fileTextChanges: readonly ts.FileTextChanges[];
  position: number;
  text: string;
  fileName: string;
}) {
  expect(includeAddTextInChanges({fileTextChanges, position, text, fileName})).toBeTruthy();
}

function includeReplacementTextInChanges({
  fileTextChanges,
  content,
  text,
  newText,
  fileName,
}: {
  fileTextChanges: readonly ts.FileTextChanges[];
  content: string;
  text: string | null;
  newText: string;
  fileName: string;
}) {
  for (const change of fileTextChanges) {
    if (!change.fileName.endsWith(fileName)) {
      continue;
    }
    for (const textChange of change.textChanges) {
      if (textChange.span.length === 0) {
        continue;
      }
      const textChangeOldText = content.slice(
        textChange.span.start,
        textChange.span.start + textChange.span.length,
      );
      const oldTextMatches = text === null || textChangeOldText === text;
      const newTextMatches = newText === textChange.newText;
      if (oldTextMatches && newTextMatches) {
        return true;
      }
    }
  }
  return false;
}

function includeAddTextInChanges({
  fileTextChanges,
  position,
  text,
  fileName,
}: {
  fileTextChanges: readonly ts.FileTextChanges[];
  position: number | null;
  text: string;
  fileName: string;
}) {
  for (const change of fileTextChanges) {
    if (!change.fileName.endsWith(fileName)) {
      continue;
    }
    for (const textChange of change.textChanges) {
      if (textChange.span.length > 0) {
        continue;
      }
      const includeAddText =
        (position === null || position === textChange.span.start) && text === textChange.newText;
      if (includeAddText) {
        return true;
      }
    }
  }
  return false;
}
