/**
 * @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.io/license
 */
/// 
import {inspect} from 'util';
import {runInEachFileSystem} from '../../src/ngtsc/file_system/testing';
import {loadStandardTestFiles} from '../../src/ngtsc/testing';
import {tsSourceMapBug29300Fixed} from '../../src/ngtsc/util/src/ts_source_map_bug_29300';
import {NgtscTestEnvironment} from './env';
import {getMappedSegments, SegmentMapping} from './sourcemap_utils';
const testFiles = loadStandardTestFiles();
runInEachFileSystem((os) => {
  describe('template source-mapping', () => {
    let env!: NgtscTestEnvironment;
    beforeEach(() => {
      env = NgtscTestEnvironment.setup(testFiles);
      env.tsconfig({sourceMap: true, target: 'es2015', enableI18nLegacyMessageIdFormat: false});
    });
    describe('Inline templates', () => {
      describe('(element creation)', () => {
        it('should map simple element with content', () => {
          const mappings = compileAndMap('
', generated: 'i0.ɵɵelementStart(0, "div")', sourceUrl: '../test.ts'});
          expectMapping(mappings, {
            source: '{{200.3 | percent : 2 }}',
            generated: 'i0.ɵɵtextInterpolate(i0.ɵɵpipeBind2(2, 1, 200.3, 2))',
            sourceUrl: '../test.ts'
          });
          expectMapping(
              mappings,
              {source: '
', generated: 'i0.ɵɵelementEnd()', sourceUrl: '../test.ts'});
        });
      });
      describe('(property bindings)', () => {
        it('should map a simple input binding expression', () => {
          const mappings = compileAndMap('',
            generated: 'i0.ɵɵelementStart(0, "div")',
            sourceUrl: '../test.ts'
          });
          // TODO: Add better mappings for binding
          expectMapping(
              mappings,
              {source: 'Message', generated: 'i0.ɵɵtext(1, "Message")', sourceUrl: '../test.ts'});
          expectMapping(
              mappings,
              {source: '
', generated: 'i0.ɵɵelementEnd()', sourceUrl: '../test.ts'});
        });
      });
      describe('(structural directives)', () => {
        it('should map *ngIf scenario', () => {
          const mappings = compileAndMap('',
            generated: 'i0.ɵɵelementStart(0, "div")',
            sourceUrl: '../test.ts'
          });
          // TODO - map the bindings better
          expectMapping(
              mappings,
              {source: '
', generated: 'i0.ɵɵelementEnd()', sourceUrl: '../test.ts'});
          // TODO: the `ctx_r...` appears to be dependent upon previous tests!!!
          // expectMapping(mappings, {
          //   source: '{{ name }}',
          //   generated: 'i0.ɵɵtextInterpolate(ctx_r0.name)',
          //   sourceUrl: '../test.ts'
          // });
        });
        it('should map ng-template [ngIf] scenario', () => {
          const mappings = compileAndMap(
              `', generated: 'i0.ɵɵelementStart(0, "div")', sourceUrl: '../test.ts'});
          // TODO - map the bindings better
          expectMapping(
              mappings,
              {source: '
', generated: 'i0.ɵɵelementEnd()', sourceUrl: '../test.ts'});
          // TODO: the `ctx_r...` appears to be dependent upon previous tests!!!
          // expectMapping(mappings, {
          //   source: '{{ name }}',
          //   generated: 'i0.ɵɵtextInterpolate(ctx_r0.name)',
          //   sourceUrl: '../test.ts'
          // });
        });
        it('should map *ngFor scenario', () => {
          const mappings = compileAndMap(
              '',
            generated: 'i0.ɵɵelementStart(0, "div")',
            sourceUrl: '../test.ts'
          });
          // TODO - map the bindings better
          expectMapping(
              mappings,
              {source: '
', generated: 'i0.ɵɵelementEnd()', sourceUrl: '../test.ts'});
        });
        it('should map ng-template [ngFor] scenario', () => {
          const mappings = compileAndMap(
              `', generated: 'i0.ɵɵelementStart(2, "div")', sourceUrl: '../test.ts'});
          expectMapping(mappings, {
            source: '',
            generated: 'i0.ɵɵprojection(3, 1)',
            sourceUrl: '../test.ts'
          });
          expectMapping(
              mappings,
              {source: '
', generated: 'i0.ɵɵelementEnd()', sourceUrl: '../test.ts'});
        });
      });
      describe('$localize', () => {
        it('should create simple i18n message source-mapping', () => {
          const mappings = compileAndMap(`',
            generated: 'i0.ɵɵelementStart(0, "div")',
            sourceUrl: '../test.ts',
          });
          expectMapping(mappings, {
            source: 'Hello, World!',
            generated: '`Hello, World!`',
            sourceUrl: '../test.ts',
          });
        });
        it('should create placeholder source-mappings', () => {
          const mappings = compileAndMap(`
Hello, {{name}}!
`);
          expectMapping(mappings, {
            source: '
',
            generated: 'i0.ɵɵelementStart(0, "div")',
            sourceUrl: '../test.ts',
          });
          expectMapping(mappings, {
            source: '
',
            generated: 'i0.ɵɵelementEnd()',
            sourceUrl: '../test.ts',
          });
          expectMapping(mappings, {
            source: 'Hello, ',
            generated: '`Hello, ${',
            sourceUrl: '../test.ts',
          });
          expectMapping(mappings, {
            source: '{{name}}',
            generated: '"\\uFFFD0\\uFFFD"',
            sourceUrl: '../test.ts',
          });
          expectMapping(mappings, {
            source: '!',
            generated: '}:INTERPOLATION:!`',
            sourceUrl: '../test.ts',
          });
        });
        it('should correctly handle collapsed whitespace in interpolation placeholder source-mappings',
           () => {
             const mappings = compileAndMap(
                 `
  pre-body {{body_value}}  post-body
`);
             expectMapping(mappings, {
               source: '
  ',
               generated: 'i0.ɵɵelementStart(0, "div", 0)',
               sourceUrl: '../test.ts',
             });
             expectMapping(mappings, {
               source: '
',
               generated: 'i0.ɵɵelementEnd()',
               sourceUrl: '../test.ts',
             });
             expectMapping(mappings, {
               source: '  pre-body ',
               generated: '` pre-body ${',
               sourceUrl: '../test.ts',
             });
             expectMapping(mappings, {
               source: '{{body_value}}',
               generated: '"\\uFFFD0\\uFFFD"',
               sourceUrl: '../test.ts',
             });
             expectMapping(mappings, {
               source: '  post-body',
               generated: '}:INTERPOLATION: post-body`',
               sourceUrl: '../test.ts',
             });
           });
        it('should correctly handle collapsed whitespace in element placeholder source-mappings',
           () => {
             const mappings =
                 compileAndMap(`
\n  pre-p\n  
\n    in-p\n  
\n  post-p\n
`);
             // $localize expressions
             expectMapping(mappings, {
               sourceUrl: '../test.ts',
               source: 'pre-p\n  ',
               generated: '` pre-p ${',
             });
             expectMapping(mappings, {
               sourceUrl: '../test.ts',
               source: '
\n    ',
               generated: '"\\uFFFD#2\\uFFFD"',
             });
             expectMapping(mappings, {
               sourceUrl: '../test.ts',
               source: 'in-p\n  ',
               generated: '}:START_PARAGRAPH: in-p ${',
             });
             expectMapping(mappings, {
               sourceUrl: '../test.ts',
               source: '
\n  ',
               generated: '"\\uFFFD/#2\\uFFFD"',
             });
             expectMapping(mappings, {
               sourceUrl: '../test.ts',
               source: 'post-p\n',
               generated: '}:CLOSE_PARAGRAPH: post-p\n`',
             });
             // ivy instructions
             expectMapping(mappings, {
               sourceUrl: '../test.ts',
               source: '
\n  ',
               generated: 'i0.ɵɵelementStart(0, "div")',
             });
             expectMapping(mappings, {
               sourceUrl: '../test.ts',
               source: '
\n  ',
               generated: 'i0.ɵɵi18nStart(1, 0)',
             });
             expectMapping(mappings, {
               sourceUrl: '../test.ts',
               source: '
\n    in-p\n  
',
               generated: 'i0.ɵɵelement(2, "p")',
             });
             expectMapping(mappings, {
               sourceUrl: '../test.ts',
               source: '
',
               generated: 'i0.ɵɵi18nEnd()',
             });
             expectMapping(mappings, {
               sourceUrl: '../test.ts',
               source: '
',
               generated: 'i0.ɵɵelementEnd()',
             });
           });
        it('should create tag (container) placeholder source-mappings', () => {
          const mappings = compileAndMap(`
Hello, World!
`);
          expectMapping(mappings, {
            source: '
',
            generated: 'i0.ɵɵelementStart(0, "div")',
            sourceUrl: '../test.ts',
          });
          expectMapping(mappings, {
            source: '
',
            generated: 'i0.ɵɵelementEnd()',
            sourceUrl: '../test.ts',
          });
          expectMapping(mappings, {
            source: 'Hello, ',
            generated: '`Hello, ${',
            sourceUrl: '../test.ts',
          });
          expectMapping(mappings, {
            source: '
',
            generated: '"\\uFFFD#2\\uFFFD"',
            sourceUrl: '../test.ts',
          });
          expectMapping(mappings, {
            source: 'World',
            generated: '}:START_BOLD_TEXT:World${',
            sourceUrl: '../test.ts',
          });
          expectMapping(mappings, {
            source: '',
            generated: '"\\uFFFD/#2\\uFFFD"',
            sourceUrl: '../test.ts',
          });
          expectMapping(mappings, {
            source: '!',
            generated: '}:CLOSE_BOLD_TEXT:!`',
            sourceUrl: '../test.ts',
          });
        });
      });
      it('should create (simple string) inline template source-mapping', () => {
        const mappings = compileAndMap('
this is a test
{{ 1 + 2 }}
');
        // Creation mode
        expectMapping(
            mappings,
            {generated: 'i0.ɵɵelementStart(0, "div")', source: '
', sourceUrl: '../test.ts'});
        expectMapping(mappings, {
          generated: 'i0.ɵɵtext(1, "this is a test")',
          source: 'this is a test',
          sourceUrl: '../test.ts'
        });
        expectMapping(
            mappings, {generated: 'i0.ɵɵelementEnd()', source: '
', sourceUrl: '../test.ts'});
        expectMapping(
            mappings,
            {generated: 'i0.ɵɵelementStart(2, "div")', source: '
', sourceUrl: '../test.ts'});
        expectMapping(
            mappings, {generated: 'i0.ɵɵtext(3)', source: '{{ 1 + 2 }}', sourceUrl: '../test.ts'});
        expectMapping(
            mappings, {generated: 'i0.ɵɵelementEnd()', source: '
', sourceUrl: '../test.ts'});
        // Update mode
        expectMapping(mappings, {
          generated: 'i0.ɵɵtextInterpolate(1 + 2)',
          source: '{{ 1 + 2 }}',
          sourceUrl: '../test.ts'
        });
      });
      it('should create (simple backtick string) inline template source-mapping', () => {
        const mappings = compileAndMap('
this is a test
{{ 1 + 2 }}
');
        // Creation mode
        expectMapping(
            mappings,
            {generated: 'i0.ɵɵelementStart(0, "div")', source: '
', sourceUrl: '../test.ts'});
        expectMapping(mappings, {
          generated: 'i0.ɵɵtext(1, "this is a test")',
          source: 'this is a test',
          sourceUrl: '../test.ts'
        });
        expectMapping(
            mappings, {generated: 'i0.ɵɵelementEnd()', source: '
', sourceUrl: '../test.ts'});
        expectMapping(
            mappings,
            {generated: 'i0.ɵɵelementStart(2, "div")', source: '
', sourceUrl: '../test.ts'});
        expectMapping(
            mappings, {generated: 'i0.ɵɵtext(3)', source: '{{ 1 + 2 }}', sourceUrl: '../test.ts'});
        expectMapping(
            mappings, {generated: 'i0.ɵɵelementEnd()', source: '
', sourceUrl: '../test.ts'});
        // TODO(benlesh): We need to circle back and prevent the extra parens from being generated.
        // Update mode
        expectMapping(mappings, {
          generated: 'i0.ɵɵtextInterpolate(1 + 2)',
          source: '{{ 1 + 2 }}',
          sourceUrl: '../test.ts'
        });
      });
      it('should create correct inline template source-mapping when the source contains escape sequences',
         () => {
           // Note that the escaped double quotes, which need un-escaping to be parsed correctly.
           const mappings = compileAndMap('
this is a test
');
           expectMapping(mappings, {
             generated: 'i0.ɵɵelementStart(0, "div", 0)',
             source: '
',
             sourceUrl: '../test.ts'
           });
           const attrsMapping =
               mappings.find(mapping => /consts: \[\[1, "some-class"\]\]/.test(mapping.generated));
           expect(attrsMapping).toBeDefined();
         });
    });
    if (tsSourceMapBug29300Fixed()) {
      describe('External templates (where TS supports source-mapping)', () => {
        it('should create external template source-mapping', () => {
          const mappings =
              compileAndMap('
this is a test
{{ 1 + 2 }}
', './dir/test.html');
          // Creation mode
          expectMapping(mappings, {
            generated: 'i0.ɵɵelementStart(0, "div")',
            source: '
',
            sourceUrl: '../dir/test.html'
          });
          expectMapping(mappings, {
            generated: 'i0.ɵɵtext(1, "this is a test")',
            source: 'this is a test',
            sourceUrl: '../dir/test.html'
          });
          expectMapping(
              mappings,
              {generated: 'i0.ɵɵelementEnd()', source: '
', sourceUrl: '../dir/test.html'});
          expectMapping(mappings, {
            generated: 'i0.ɵɵelementStart(2, "div")',
            source: '
',
            sourceUrl: '../dir/test.html'
          });
          expectMapping(
              mappings,
              {generated: 'i0.ɵɵtext(3)', source: '{{ 1 + 2 }}', sourceUrl: '../dir/test.html'});
          expectMapping(
              mappings,
              {generated: 'i0.ɵɵelementEnd()', source: '
', sourceUrl: '../dir/test.html'});
          // Update mode
          expectMapping(mappings, {
            generated: 'i0.ɵɵtextInterpolate(1 + 2)',
            source: '{{ 1 + 2 }}',
            sourceUrl: '../dir/test.html'
          });
        });
        it('should create correct mappings when templateUrl is in a different rootDir', () => {
          const mappings = compileAndMap(
              '
this is a test
{{ 1 + 2 }}
', 'extraRootDir/test.html');
          // Creation mode
          expectMapping(mappings, {
            generated: 'i0.ɵɵelementStart(0, "div")',
            source: '
',
            sourceUrl: '../extraRootDir/test.html'
          });
          expectMapping(mappings, {
            generated: 'i0.ɵɵtext(1, "this is a test")',
            source: 'this is a test',
            sourceUrl: '../extraRootDir/test.html'
          });
          expectMapping(mappings, {
            generated: 'i0.ɵɵelementEnd()',
            source: '
',
            sourceUrl: '../extraRootDir/test.html'
          });
          expectMapping(mappings, {
            generated: 'i0.ɵɵelementStart(2, "div")',
            source: '
',
            sourceUrl: '../extraRootDir/test.html'
          });
          expectMapping(mappings, {
            generated: 'i0.ɵɵtext(3)',
            source: '{{ 1 + 2 }}',
            sourceUrl: '../extraRootDir/test.html'
          });
          expectMapping(mappings, {
            generated: 'i0.ɵɵelementEnd()',
            source: '
',
            sourceUrl: '../extraRootDir/test.html'
          });
          // Update mode
          expectMapping(mappings, {
            generated: 'i0.ɵɵtextInterpolate(1 + 2)',
            source: '{{ 1 + 2 }}',
            sourceUrl: '../extraRootDir/test.html'
          });
        });
      });
    }
    function compileAndMap(template: string, templateUrl: string|null = null) {
      const templateConfig = templateUrl ? `templateUrl: '${templateUrl}'` :
                                           ('template: `' + template.replace(/`/g, '\\`') + '`');
      env.write('test.ts', `
        import {Component} from '@angular/core';
        @Component({
          selector: 'test-cmp',
          ${templateConfig}
        })
        export class TestCmp {}
    `);
      if (templateUrl) {
        env.write(templateUrl, template);
      }
      env.driveMain();
      return getMappedSegments(env, 'test.js');
    }
    /**
     * Helper function for debugging failed mappings.
     * This lays out the segment mappings in the console to make it easier to compare.
     */
    function dumpMappings(mappings: SegmentMapping[]) {
      mappings.forEach(map => {
        // tslint:disable-next-line:no-console
        console.log(
            padValue(map.sourceUrl, 20, 0) + ' : ' + padValue(inspect(map.source), 100, 23) +
            ' : ' + inspect(map.generated));
      });
      function padValue(value: string, max: number, start: number) {
        const padding = value.length > max ? ('\n' +
                                              ' '.repeat(max + start)) :
                                             ' '.repeat(max - value.length);
        return value + padding;
      }
    }
    function expectMapping(mappings: SegmentMapping[], expected: SegmentMapping): void {
      if (mappings.some(
              m => m.generated === expected.generated && m.source === expected.source &&
                  m.sourceUrl === expected.sourceUrl)) {
        return;
      }
      const matchingGenerated = mappings.filter(m => m.generated === expected.generated);
      const matchingSource = mappings.filter(m => m.source === expected.source);
      const message = [
        'Expected mappings to contain the following mapping',
        prettyPrintMapping(expected),
      ];
      if (matchingGenerated.length > 0) {
        message.push('');
        message.push('There are the following mappings that match the generated text:');
        matchingGenerated.forEach(m => message.push(prettyPrintMapping(m)));
      }
      if (matchingSource.length > 0) {
        message.push('');
        message.push('There are the following mappings that match the source text:');
        matchingSource.forEach(m => message.push(prettyPrintMapping(m)));
      }
      fail(message.join('\n'));
    }
    function prettyPrintMapping(mapping: SegmentMapping): string {
      return [
        '{',
        `  generated: ${JSON.stringify(mapping.generated)}`,
        `  source:    ${JSON.stringify(mapping.source)}`,
        `  sourceUrl: ${JSON.stringify(mapping.sourceUrl)}`,
        '}',
      ].join('\n');
    }
  });
});