/**
 * @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 {
  assertFileNames,
  assertTextSpans,
  createModuleAndProjectWithDeclarations,
  createProjectWithStandaloneDeclarations,
  humanizeDocumentSpanLike,
  LanguageServiceTestEnv,
  OpenBuffer,
  Project,
} from '../../testing';

describe('definitions', () => {
  it('gets definition for template reference in overridden template', () => {
    initMockFileSystem('Native');
    const files = {
      'app.html': '',
      'app.ts': `
         import {Component} from '@angular/core';

         @Component({
          templateUrl: '/app.html',
          standalone: false,
         })
         export class AppCmp {}
       `,
    };
    const env = LanguageServiceTestEnv.setup();

    const project = createModuleAndProjectWithDeclarations(env, 'test', files);
    const template = project.openFile('app.html');
    template.contents = '<input #myInput /> {{myInput.value}}';
    project.expectNoSourceDiagnostics();

    template.moveCursorToText('{{myIn¦put.value}}');
    const {definitions} = getDefinitionsAndAssertBoundSpan(env, template);
    expect(definitions![0].name).toEqual('myInput');
    assertFileNames(Array.from(definitions!), ['app.html']);
  });

  it('returns the pipe definitions when checkTypeOfPipes is false', () => {
    initMockFileSystem('Native');
    const files = {
      'app.ts': `
         import {Component, NgModule} from '@angular/core';
         import {CommonModule} from '@angular/common';

         @Component({
          templateUrl: 'app.html',
          standalone: false,
         })
         export class AppCmp {}
       `,
      'app.html': '{{"1/1/2020" | date}}',
    };
    // checkTypeOfPipes is set to false when strict templates is false
    const env = LanguageServiceTestEnv.setup();
    const project = createModuleAndProjectWithDeclarations(env, 'test', files, {
      strictTemplates: false,
    });
    const template = project.openFile('app.html');
    project.expectNoSourceDiagnostics();
    template.moveCursorToText('da¦te');

    const {textSpan, definitions} = getDefinitionsAndAssertBoundSpan(env, template);
    expect(template.contents.slice(textSpan.start, textSpan.start + textSpan.length)).toEqual(
      'date',
    );
    expect(definitions.length).toEqual(3);
    assertTextSpans(definitions, ['transform']);
    assertFileNames(definitions, ['fake_common.d.ts']);
  });

  it('gets definitions for all inputs when attribute matches more than one', () => {
    initMockFileSystem('Native');
    const files = {
      'app.ts': `
         import {Component, NgModule} from '@angular/core';
         import {CommonModule} from '@angular/common';

         @Component({
          templateUrl: 'app.html',
          standalone: false,
         })
         export class AppCmp {}
       `,
      'app.html': '<div dir inputA="abc"></div>',
      'dir.ts': `
       import {Directive, Input} from '@angular/core';

       @Directive({
        selector: '[dir]',
        standalone: false,
       })
       export class MyDir {
         @Input() inputA!: any;
       }`,
      'dir2.ts': `
       import {Directive, Input} from '@angular/core';

       @Directive({
        selector: '[dir]',
        standalone: false,
       })
       export class MyDir2 {
         @Input() inputA!: any;
       }`,
    };
    const env = LanguageServiceTestEnv.setup();
    const project = createModuleAndProjectWithDeclarations(env, 'test', files);
    const template = project.openFile('app.html');
    template.moveCursorToText('inpu¦tA="abc"');

    const {textSpan, definitions} = getDefinitionsAndAssertBoundSpan(env, template);
    expect(template.contents.slice(textSpan.start, textSpan.start + textSpan.length)).toEqual(
      'inputA',
    );

    expect(definitions!.length).toEqual(2);
    const [def, def2] = definitions!;
    expect(def.textSpan).toContain('inputA');
    expect(def2.textSpan).toContain('inputA');
    // TODO(atscott): investigate why the text span includes more than just 'inputA'
    // assertTextSpans([def, def2], ['inputA']);
    assertFileNames([def, def2], ['dir2.ts', 'dir.ts']);
  });

  it('gets definitions for all signal-inputs when attribute matches more than one', () => {
    initMockFileSystem('Native');
    const files = {
      'app.ts': `
         import {Component, NgModule} from '@angular/core';
         import {CommonModule} from '@angular/common';

         @Component({
          templateUrl: 'app.html',
          standalone: false,
         })
         export class AppCmp {}
       `,
      'app.html': '<div dir inputA="abc"></div>',
      'dir.ts': `
       import {Directive, input} from '@angular/core';

       @Directive({
        selector: '[dir]',
        standalone: false,
       })
       export class MyDir {
         inputA = input();
       }`,
      'dir2.ts': `
       import {Directive, input} from '@angular/core';

       @Directive({
        selector: '[dir]',
        standalone: false,
       })
       export class MyDir2 {
         inputA = input();
       }`,
    };
    const env = LanguageServiceTestEnv.setup();
    const project = createModuleAndProjectWithDeclarations(env, 'test', files);
    const template = project.openFile('app.html');
    template.moveCursorToText('inpu¦tA="abc"');

    const {textSpan, definitions} = getDefinitionsAndAssertBoundSpan(env, template);
    expect(template.contents.slice(textSpan.start, textSpan.start + textSpan.length)).toEqual(
      'inputA',
    );

    expect(definitions!.length).toEqual(2);
    const [def, def2] = definitions!;
    expect(def.textSpan).toContain('inputA');
    expect(def2.textSpan).toContain('inputA');
    // TODO(atscott): investigate why the text span includes more than just 'inputA'
    // assertTextSpans([def, def2], ['inputA']);
    assertFileNames([def, def2], ['dir2.ts', 'dir.ts']);
  });

  it('gets definitions for all outputs when attribute matches more than one', () => {
    initMockFileSystem('Native');
    const files = {
      'app.html': '<div dir (someEvent)="doSomething()"></div>',
      'dir.ts': `
       import {Directive, Output, EventEmitter} from '@angular/core';

       @Directive({
        selector: '[dir]',
        standalone: false,
       })
       export class MyDir {
         @Output() someEvent = new EventEmitter<void>();
       }`,
      'dir2.ts': `
       import {Directive, Output, EventEmitter} from '@angular/core';

       @Directive({
        selector: '[dir]',
        standalone: false,
       })
       export class MyDir2 {
         @Output() someEvent = new EventEmitter<void>();
       }`,
      'dir3.ts': `
       import {Directive, output, EventEmitter} from '@angular/core';

       @Directive({
        selector: '[dir]',
        standalone: false,
       })
       export class MyDir3 {
         someEvent = output();
       }`,
      'dir4.ts': `
         import {Directive, EventEmitter} from '@angular/core';
         import {outputFromObservable} from '@angular/core/rxjs-interop';

         @Directive({
          selector: '[dir]',
          standalone: false,
         })
         export class MyDir4 {
           someEvent = outputFromObservable(new EventEmitter<void>);
         }
       `,
      'app.ts': `
         import {Component, NgModule} from '@angular/core';
         import {CommonModule} from '@angular/common';

         @Component({
          templateUrl: 'app.html',
          standalone: false,
         })
         export class AppCmp {
           doSomething() {}
         }
       `,
    };
    const env = LanguageServiceTestEnv.setup();
    const project = createModuleAndProjectWithDeclarations(env, 'test', files);
    const template = project.openFile('app.html');
    template.moveCursorToText('(someEv¦ent)');

    const {textSpan, definitions} = getDefinitionsAndAssertBoundSpan(env, template);
    expect(template.contents.slice(textSpan.start, textSpan.start + textSpan.length)).toEqual(
      'someEvent',
    );

    expect(definitions.length).toEqual(4);
    const [def4, def3, def2, def] = definitions;
    expect(def.textSpan).toContain('someEvent');
    expect(def2.textSpan).toContain('someEvent');
    expect(def3.textSpan).toContain('someEvent');
    expect(def3.contextSpan!).toBe('someEvent = output();');
    expect(def4.textSpan).toContain('someEvent');
    expect(def4.contextSpan!).toBe(`someEvent = outputFromObservable(new EventEmitter<void>);`);
    // TODO(atscott): investigate why the text span includes more than just 'someEvent'
    // assertTextSpans([def, def2], ['someEvent']);
    assertFileNames([def4, def3, def2, def], ['dir4.ts', 'dir3.ts', 'dir2.ts', 'dir.ts']);
  });

  it('gets definitions for all model inputs when attribute matches more than one in a static attribute', () => {
    initMockFileSystem('Native');
    const files = {
      'app.ts': `
          import {Component, NgModule} from '@angular/core';
          import {CommonModule} from '@angular/common';

          @Component({
            templateUrl: 'app.html',
            standalone: false,
          })
          export class AppCmp {}
        `,
      'app.html': '<div dir inputA="abc"></div>',
      'dir.ts': `
        import {Directive, model} from '@angular/core';

        @Directive({
          selector: '[dir]',
          standalone: false,
        })
        export class MyDir {
          inputA = model('');
        }`,
      'dir2.ts': `
        import {Directive, model} from '@angular/core';

        @Directive({
          selector: '[dir]',
          standalone: false,
        })
        export class MyDir2 {
          inputA = model('');
        }`,
    };
    const env = LanguageServiceTestEnv.setup();
    const project = createModuleAndProjectWithDeclarations(env, 'test', files);
    const template = project.openFile('app.html');
    template.moveCursorToText('inpu¦tA="abc"');

    const {textSpan, definitions} = getDefinitionsAndAssertBoundSpan(env, template);
    expect(template.contents.slice(textSpan.start, textSpan.start + textSpan.length)).toBe(
      'inputA',
    );

    expect(definitions.length).toBe(2);
    const [def, def2] = definitions;
    expect(def.textSpan).toContain('inputA');
    expect(def2.textSpan).toContain('inputA');
    // TODO(atscott): investigate why the text span includes more than just 'inputA'
    // assertTextSpans([def, def2], ['inputA']);
    assertFileNames([def, def2], ['dir2.ts', 'dir.ts']);
  });

  it('gets definitions for all model inputs when attribute matches more than one in a two-way binding', () => {
    initMockFileSystem('Native');
    const files = {
      'app.ts': `
            import {Component, NgModule} from '@angular/core';
            import {CommonModule} from '@angular/common';

            @Component({
              templateUrl: 'app.html',
              standalone: false,
            })
            export class AppCmp {
              value = 'abc';
            }
          `,
      'app.html': '<div dir [(inputA)]="value"></div>',
      'dir.ts': `
            import {Directive, model} from '@angular/core';

          @Directive({
            selector: '[dir]',
            standalone: false,
          })
          export class MyDir {
            inputA = model('');
          }`,
      'dir2.ts': `
          import {Directive, model} from '@angular/core';

          @Directive({
            selector: '[dir]',
            standalone: false,
          })
          export class MyDir2 {
            inputA = model('');
          }`,
    };
    const env = LanguageServiceTestEnv.setup();
    const project = createModuleAndProjectWithDeclarations(env, 'test', files);
    const template = project.openFile('app.html');
    template.moveCursorToText('[(inpu¦tA)]="value"');

    const {textSpan, definitions} = getDefinitionsAndAssertBoundSpan(env, template);
    expect(template.contents.slice(textSpan.start, textSpan.start + textSpan.length)).toBe(
      'inputA',
    );

    expect(definitions.length).toBe(4);
    const [def, def2, def3, def4] = definitions;

    // We have four definitons to the `inputA` member, because each `model()`
    // instance implicitly creates both an input and an output.
    expect(def.textSpan).toContain('inputA');
    expect(def2.textSpan).toContain('inputA');
    expect(def3.textSpan).toContain('inputA');
    expect(def4.textSpan).toContain('inputA');
    assertFileNames([def, def2, def3, def4], ['dir2.ts', 'dir.ts', 'dir2.ts', 'dir.ts']);
  });

  it('should go to the pre-compiled style sheet', () => {
    initMockFileSystem('Native');
    const files = {
      'app.ts': `
       import {Component} from '@angular/core';

       @Component({
         template: '',
         styleUrls: ['./style.css'],
         standalone: false,
       })
       export class AppCmp {}
       `,
      'style.scss': '',
    };
    const env = LanguageServiceTestEnv.setup();
    const project = createModuleAndProjectWithDeclarations(env, 'test', files);
    const appFile = project.openFile('app.ts');
    appFile.moveCursorToText(`['./styl¦e.css']`);
    const {textSpan, definitions} = getDefinitionsAndAssertBoundSpan(env, appFile);
    expect(appFile.contents.slice(textSpan.start, textSpan.start + textSpan.length)).toEqual(
      './style.css',
    );

    expect(definitions.length).toEqual(1);
    assertFileNames(definitions, ['style.scss']);
  });

  it('gets definition for property of variable declared in template', () => {
    initMockFileSystem('Native');
    const files = {
      'app.html': `
         <ng-container *ngIf="{prop: myVal} as myVar">
           {{myVar.prop.name}}
         </ng-container>
       `,
      'app.ts': `
         import {Component} from '@angular/core';

         @Component({
          templateUrl: '/app.html',
          standalone: false,
         })
         export class AppCmp {
           myVal = {name: 'Andrew'};
         }
       `,
    };
    const env = LanguageServiceTestEnv.setup();

    const project = createModuleAndProjectWithDeclarations(env, 'test', files);
    const template = project.openFile('app.html');
    project.expectNoSourceDiagnostics();

    template.moveCursorToText('{{myVar.pro¦p.name}}');
    const {definitions} = getDefinitionsAndAssertBoundSpan(env, template);
    expect(definitions![0].name).toEqual('"prop"');
    assertFileNames(Array.from(definitions!), ['app.html']);
  });

  it('gets definition for component property access in an arrow function', () => {
    initMockFileSystem('Native');
    const files = {
      'app.html': '',
      'app.ts': `
         import {Component} from '@angular/core';

         @Component({templateUrl: '/app.html', standalone: false})
         export class AppCmp {
          componentProp = 123;
         }
       `,
    };
    const env = LanguageServiceTestEnv.setup();

    const project = createModuleAndProjectWithDeclarations(env, 'test', files);
    const template = project.openFile('app.html');
    template.contents = '{{() => componentProp + 1}}';
    project.expectNoSourceDiagnostics();

    template.moveCursorToText('{{() => compon¦entProp + 1}}');
    const {definitions} = getDefinitionsAndAssertBoundSpan(env, template);
    expect(definitions[0].name).toEqual('componentProp');
    expect(definitions[0].kind).toBe(ts.ScriptElementKind.memberVariableElement);
    expect(definitions[0].textSpan).toBe('componentProp');
    assertFileNames(Array.from(definitions), ['app.ts']);
  });

  it('gets definition for parameter access in an arrow function', () => {
    initMockFileSystem('Native');
    const files = {
      'app.html': '',
      'app.ts': `
         import {Component} from '@angular/core';

         @Component({templateUrl: '/app.html', standalone: false})
         export class AppCmp {}
       `,
    };
    const env = LanguageServiceTestEnv.setup();

    const project = createModuleAndProjectWithDeclarations(env, 'test', files);
    const template = project.openFile('app.html');
    template.contents = '{{(val) => val + 1}}';
    project.expectNoSourceDiagnostics();

    template.moveCursorToText('{{(val) => va¦l + 1}}');
    const {definitions} = getDefinitionsAndAssertBoundSpan(env, template);
    expect(definitions[0].name).toEqual('val');
    expect(definitions[0].kind).toBe(ts.ScriptElementKind.parameterElement);
    expect(definitions[0].textSpan).toBe('val + 1');
    assertFileNames(Array.from(definitions), ['app.html']);
  });

  it('gets definition for a let declaration', () => {
    initMockFileSystem('Native');
    const files = {
      'app.html': '',
      'app.ts': `
         import {Component} from '@angular/core';

         @Component({
          templateUrl: '/app.html',
          standalone: false,
         })
         export class AppCmp {}
       `,
    };
    const env = LanguageServiceTestEnv.setup();

    const project = createModuleAndProjectWithDeclarations(env, 'test', files);
    const template = project.openFile('app.html');
    template.contents = '@let foo = {value: 123}; {{foo.value}}';
    project.expectNoSourceDiagnostics();

    template.moveCursorToText('{{fo¦o.value}}');
    const {definitions} = getDefinitionsAndAssertBoundSpan(env, template);
    expect(definitions[0].name).toEqual('foo');
    expect(definitions[0].kind).toBe(ts.ScriptElementKind.variableElement);
    expect(definitions[0].textSpan).toBe('@let foo = {value: 123};');
    assertFileNames(Array.from(definitions), ['app.html']);
  });

  it('gets definition for selectorless component', () => {
    initMockFileSystem('Native');
    const files = {
      'app.html': '',
      'dep.ts': `
         import {Component} from '@angular/core';

         @Component({template: ''})
         export class Dep {}
       `,
      'app.ts': `
         import {Component} from '@angular/core';
         import {Dep} from './dep';

         @Component({
          templateUrl: '/app.html',
         })
         export class AppCmp {}
       `,
    };
    const env = LanguageServiceTestEnv.setup();

    const project = createProjectWithStandaloneDeclarations(env, 'test', files, {
      _enableSelectorless: true,
    });
    const template = project.openFile('app.html');
    template.contents = '<Dep/>';
    project.expectNoSourceDiagnostics();

    template.moveCursorToText('<De¦p/>');
    const {definitions} = getDefinitionsAndAssertBoundSpan(env, template);
    expect(definitions[0].name).toEqual('Dep');
    expect(definitions[0].kind).toBe(ts.ScriptElementKind.classElement);
    expect(definitions[0].textSpan).toBe('Dep');
    assertFileNames(Array.from(definitions), ['dep.ts']);
  });

  it('gets definition for selectorless directive', () => {
    initMockFileSystem('Native');
    const files = {
      'app.html': '',
      'dep.ts': `
         import {Directive} from '@angular/core';

         @Directive()
         export class Dep {}
       `,
      'app.ts': `
         import {Component} from '@angular/core';
         import {Dep} from './dep';

         @Component({
          templateUrl: '/app.html',
         })
         export class AppCmp {}
       `,
    };
    const env = LanguageServiceTestEnv.setup();

    const project = createProjectWithStandaloneDeclarations(env, 'test', files, {
      _enableSelectorless: true,
    });
    const template = project.openFile('app.html');
    template.contents = '<div @Dep></div>';
    project.expectNoSourceDiagnostics();

    template.moveCursorToText('@De¦p');
    const {definitions} = getDefinitionsAndAssertBoundSpan(env, template);
    expect(definitions[0].name).toEqual('Dep');
    expect(definitions[0].kind).toBe(ts.ScriptElementKind.classElement);
    expect(definitions[0].textSpan).toBe('Dep');
    assertFileNames(Array.from(definitions), ['dep.ts']);
  });

  it('gets definition of selectorless component input', () => {
    initMockFileSystem('Native');
    const files = {
      'app.html': '',
      'dep.ts': `
         import {Component, Input} from '@angular/core';

         @Component({template: ''})
         export class Dep {
          @Input() someInput: any;
         }
       `,
      'app.ts': `
         import {Component} from '@angular/core';
         import {Dep} from './dep';

         @Component({
          templateUrl: '/app.html',
         })
         export class AppCmp {}
       `,
    };
    const env = LanguageServiceTestEnv.setup();

    const project = createProjectWithStandaloneDeclarations(env, 'test', files, {
      _enableSelectorless: true,
    });
    const template = project.openFile('app.html');
    template.contents = '<Dep [someInput]="123"/>';
    project.expectNoSourceDiagnostics();

    template.moveCursorToText('[some¦Input]');
    const {textSpan, definitions} = getDefinitionsAndAssertBoundSpan(env, template);

    expect(template.contents.slice(textSpan.start, textSpan.start + textSpan.length)).toEqual(
      'someInput',
    );
    expect(definitions.length).toBe(1);
    expect(definitions[0].textSpan).toContain('someInput');
    assertFileNames(definitions, ['dep.ts']);
  });

  it('gets definition of selectorless directive input', () => {
    initMockFileSystem('Native');
    const files = {
      'app.html': '',
      'dep.ts': `
         import {Directive, Input} from '@angular/core';

         @Directive()
         export class Dep {
          @Input() someInput: any;
         }
       `,
      'app.ts': `
         import {Component} from '@angular/core';
         import {Dep} from './dep';

         @Component({
          templateUrl: '/app.html',
         })
         export class AppCmp {}
       `,
    };
    const env = LanguageServiceTestEnv.setup();

    const project = createProjectWithStandaloneDeclarations(env, 'test', files, {
      _enableSelectorless: true,
    });
    const template = project.openFile('app.html');
    template.contents = '<div @Dep([someInput]="123")></div>';
    project.expectNoSourceDiagnostics();

    template.moveCursorToText('[some¦Input]');
    const {textSpan, definitions} = getDefinitionsAndAssertBoundSpan(env, template);

    expect(template.contents.slice(textSpan.start, textSpan.start + textSpan.length)).toEqual(
      'someInput',
    );
    expect(definitions.length).toBe(1);
    expect(definitions[0].textSpan).toContain('someInput');
    assertFileNames(definitions, ['dep.ts']);
  });

  it('gets definition of selectorless component output', () => {
    initMockFileSystem('Native');
    const files = {
      'app.html': '',
      'dep.ts': `
         import {Component, Output, EventEmitter} from '@angular/core';

         @Component({template: ''})
         export class Dep {
          @Output() someEvent = new EventEmitter<void>();
         }
       `,
      'app.ts': `
         import {Component} from '@angular/core';
         import {Dep} from './dep';

         @Component({
          templateUrl: '/app.html',
         })
         export class AppCmp {
          handler() {}
         }
       `,
    };
    const env = LanguageServiceTestEnv.setup();

    const project = createProjectWithStandaloneDeclarations(env, 'test', files, {
      _enableSelectorless: true,
    });
    const template = project.openFile('app.html');
    template.contents = '<Dep (someEvent)="handler()"/>';
    project.expectNoSourceDiagnostics();

    template.moveCursorToText('(some¦Event)');
    const {textSpan, definitions} = getDefinitionsAndAssertBoundSpan(env, template);

    expect(template.contents.slice(textSpan.start, textSpan.start + textSpan.length)).toEqual(
      'someEvent',
    );
    expect(definitions.length).toBe(1);
    expect(definitions[0].textSpan).toContain('someEvent');
    assertFileNames(definitions, ['dep.ts']);
  });

  it('gets definition of selectorless directive output', () => {
    initMockFileSystem('Native');
    const files = {
      'app.html': '',
      'dep.ts': `
         import {Directive, Output, EventEmitter} from '@angular/core';

         @Directive()
         export class Dep {
          @Output() someEvent = new EventEmitter<void>();
         }
       `,
      'app.ts': `
         import {Component} from '@angular/core';
         import {Dep} from './dep';

         @Component({
          templateUrl: '/app.html',
         })
         export class AppCmp {
          handler() {}
         }
       `,
    };
    const env = LanguageServiceTestEnv.setup();

    const project = createProjectWithStandaloneDeclarations(env, 'test', files, {
      _enableSelectorless: true,
    });
    const template = project.openFile('app.html');
    template.contents = '<div @Dep((someEvent)="handler()")></div>';
    project.expectNoSourceDiagnostics();

    template.moveCursorToText('(some¦Event)');
    const {textSpan, definitions} = getDefinitionsAndAssertBoundSpan(env, template);

    expect(template.contents.slice(textSpan.start, textSpan.start + textSpan.length)).toEqual(
      'someEvent',
    );
    expect(definitions.length).toBe(1);
    expect(definitions[0].textSpan).toContain('someEvent');
    assertFileNames(definitions, ['dep.ts']);
  });

  it('gets definition for a method in a void expression', () => {
    initMockFileSystem('Native');
    const files = {
      'app.html': '',
      'app.ts': `
         import {Component} from '@angular/core';

         @Component({
          templateUrl: '/app.html',
          standalone: false,
         })
         export class AppCmp {
           doSomething() {}
         }
       `,
    };
    const env = LanguageServiceTestEnv.setup();

    const project = createModuleAndProjectWithDeclarations(env, 'test', files);
    const template = project.openFile('app.html');
    template.contents = '<div (click)="void doSomething()">';
    project.expectNoSourceDiagnostics();

    template.moveCursorToText('void doSomet¦hing()');
    const {definitions} = getDefinitionsAndAssertBoundSpan(env, template);
    expect(definitions[0].name).toEqual('doSomething');
    expect(definitions[0].kind).toBe(ts.ScriptElementKind.memberFunctionElement);
    expect(definitions[0].textSpan).toBe('doSomething');
    assertFileNames(Array.from(definitions), ['app.ts']);
  });

  it('gets definition for a tagged template literal expression', () => {
    initMockFileSystem('Native');
    const files = {
      'app.html': '',
      'app.ts': `
         import {Component} from '@angular/core';

         @Component({
          templateUrl: '/app.html',
          standalone: false,
         })
         export class AppCmp {
           name = 'Bob';
           tag = (...args: unknown[]) => '';
         }
       `,
    };
    const env = LanguageServiceTestEnv.setup();

    const project = createModuleAndProjectWithDeclarations(env, 'test', files);
    const template = project.openFile('app.html');
    template.contents = '{{ tag`Hello, ${name}!` }}';
    project.expectNoSourceDiagnostics();

    template.moveCursorToText('${na¦me}');
    const {definitions} = getDefinitionsAndAssertBoundSpan(env, template);
    expect(definitions[0].name).toEqual('name');
    expect(definitions[0].kind).toBe(ts.ScriptElementKind.memberVariableElement);
    expect(definitions[0].textSpan).toBe('name');
    assertFileNames(Array.from(definitions), ['app.ts']);
  });

  it('gets definition for a tagged template literal tag', () => {
    initMockFileSystem('Native');
    const files = {
      'app.html': '',
      'app.ts': `
         import {Component} from '@angular/core';

         @Component({
          templateUrl: '/app.html',
          standalone: false,
         })
         export class AppCmp {
           name = 'Bob';
           tag = (...args: unknown[]) => '';
         }
       `,
    };
    const env = LanguageServiceTestEnv.setup();

    const project = createModuleAndProjectWithDeclarations(env, 'test', files);
    const template = project.openFile('app.html');
    template.contents = '{{ tag`Hello, ${name}!` }}';
    project.expectNoSourceDiagnostics();

    template.moveCursorToText('t¦ag`');
    const {definitions} = getDefinitionsAndAssertBoundSpan(env, template);
    expect(definitions[0].name).toEqual('tag');
    expect(definitions[0].kind).toBe(ts.ScriptElementKind.memberVariableElement);
    expect(definitions[0].textSpan).toBe('tag');
    assertFileNames(Array.from(definitions), ['app.ts']);
  });

  it('gets definition for a host binding value of a component', () => {
    initMockFileSystem('Native');
    const files = {
      'app.html': '',
      'app.ts': `
        import {Component} from '@angular/core';
        @Component({
          template: '',
          standalone: false,
          host: {
            '[title]': 'myTitle',
          }
        })
        export class AppCmp {
          myTitle = 'hello';
        }
       `,
    };
    const env = LanguageServiceTestEnv.setup();

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

    appFile.moveCursorToText(`'[title]': 'myT¦itle'`);
    const {definitions} = getDefinitionsAndAssertBoundSpan(env, appFile);
    expect(definitions[0].name).toEqual('myTitle');
    expect(definitions[0].kind).toBe(ts.ScriptElementKind.memberVariableElement);
    expect(definitions[0].textSpan).toBe('myTitle');
    assertFileNames(Array.from(definitions), ['app.ts']);
  });

  it('gets definition for a host listener of a component', () => {
    initMockFileSystem('Native');
    const files = {
      'app.html': '',
      'app.ts': `
        import {Component} from '@angular/core';
        @Component({
          template: '',
          standalone: false,
          host: {
            '(click)': 'handleClick($event)',
          }
        })
        export class AppCmp {
          handleClick(event: MouseEvent) {}
        }
       `,
    };
    const env = LanguageServiceTestEnv.setup();

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

    appFile.moveCursorToText(`'(click)': 'handle¦Click($event)'`);
    const {definitions} = getDefinitionsAndAssertBoundSpan(env, appFile);
    expect(definitions[0].name).toEqual('handleClick');
    expect(definitions[0].kind).toBe(ts.ScriptElementKind.memberFunctionElement);
    expect(definitions[0].textSpan).toBe('handleClick');
    assertFileNames(Array.from(definitions), ['app.ts']);
  });

  it('gets definition for a host binding value of a directive', () => {
    initMockFileSystem('Native');
    const files = {
      'dir.ts': `
        import {Directive} from '@angular/core';
        @Directive({
          selector: '[my-dir]',
          standalone: false,
          host: {
            '[title]': 'myTitle',
          }
        })
        export class MyDir {
          myTitle = 'hello';
        }
       `,
    };
    const env = LanguageServiceTestEnv.setup();

    const project = createModuleAndProjectWithDeclarations(env, 'test', files);
    const dirFile = project.openFile('dir.ts');
    project.expectNoSourceDiagnostics();

    dirFile.moveCursorToText(`'[title]': 'myT¦itle'`);
    const {definitions} = getDefinitionsAndAssertBoundSpan(env, dirFile);
    expect(definitions[0].name).toEqual('myTitle');
    expect(definitions[0].kind).toBe(ts.ScriptElementKind.memberVariableElement);
    expect(definitions[0].textSpan).toBe('myTitle');
    assertFileNames(Array.from(definitions), ['dir.ts']);
  });

  it('gets definition for a property in a "in" expression', () => {
    initMockFileSystem('Native');
    const files = {
      'app.html': `<div>{{'foo' in myObj}}</div>`,
      'app.ts': `
         import {Component} from '@angular/core';
         @Component({
          templateUrl: '/app.html',
          standalone: false,
         })
         export class AppCmp {
           myObj: {foo: string} = {foo: 'bar'};
         }
       `,
    };
    const env = LanguageServiceTestEnv.setup();

    const project = createModuleAndProjectWithDeclarations(env, 'test', files);
    const template = project.openFile('app.html');
    project.expectNoSourceDiagnostics();

    template.moveCursorToText(`'foo' in myO¦bj`);
    const {definitions} = getDefinitionsAndAssertBoundSpan(env, template);
    expect(definitions[0].name).toEqual('myObj');
    expect(definitions[0].kind).toBe(ts.ScriptElementKind.memberVariableElement);
    expect(definitions[0].textSpan).toBe('myObj');
    assertFileNames(Array.from(definitions), ['app.ts']);
  });

  function getDefinitionsAndAssertBoundSpan(env: LanguageServiceTestEnv, file: OpenBuffer) {
    env.expectNoSourceDiagnostics();
    const definitionAndBoundSpan = file.getDefinitionAndBoundSpan();

    if (!definitionAndBoundSpan) {
      throw new Error('Could not retrieve definition');
    }

    const {textSpan, definitions} = definitionAndBoundSpan;
    expect(definitions).toBeTruthy();
    return {textSpan, definitions: definitions!.map((d) => humanizeDocumentSpanLike(d, env))};
  }

  describe('when an input has a dollar sign', () => {
    let env: LanguageServiceTestEnv;
    const files = {
      'app.ts': `
	 import {Component, NgModule, Input} from '@angular/core';

	 @Component({
    selector: 'dollar-cmp', template: '',
    standalone: false,
   })
	 export class DollarCmp {
	   @Input() obs$!: string;
	 }

	 @Component({
    template: '<dollar-cmp [obs$]="greeting"></dollar-cmp>',
    standalone: false,
   })
	 export class AppCmp {
	   greeting = 'hello';
	 }

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

    beforeEach(() => {
      initMockFileSystem('Native');
      env = LanguageServiceTestEnv.setup();
    });

    it('can get definitions for input', () => {
      const project = env.addProject('test', files, {strictTemplates: false});
      const definitions = getDefinitionsAndAssertBoundSpanFromProject(
        project,
        'app.ts',
        '[o¦bs$]="greeting"',
        env,
      );
      expect(definitions!.length).toEqual(1);

      assertTextSpans(definitions, ['obs$']);
      assertFileNames(definitions, ['app.ts']);
    });

    it('can get definitions for component', () => {
      const project = env.addProject('test', files, {strictTemplates: false});
      const definitions = getDefinitionsAndAssertBoundSpanFromProject(
        project,
        'app.ts',
        '<dollar-cm¦p',
        env,
      );
      expect(definitions!.length).toEqual(1);

      assertTextSpans(definitions, ['DollarCmp']);
      assertFileNames(definitions, ['app.ts']);
    });
  });

  describe('when a selector and input of a directive have a dollar sign', () => {
    let env: LanguageServiceTestEnv;
    it('can get definitions', () => {
      initMockFileSystem('Native');
      env = LanguageServiceTestEnv.setup();
      const files = {
        'app.ts': `
	 import {Component, Directive, NgModule, Input} from '@angular/core';

	 @Directive({
    selector: '[dollar\\\\$]',
    standalone: false,
   })
	 export class DollarDir {
	   @Input() dollar$!: string;
	 }

	 @Component({
    template: '<div [dollar$]="greeting"></div>',
    standalone: false,
   })
	 export class AppCmp {
	   greeting = 'hello';
	 }

	 @NgModule({declarations: [AppCmp, DollarDir]})
	 export class AppModule {}
       `,
      };
      const project = env.addProject('test', files, {strictTemplates: false});
      const definitions = getDefinitionsAndAssertBoundSpanFromProject(
        project,
        'app.ts',
        '[dollar¦$]="greeting"',
        env,
      );
      expect(definitions!.length).toEqual(2);

      assertTextSpans(definitions, ['dollar$', 'DollarDir']);
      assertFileNames(definitions, ['app.ts']);
    });
  });

  function getDefinitionsAndAssertBoundSpanFromProject(
    project: Project,
    file: string,
    targetText: string,
    env: LanguageServiceTestEnv,
  ) {
    const template = project.openFile(file);
    env.expectNoSourceDiagnostics();
    project.expectNoTemplateDiagnostics('app.ts', 'AppCmp');

    template.moveCursorToText(targetText);
    const defAndBoundSpan = template.getDefinitionAndBoundSpan();
    expect(defAndBoundSpan).toBeTruthy();
    expect(defAndBoundSpan!.definitions).toBeTruthy();
    return defAndBoundSpan!.definitions!.map((d) => humanizeDocumentSpanLike(d, env));
  }
});
