/**
 * @license
 * Copyright Google Inc. All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.io/license
 */
import * as ts from 'typescript';
import {createLanguageService} from '../src/language_service';
import {TypeScriptServiceHost} from '../src/typescript_host';
import {MockTypescriptHost} from './test_utils';
/**
 * Note: If we want to test that a specific diagnostic message is emitted, then
 * use the `mockHost.addCode()` helper method to add code to an existing file and check
 * that the diagnostic messages contain the expected output.
 *
 * If the goal is to assert that there is no error in a specific file, then use
 * `mockHost.override()` method to completely override an existing file, and
 * make sure no diagnostics are produced. When doing so, be extra cautious
 * about import statements and make sure to assert empty TS diagnostic messages
 * as well.
 */
const EXPRESSION_CASES = '/app/expression-cases.ts';
const NG_FOR_CASES = '/app/ng-for-cases.ts';
const NG_IF_CASES = '/app/ng-if-cases.ts';
const TEST_TEMPLATE = '/app/test.ng';
const APP_COMPONENT = '/app/app.component.ts';
describe('diagnostics', () => {
  const mockHost = new MockTypescriptHost(['/app/main.ts', '/app/parsing-cases.ts']);
  const tsLS = ts.createLanguageService(mockHost);
  const ngHost = new TypeScriptServiceHost(mockHost, tsLS);
  const ngLS = createLanguageService(ngHost);
  beforeEach(() => { mockHost.reset(); });
  it('should produce no diagnostics for test.ng', () => {
    // there should not be any errors on existing external template
    expect(ngLS.getDiagnostics('/app/test.ng')).toEqual([]);
  });
  it('should not return TS and NG errors for existing files', () => {
    const files = [
      '/app/app.component.ts',
      '/app/main.ts',
    ];
    for (const file of files) {
      const syntaxDiags = tsLS.getSyntacticDiagnostics(file);
      expect(syntaxDiags).toEqual([]);
      const semanticDiags = tsLS.getSemanticDiagnostics(file);
      expect(semanticDiags).toEqual([]);
      const ngDiags = ngLS.getDiagnostics(file);
      expect(ngDiags).toEqual([]);
    }
  });
  it('should report error for unexpected end of expression', () => {
    const content = mockHost.override(TEST_TEMPLATE, `{{ 5 / }}`);
    const diags = ngLS.getDiagnostics(TEST_TEMPLATE);
    expect(diags.length).toBe(1);
    const {messageText, start, length} = diags[0];
    expect(messageText)
        .toBe(
            'Parser Error: Unexpected end of expression: {{ 5 / }} ' +
            'at the end of the expression [{{ 5 / }}] in /app/test.ng@0:0');
    expect(start).toBe(0);
    expect(length).toBe(content.length);
  });
  // https://github.com/angular/vscode-ng-language-service/issues/242
  it('should support $any() type cast function', () => {
    mockHost.override(TEST_TEMPLATE, `
{{$any(title).xyz}}
`);
    const diags = ngLS.getDiagnostics(TEST_TEMPLATE);
    expect(diags).toEqual([]);
  });
  it('should report error for $any() with incorrect number of arguments', () => {
    const templates = [
      '{{$any().xyz}}
',              // no argument
      '{{$any(title, title).xyz}}
',  // two arguments
    ];
    for (const template of templates) {
      mockHost.override(TEST_TEMPLATE, template);
      const diags = ngLS.getDiagnostics(TEST_TEMPLATE);
      expect(diags.length).toBe(1);
      expect(diags[0].messageText).toBe('Unable to resolve signature for call of method $any');
    }
  });
  it('should not produce diagnostics for slice pipe with arguments', () => {
    mockHost.override(TEST_TEMPLATE, `
      
        {{h.name}}
      
`);
    const diags = ngLS.getDiagnostics(TEST_TEMPLATE);
    expect(diags).toEqual([]);
  });
  it('should produce diagnostics for slice pipe with args when member is invalid', () => {
    mockHost.override(TEST_TEMPLATE, `
      
        {{h.age}}
      
`);
    const diags = ngLS.getDiagnostics(TEST_TEMPLATE);
    expect(diags.length).toBe(1);
    expect(diags[0].messageText)
        .toBe(`Identifier 'age' is not defined. 'Hero' does not contain such a member`);
  });
  it('should not report error for variable initialized as class method', () => {
    mockHost.override(TEST_TEMPLATE, `
      
        
      
    `);
    const diagnostics = ngLS.getDiagnostics(TEST_TEMPLATE);
    expect(diagnostics).toEqual([]);
  });
  describe('diagnostics for ngFor exported values', () => {
    it('should report errors for mistmatched exported types', () => {
      mockHost.override(TEST_TEMPLATE, `
        
            'i' is a number; 'isFirst' is a boolean
          {{ i === isFirst }}
        
      `);
      const diags = ngLS.getDiagnostics(TEST_TEMPLATE);
      expect(diags.length).toBe(1);
      expect(diags[0].messageText).toBe(`Expected the operants to be of similar type or any`);
    });
    it('should not report errors for matching exported type', () => {
      mockHost.override(TEST_TEMPLATE, `
        
            'i' is a number
          {{ i < 2 }}
        
      `);
      const diags = ngLS.getDiagnostics(TEST_TEMPLATE);
      expect(diags.length).toBe(0);
    });
  });
  describe('diagnostics for invalid indexed type property access', () => {
    it('should work with numeric index signatures (arrays)', () => {
      mockHost.override(TEST_TEMPLATE, `
        {{heroes[0].badProperty}}`);
      const diags = ngLS.getDiagnostics(TEST_TEMPLATE);
      expect(diags.length).toBe(1);
      expect(diags[0].messageText)
          .toBe(`Identifier 'badProperty' is not defined. 'Hero' does not contain such a member`);
    });
    describe('with string index signatures', () => {
      it('should work with index notation', () => {
        mockHost.override(TEST_TEMPLATE, `
        {{heroesByName['Jacky'].badProperty}}`);
        const diags = ngLS.getDiagnostics(TEST_TEMPLATE);
        expect(diags.length).toBe(1);
        expect(diags[0].messageText)
            .toBe(`Identifier 'badProperty' is not defined. 'Hero' does not contain such a member`);
      });
      it('should work with dot notation', () => {
        mockHost.override(TEST_TEMPLATE, `
        {{heroesByName.jacky.badProperty}}`);
        const diags = ngLS.getDiagnostics(TEST_TEMPLATE);
        expect(diags.length).toBe(1);
        expect(diags[0].messageText)
            .toBe(`Identifier 'badProperty' is not defined. 'Hero' does not contain such a member`);
      });
      it('should not produce errors with dot notation if stringIndexType is a primitive type',
         () => {
           mockHost.override(TEST_TEMPLATE, `{{primitiveIndexType.test}}`);
           const diags = ngLS.getDiagnostics(TEST_TEMPLATE);
           expect(diags.length).toBe(0);
         });
    });
  });
  it('should produce diagnostics for invalid tuple type property access', () => {
    mockHost.override(TEST_TEMPLATE, `
        {{tupleArray[1].badProperty}}`);
    const diags = ngLS.getDiagnostics(TEST_TEMPLATE);
    expect(diags.length).toBe(1);
    expect(diags[0].messageText)
        .toBe(`Identifier 'badProperty' is not defined. 'Hero' does not contain such a member`);
  });
  it('should not produce errors if tuple array index out of bound', () => {
    mockHost.override(TEST_TEMPLATE, `
        {{tupleArray[2].badProperty}}`);
    const diags = ngLS.getDiagnostics(TEST_TEMPLATE);
    expect(diags).toEqual([]);
  });
  it('should not produce errors on function.bind()', () => {
    mockHost.override(TEST_TEMPLATE, `
      
      `);
    const diags = ngLS.getDiagnostics(TEST_TEMPLATE);
    expect(diags).toEqual([]);
  });
  describe('in expression-cases.ts', () => {
    it('should report access to an unknown field', () => {
      const diags = ngLS.getDiagnostics(EXPRESSION_CASES).map(d => d.messageText);
      expect(diags).toContain(
          `Identifier 'foo' is not defined. ` +
          `The component declaration, template variable declarations, ` +
          `and element references do not contain such a member`);
    });
    it('should report access to an unknown sub-field', () => {
      const diags = ngLS.getDiagnostics(EXPRESSION_CASES).map(d => d.messageText);
      expect(diags).toContain(
          `Identifier 'nam' is not defined. 'Person' does not contain such a member`);
    });
    it('should report access to a private member', () => {
      const diags = ngLS.getDiagnostics(EXPRESSION_CASES).map(d => d.messageText);
      expect(diags).toContain(`Identifier 'myField' refers to a private member of the component`);
    });
    it('should report numeric operator errors', () => {
      const diags = ngLS.getDiagnostics(EXPRESSION_CASES).map(d => d.messageText);
      expect(diags).toContain('Expected a numeric type');
    });
  });
  describe('in ng-for-cases.ts', () => {
    it('should report an unknown field', () => {
      const diags = ngLS.getDiagnostics(NG_FOR_CASES).map(d => d.messageText);
      expect(diags).toContain(
          `Identifier 'people_1' is not defined. ` +
          `The component declaration, template variable declarations, ` +
          `and element references do not contain such a member`);
    });
    it('should report an unknown context reference', () => {
      const diags = ngLS.getDiagnostics(NG_FOR_CASES).map(d => d.messageText);
      expect(diags).toContain(`The template context does not define a member called 'even_1'`);
    });
    it('should report an unknown value in a key expression', () => {
      const diags = ngLS.getDiagnostics(NG_FOR_CASES).map(d => d.messageText);
      expect(diags).toContain(
          `Identifier 'trackBy_1' is not defined. ` +
          `The component declaration, template variable declarations, ` +
          `and element references do not contain such a member`);
    });
  });
  describe('in ng-if-cases.ts', () => {
    it('should report an implicit context reference', () => {
      const diags = ngLS.getDiagnostics(NG_IF_CASES).map(d => d.messageText);
      expect(diags).toContain(`The template context does not define a member called 'unknown'`);
    });
  });
  // #17611
  it('should not report diagnostic on iteration of any', () => {
    const fileName = '/app/test.ng';
    mockHost.override(fileName, '{{value.someField}}
');
    const diagnostics = ngLS.getDiagnostics(fileName);
    expect(diagnostics).toEqual([]);
  });
  it('should report diagnostic for invalid property in nested ngFor', () => {
    const content = mockHost.override(TEST_TEMPLATE, `
      
    `);
    const diagnostics = ngLS.getDiagnostics(TEST_TEMPLATE);
    expect(diagnostics.length).toBe(1);
    const {messageText, start, length} = diagnostics[0];
    expect(messageText)
        .toBe(`Identifier 'xyz' is not defined. 'Hero' does not contain such a member`);
    expect(start).toBe(content.indexOf('member.xyz'));
    expect(length).toBe('member.xyz'.length);
  });
  describe('with $event', () => {
    it('should accept an event', () => {
      const fileName = '/app/test.ng';
      mockHost.override(fileName, 'Click me!
');
      const diagnostics = ngLS.getDiagnostics(fileName);
      expect(diagnostics).toEqual([]);
    });
    it('should reject it when not in an event binding', () => {
      const fileName = '/app/test.ng';
      const content = mockHost.override(fileName, '');
      const diagnostics = ngLS.getDiagnostics(fileName) !;
      expect(diagnostics.length).toBe(1);
      const {messageText, start, length} = diagnostics[0];
      expect(messageText)
          .toBe(
              `Identifier '$event' is not defined. The component declaration, template variable declarations, and element references do not contain such a member`);
      const keyword = '$event';
      expect(start).toBe(content.lastIndexOf(keyword));
      expect(length).toBe(keyword.length);
    });
  });
  it('should not crash with a incomplete *ngFor', () => {
    const fileName = mockHost.addCode(`
      @Component({
        template: ' ~{after-div}'
      })
      export class MyComponent {}`);
    expect(() => ngLS.getDiagnostics(fileName)).not.toThrow();
  });
  it('should report a component not in a module', () => {
    const fileName = mockHost.addCode(`
      @Component({
        template: ''
      })
      export class MyComponent {}`);
    const diagnostics = ngLS.getDiagnostics(fileName) !;
    expect(diagnostics.length).toBe(1);
    const {messageText, start, length} = diagnostics[0];
    expect(messageText)
        .toBe(
            `Component 'MyComponent' is not included in a module and will not be available inside a template. Consider adding it to a NgModule declaration.`);
    const content = mockHost.readFile(fileName) !;
    const keyword = '@Component';
    expect(start).toBe(content.lastIndexOf(keyword) + 1);  // exclude leading '@'
    expect(length).toBe(keyword.length - 1);               // exclude leading '@'
  });
  it(`should not report an error for a form's host directives`, () => {
    mockHost.override(APP_COMPONENT, `
      import { Component } from '@angular/core';
      @Component({
        template: ''})
      export class AppComponent {}`);
    const tsDiags = tsLS.getSemanticDiagnostics(APP_COMPONENT);
    expect(tsDiags).toEqual([]);
    const ngDiags = ngLS.getDiagnostics(APP_COMPONENT);
    expect(ngDiags).toEqual([]);
  });
  it('should not throw getting diagnostics for an index expression', () => {
    const fileName = mockHost.addCode(`
      @Component({
        template: ''
      })
      export class MyComponent {}`);
    expect(() => ngLS.getDiagnostics(fileName)).not.toThrow();
  });
  it('should not throw using a directive with no value', () => {
    const fileName = mockHost.addCode(`
      @Component({
        template: ''
      })
      export class MyComponent {
        name = 'some name';
      }`);
    expect(() => ngLS.getDiagnostics(fileName)).not.toThrow();
  });
  it('should report an error for invalid metadata', () => {
    const content = mockHost.override(APP_COMPONENT, `
      import { Component } from '@angular/core';
      @Component({
        template: '',
        providers: [
          {provide: 'foo', useFactory: () => 'foo' }
        ]
      })
      export class AppComponent {
        name = 'some name';
      }`);
    const tsDiags = tsLS.getSemanticDiagnostics(APP_COMPONENT);
    expect(tsDiags).toEqual([]);
    const ngDiags = ngLS.getDiagnostics(APP_COMPONENT) !;
    expect(ngDiags.length).toBe(1);
    const {messageText, start, length} = ngDiags[0];
    const keyword = `() => 'foo'`;
    expect(start).toBe(content.lastIndexOf(keyword));
    expect(length).toBe(keyword.length);
    // messageText is a three-part chain
    const firstPart = messageText as ts.DiagnosticMessageChain;
    expect(firstPart.messageText).toBe(`Error during template compile of 'AppComponent'`);
    const secondPart = firstPart.next !;
    expect(secondPart[0].messageText).toBe('Function expressions are not supported in decorators');
    const thirdPart = secondPart[0].next !;
    expect(thirdPart[0].messageText)
        .toBe('Consider changing the function expression into an exported function');
    expect(thirdPart[0].next).toBeFalsy();
  });
  it('should not throw for an invalid class', () => {
    const fileName = mockHost.addCode(`
      @Component({
        template: ''
      }) class`);
    expect(() => ngLS.getDiagnostics(fileName)).not.toThrow();
  });
  it('should not report an error for sub-types of string in non-strict mode', () => {
    mockHost.override(APP_COMPONENT, `
      import { Component } from '@angular/core';
      @Component({
        template: \`\`
      })
      export class AppComponent {
        something: 'foo' | 'bar';
      }`);
    mockHost.overrideOptions({
      strict: false,  // TODO: this test fails in strict mode
    });
    const tsDiags = tsLS.getSemanticDiagnostics(APP_COMPONENT);
    expect(tsDiags).toEqual([]);
    const ngDiags = ngLS.getDiagnostics(APP_COMPONENT);
    expect(ngDiags).toEqual([]);
  });
  it('should not report an error for sub-types of number in non-strict mode', () => {
    mockHost.override(APP_COMPONENT, `
      import { Component } from '@angular/core';
      @Component({
        template: ''
      })
      export class AppComponent {
        something: 123 | 456;
      }`);
    mockHost.overrideOptions({
      strict: false,  // TODO: This test fails in strict mode
    });
    const tsDiags = tsLS.getSemanticDiagnostics(APP_COMPONENT);
    expect(tsDiags).toEqual([]);
    const ngDiags = ngLS.getDiagnostics(APP_COMPONENT);
    expect(ngDiags).toEqual([]);
  });
  it('should report a warning if an event results in a callable expression', () => {
    const content = mockHost.override(APP_COMPONENT, `
      import { Component } from '@angular/core';
      @Component({
        template: ''
      })
      export class AppComponent {
        onClick() { }
      }`);
    const diagnostics = ngLS.getDiagnostics(APP_COMPONENT) !;
    const {messageText, start, length} = diagnostics[0];
    expect(messageText).toBe('Unexpected callable expression. Expected a method call');
    const keyword = `"onClick"`;
    expect(start).toBe(content.lastIndexOf(keyword) + 1);  // exclude leading quote
    expect(length).toBe(keyword.length - 2);               // exclude leading and trailing quotes
  });
  // #13412
  it('should not report an error for using undefined under non-strict mode', () => {
    mockHost.override(APP_COMPONENT, `
      import { Component } from '@angular/core';
      @Component({
        template: ''
      })
      export class AppComponent {
        something = 'foo';
      }`);
    mockHost.overrideOptions({
      strict: false,  // TODO: This test fails in strict mode
    });
    const tsDiags = tsLS.getSemanticDiagnostics(APP_COMPONENT);
    expect(tsDiags).toEqual([]);
    const ngDiags = ngLS.getDiagnostics(APP_COMPONENT);
    expect(ngDiags).toEqual([]);
  });
  // Issue #13326
  it('should report a narrow span for invalid pipes', () => {
    const content = mockHost.override(APP_COMPONENT, `
      import { Component } from '@angular/core';
      @Component({
        template: ' Using an invalid pipe {{data | dat}} 
'
      })
      export class AppComponent {
        data = 'some data';
      }`);
    const tsDiags = tsLS.getSemanticDiagnostics(APP_COMPONENT);
    expect(tsDiags).toEqual([]);
    const ngDiags = ngLS.getDiagnostics(APP_COMPONENT);
    expect(ngDiags.length).toBe(1);
    const {messageText, start, length} = ngDiags[0];
    expect(messageText).toBe(`The pipe 'dat' could not be found`);
    const keyword = 'data | dat';
    expect(start).toBe(content.lastIndexOf(keyword));
    expect(length).toBe(keyword.length);
  });
  // Issue #19406
  it('should allow empty template', () => {
    mockHost.override(APP_COMPONENT, `
      import { Component } from '@angular/core';
      @Component({
        template : '',
      })
      export class AppComponent {}`);
    const tsDiags = tsLS.getSemanticDiagnostics(APP_COMPONENT);
    expect(tsDiags).toEqual([]);
    const ngDiags = ngLS.getDiagnostics(APP_COMPONENT);
    expect(ngDiags).toEqual([]);
  });
  // Issue #15460
  it('should be able to find members defined on an ancestor type', () => {
    mockHost.override(APP_COMPONENT, `
      import { Component } from '@angular/core';
      import { NgForm } from '@angular/forms';
      @Component({
        selector: 'example-app',
        template: \`
           
          First name value: {{ first.value }}
          First name valid: {{ first.valid }}
          Form value: {{ f.value | json }}
          Form valid: {{ f.valid }}
       \`,
      })
      export class AppComponent {
        onSubmit(form: NgForm) {}
      }`);
    const tsDiags = tsLS.getSemanticDiagnostics(APP_COMPONENT);
    expect(tsDiags).toEqual([]);
    const ngDiags = ngLS.getDiagnostics(APP_COMPONENT);
    expect(ngDiags).toEqual([]);
  });
  it('should report an error for invalid providers', () => {
    const content = mockHost.override(APP_COMPONENT, `
      import { Component } from '@angular/core';
      @Component({
        template: '',
        providers: [null]
      })
      export class AppComponent {}`);
    const tsDiags = tsLS.getSemanticDiagnostics(APP_COMPONENT);
    expect(tsDiags.length).toBe(1);
    const msgText = ts.flattenDiagnosticMessageText(tsDiags[0].messageText, '\n');
    expect(msgText).toBe(
        `Type 'null[]' is not assignable to type 'Provider[]'.\n  Type 'null' is not assignable to type 'Provider'.`);
    const ngDiags = ngLS.getDiagnostics(APP_COMPONENT);
    expect(ngDiags.length).toBe(1);
    const {messageText, start, length} = ngDiags[0];
    expect(messageText)
        .toBe(
            'Invalid providers for "AppComponent in /app/app.component.ts" - only instances of Provider and Type are allowed, got: [?null?]');
    // TODO: Looks like this is the wrong span. Should point to 'null' instead.
    const keyword = '@Component';
    expect(start).toBe(content.lastIndexOf(keyword) + 1);  // exclude leading '@'
    expect(length).toBe(keyword.length - 1);               // exclude leading '@
  });
  // Issue #15768
  it('should be able to parse a template reference', () => {
    mockHost.override(APP_COMPONENT, `
      import { Component } from '@angular/core';
      @Component({
        selector: 'my-component',
        template: \`
          
          
          Loading comps...
        \`
      })
      export class AppComponent {}`);
    const tsDiags = tsLS.getSemanticDiagnostics(APP_COMPONENT);
    expect(tsDiags).toEqual([]);
    const ngDiags = ngLS.getDiagnostics(APP_COMPONENT);
    expect(ngDiags).toEqual([]);
  });
  // Issue #15625
  it('should not report errors for localization syntax', () => {
    mockHost.override(APP_COMPONENT, `
      import { Component } from '@angular/core';
      @Component({
        selector: 'my-component',
        template: \`
        
            {fieldCount, plural, =0 {no fields} =1 {1 field} other {{{fieldCount}} fields}}
        
        \`
      })
      export class AppComponent {
        fieldCount: number = 0;
      }`);
    const tsDiags = tsLS.getSemanticDiagnostics(APP_COMPONENT);
    expect(tsDiags).toEqual([]);
    const ngDiags = ngLS.getDiagnostics(APP_COMPONENT);
    expect(ngDiags).toEqual([]);
  });
  // Issue #15885
  it('should be able to remove null and undefined from a type', () => {
    mockHost.overrideOptions({
      strictNullChecks: true,
    });
    mockHost.override(APP_COMPONENT, `
      import { Component } from '@angular/core';
      @Component({
        selector: 'my-component',
        template: '{{test?.a}}',
      })
      export class AppComponent {
        test: {a: number, b: number} | null = {
          a: 1,
          b: 2,
        };
      }`);
    const tsDiags = tsLS.getSemanticDiagnostics(APP_COMPONENT);
    expect(tsDiags).toEqual([]);
    const ngDiags = ngLS.getDiagnostics(APP_COMPONENT);
    expect(ngDiags).toEqual([]);
  });
  it('should be able to resolve modules using baseUrl', () => {
    mockHost.override(APP_COMPONENT, `
      import { Component } from '@angular/core';
      import { NgForm } from '@angular/forms';
      import { Server } from 'app/server';
      @Component({
        selector: 'example-app',
        template: '...',
        providers: [Server]
      })
      export class AppComponent {
        onSubmit(form: NgForm) {}
      }`);
    mockHost.addScript('/other/files/app/server.ts', 'export class Server {}');
    mockHost.overrideOptions({
      baseUrl: '/other/files',
    });
    const tsDiags = tsLS.getSemanticDiagnostics(APP_COMPONENT);
    expect(tsDiags).toEqual([]);
    const diagnostic = ngLS.getDiagnostics(APP_COMPONENT);
    expect(diagnostic).toEqual([]);
  });
  it('should report errors for using the now removed OpaqueToken (deprecated)', () => {
    mockHost.override(APP_COMPONENT, `
      import { Component, Inject, OpaqueToken } from '@angular/core';
      import { NgForm } from '@angular/forms';
      export const token = new OpaqueToken('some token');
      @Component({
        selector: 'example-app',
        template: '...'
      })
      export class AppComponent {
        constructor (@Inject(token) value: string) {}
        onSubmit(form: NgForm) {}
      }`);
    const tsDiags = tsLS.getSemanticDiagnostics(APP_COMPONENT);
    expect(tsDiags.length).toBe(1);
    expect(tsDiags[0].messageText)
        .toBe(
            `Module '"../node_modules/@angular/core/core"' has no exported member 'OpaqueToken'.`);
  });
  describe('templates', () => {
    it('should report errors for invalid templateUrls', () => {
      const fileName = mockHost.addCode(`
        @Component({
          templateUrl: '«notAFile»',
        })
        export class MyComponent {}`);
      const marker = mockHost.getReferenceMarkerFor(fileName, 'notAFile');
      const diagnostics = ngLS.getDiagnostics(fileName) !;
      const urlDiagnostic =
          diagnostics.find(d => d.messageText === 'URL does not point to a valid file');
      expect(urlDiagnostic).toBeDefined();
      const {start, length} = urlDiagnostic !;
      expect(start).toBe(marker.start);
      expect(length).toBe(marker.length);
    });
    it('should not report errors for valid templateUrls', () => {
      const fileName = mockHost.addCode(`
        @Component({
          templateUrl: './test.ng',
        })
        export class MyComponent {}`);
      const diagnostics = ngLS.getDiagnostics(fileName) !;
      const urlDiagnostic =
          diagnostics.find(d => d.messageText === 'URL does not point to a valid file');
      expect(urlDiagnostic).toBeUndefined();
    });
    it('should report diagnostic for missing template or templateUrl', () => {
      const content = mockHost.override(APP_COMPONENT, `
        import {Component} from '@angular/core';
        @Component({
          selector: 'app-example',
        })
        export class AppComponent {}`);
      const diags = ngLS.getDiagnostics(APP_COMPONENT);
      expect(diags.length).toBe(1);
      const {file, messageText, start, length} = diags[0];
      expect(file !.fileName).toBe(APP_COMPONENT);
      expect(messageText).toBe(`Component 'AppComponent' must have a template or templateUrl`);
      expect(start).toBe(content.indexOf(`@Component`) + 1);
      expect(length).toBe('Component'.length);
    });
    it('should report diagnostic for both template and templateUrl', () => {
      const content = mockHost.override(APP_COMPONENT, `
        import {Component} from '@angular/core';
        @Component({
          selector: 'app-example',
          template: '',
          templateUrl: './test.ng',
        })
        export class AppComponent {}`);
      const diags = ngLS.getDiagnostics(APP_COMPONENT);
      expect(diags.length).toBe(1);
      const {file, messageText, start, length} = diags[0];
      expect(file !.fileName).toBe(APP_COMPONENT);
      expect(messageText)
          .toBe(`Component 'AppComponent' must not have both template and templateUrl`);
      expect(start).toBe(content.indexOf(`@Component`) + 1);
      expect(length).toBe('Component'.length);
    });
    it('should report errors for invalid styleUrls', () => {
      const fileName = mockHost.addCode(`
        @Component({
          styleUrls: ['«notAFile»'],
        })
        export class MyComponent {}`);
      const marker = mockHost.getReferenceMarkerFor(fileName, 'notAFile');
      const diagnostics = ngLS.getDiagnostics(fileName) !;
      const urlDiagnostic =
          diagnostics.find(d => d.messageText === 'URL does not point to a valid file');
      expect(urlDiagnostic).toBeDefined();
      const {start, length} = urlDiagnostic !;
      expect(start).toBe(marker.start);
      expect(length).toBe(marker.length);
    });
    it('should not report errors for valid styleUrls', () => {
      mockHost.override(APP_COMPONENT, `
        import {Component} from '@angular/core';
        @Component({
          template: '',
          styleUrls: ['./test.css', './test.css'],
        })
        export class AppComponent {}`);
      const diagnostics = ngLS.getDiagnostics(APP_COMPONENT) !;
      expect(diagnostics.length).toBe(0);
    });
  });
  it('should work correctly with CRLF endings in external template', () => {
    // https://github.com/angular/vscode-ng-language-service/issues/235
    // In the example below, the string
    // `\r\n{{line0}}\r\n{{line1}}\r\n{{line2}}` is tokenized as a whole,
    // and then CRLF characters are converted to LF.
    // Source span information is lost in the process.
    const content = mockHost.override(
        TEST_TEMPLATE, '\r\n\r\n{{line0}}\r\n{{line1}}\r\n{{line2}}\r\n
');
    const ngDiags = ngLS.getDiagnostics(TEST_TEMPLATE);
    expect(ngDiags.length).toBe(3);
    for (let i = 0; i < 3; ++i) {
      const {messageText, start, length} = ngDiags[i];
      expect(messageText)
          .toBe(
              `Identifier 'line${i}' is not defined. ` +
              `The component declaration, template variable declarations, and ` +
              `element references do not contain such a member`);
      // Assert that the span is actually highlight the bounded text. The span
      // would be off if CRLF endings are not handled properly.
      expect(content.substring(start !, start ! + length !)).toBe(`line${i}`);
    }
  });
  it('should work correctly with CRLF endings in inline template', () => {
    const fileName = mockHost.addCode(
        '\n@Component({template:`\r\n\r\n{{line}}`})export class ComponentCRLF {}');
    const content = mockHost.readFile(fileName) !;
    const ngDiags = ngLS.getDiagnostics(fileName);
    expect(ngDiags.length).toBeGreaterThan(0);
    const {messageText, start, length} = ngDiags[0];
    expect(messageText)
        .toBe(
            `Identifier 'line' is not defined. ` +
            `The component declaration, template variable declarations, and ` +
            `element references do not contain such a member`);
    expect(content.substring(start !, start ! + length !)).toBe('line');
  });
  it('should not produce diagnostics for non-exported directives', () => {
    const fileName = '/app/test.component.ts';
    mockHost.addScript(fileName, `
      import {Component} from '@angular/core';
      @Component({
        template: ''
      })
      class TestHostComponent {}
    `);
    const tsDiags = tsLS.getSemanticDiagnostics(fileName);
    expect(tsDiags).toEqual([]);
    const ngDiags = ngLS.getDiagnostics(fileName);
    expect(ngDiags).toEqual([]);
  });
});