/**
 * @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 {Reference} from '../../../src/ngtsc/imports';
import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry';
import {PrivateDeclarationsAnalyzer} from '../../src/analysis/private_declarations_analyzer';
import {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
import {MockLogger} from '../helpers/mock_logger';
import {getDeclaration, makeTestBundleProgram, makeTestProgram} from '../helpers/utils';

describe('PrivateDeclarationsAnalyzer', () => {
  describe('analyzeProgram()', () => {

    const TEST_PROGRAM = [
      {
        name: '/src/entry_point.js',
        isRoot: true,
        contents: `
    export {PublicComponent} from './a';
    export {ModuleA} from './mod';
    export {ModuleB} from './b';
  `
      },
      {
        name: '/src/a.js',
        isRoot: false,
        contents: `
    import {Component} from '@angular/core';
    export class PublicComponent {}
    PublicComponent.decorators = [
      {type: Component, args: [{selectors: 'a', template: ''}]}
    ];
  `
      },
      {
        name: '/src/b.js',
        isRoot: false,
        contents: `
    import {Component, NgModule} from '@angular/core';
    class PrivateComponent1 {}
    PrivateComponent1.decorators = [
      {type: Component, args: [{selectors: 'b', template: ''}]}
    ];
    class PrivateComponent2 {}
    PrivateComponent2.decorators = [
      {type: Component, args: [{selectors: 'c', template: ''}]}
    ];
    export class ModuleB {}
    ModuleB.decorators = [
      {type: NgModule, args: [{declarations: [PrivateComponent1]}]}
    ];
  `
      },
      {
        name: '/src/c.js',
        isRoot: false,
        contents: `
    import {Component} from '@angular/core';
    export class InternalComponent1 {}
    InternalComponent1.decorators = [
      {type: Component, args: [{selectors: 'd', template: ''}]}
    ];
    export class InternalComponent2 {}
    InternalComponent2.decorators = [
      {type: Component, args: [{selectors: 'e', template: ''}]}
    ];
  `
      },
      {
        name: '/src/mod.js',
        isRoot: false,
        contents: `
    import {Component, NgModule} from '@angular/core';
    import {PublicComponent} from './a';
    import {ModuleB} from './b';
    import {InternalComponent1} from './c';
    export class ModuleA {}
    ModuleA.decorators = [
      {type: NgModule, args: [{
        declarations: [PublicComponent, InternalComponent1],
        imports: [ModuleB]
      }]}
    ];
  `
      }
    ];
    const TEST_DTS_PROGRAM = [
      {
        name: '/typings/entry_point.d.ts',
        isRoot: true,
        contents: `
    export {PublicComponent} from './a';
    export {ModuleA} from './mod';
    export {ModuleB} from './b';
  `
      },
      {
        name: '/typings/a.d.ts',
        isRoot: false,
        contents: `
    export declare class PublicComponent {}
  `
      },
      {
        name: '/typings/b.d.ts',
        isRoot: false,
        contents: `
    export declare class ModuleB {}
  `
      },
      {
        name: '/typings/c.d.ts',
        isRoot: false,
        contents: `
    export declare class InternalComponent1 {}
  `
      },
      {
        name: '/typings/mod.d.ts',
        isRoot: false,
        contents: `
    import {PublicComponent} from './a';
    import {ModuleB} from './b';
    import {InternalComponent1} from './c';
    export declare class ModuleA {}
  `
      },
    ];

    it('should find all NgModule declarations that were not publicly exported from the entry-point',
       () => {
         const {program, referencesRegistry, analyzer} = setup(TEST_PROGRAM, TEST_DTS_PROGRAM);

         addToReferencesRegistry(program, referencesRegistry, '/src/a.js', 'PublicComponent');
         addToReferencesRegistry(program, referencesRegistry, '/src/b.js', 'PrivateComponent1');
         addToReferencesRegistry(program, referencesRegistry, '/src/c.js', 'InternalComponent1');

         const analyses = analyzer.analyzeProgram(program);
         // Note that `PrivateComponent2` and `InternalComponent2` are not found because they are
         // not added to the ReferencesRegistry (i.e. they were not declared in an NgModule).
         expect(analyses.length).toEqual(2);
         expect(analyses).toEqual([
           {identifier: 'PrivateComponent1', from: '/src/b.js', dtsFrom: null, alias: null},
           {
             identifier: 'InternalComponent1',
             from: '/src/c.js',
             dtsFrom: '/typings/c.d.ts',
             alias: null
           },
         ]);
       });

    const ALIASED_EXPORTS_PROGRAM = [
      {
        name: '/src/entry_point.js',
        isRoot: true,
        contents: `
        // This component is only exported as an alias.
        export {ComponentOne as aliasedComponentOne} from './a';
        // This component is exported both as itself and an alias.
        export {ComponentTwo as aliasedComponentTwo, ComponentTwo} from './a';
      `
      },
      {
        name: '/src/a.js',
        isRoot: false,
        contents: `
      import {Component} from '@angular/core';
      export class ComponentOne {}
      ComponentOne.decorators = [
        {type: Component, args: [{selectors: 'a', template: ''}]}
      ];

      export class ComponentTwo {}
      Component.decorators = [
        {type: Component, args: [{selectors: 'a', template: ''}]}
      ];
    `
      }
    ];
    const ALIASED_EXPORTS_DTS_PROGRAM = [
      {
        name: '/typings/entry_point.d.ts',
        isRoot: true,
        contents: `
        export declare class aliasedComponentOne {}
        export declare class ComponentTwo {}
        export {ComponentTwo as aliasedComponentTwo}
      `
      },
    ];

    it('should find all non-public declarations that were aliased', () => {
      const {program, referencesRegistry, analyzer} =
          setup(ALIASED_EXPORTS_PROGRAM, ALIASED_EXPORTS_DTS_PROGRAM);

      addToReferencesRegistry(program, referencesRegistry, '/src/a.js', 'ComponentOne');
      addToReferencesRegistry(program, referencesRegistry, '/src/a.js', 'ComponentTwo');

      const analyses = analyzer.analyzeProgram(program);
      expect(analyses).toEqual([{
        identifier: 'ComponentOne',
        from: '/src/a.js',
        dtsFrom: null,
        alias: 'aliasedComponentOne',
      }]);
    });
  });
});

type Files = {
  name: string,
  contents: string, isRoot?: boolean | undefined
}[];

function setup(jsProgram: Files, dtsProgram: Files) {
  const program = makeTestProgram(...jsProgram);
  const dts = makeTestBundleProgram(dtsProgram);
  const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker(), dts);
  const referencesRegistry = new NgccReferencesRegistry(host);
  const analyzer = new PrivateDeclarationsAnalyzer(host, referencesRegistry);
  return {program, referencesRegistry, analyzer};
}

/**
 * Add up the named component to the references registry.
 *
 * This would normally be done by the decoration handlers in the `DecorationAnalyzer`.
 */
function addToReferencesRegistry(
    program: ts.Program, registry: NgccReferencesRegistry, fileName: string,
    componentName: string) {
  const declaration = getDeclaration(program, fileName, componentName, ts.isClassDeclaration);
  registry.add(null !, new Reference(declaration));
}
