import {
  ddescribe,
  describe,
  xdescribe,
  it,
  iit,
  xit,
  expect,
  beforeEach,
  afterEach,
  AsyncTestCompleter,
  inject,
  beforeEachProviders
} from 'angular2/testing_internal';

import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
import {Type, isPresent, isBlank, stringify, isString} from 'angular2/src/facade/lang';
import {MapWrapper, SetWrapper, ListWrapper} from 'angular2/src/facade/collection';
import {RuntimeMetadataResolver} from 'angular2/src/compiler/runtime_metadata';
import {
  TemplateCompiler,
  NormalizedComponentWithViewDirectives
} from 'angular2/src/compiler/template_compiler';
import {CompileDirectiveMetadata} from 'angular2/src/compiler/directive_metadata';
import {evalModule} from './eval_module';
import {SourceModule, moduleRef} from 'angular2/src/compiler/source_module';
import {XHR} from 'angular2/src/compiler/xhr';
import {MockXHR} from 'angular2/src/compiler/xhr_mock';
import {ViewEncapsulation} from 'angular2/src/core/metadata/view';

import {Locals} from 'angular2/src/core/change_detection/change_detection';

import {
  CommandVisitor,
  TextCmd,
  NgContentCmd,
  BeginElementCmd,
  BeginComponentCmd,
  EmbeddedTemplateCmd,
  TemplateCmd,
  visitAllCommands,
  CompiledComponentTemplate
} from 'angular2/src/core/linker/template_commands';

import {Component, View, Directive, provide} from 'angular2/core';

import {TEST_PROVIDERS} from './test_bindings';
import {TestDispatcher, TestPipes} from './change_detector_mocks';
import {codeGenValueFn, codeGenExportVariable, MODULE_SUFFIX} from 'angular2/src/compiler/util';

// Attention: This path has to point to this test file!
const THIS_MODULE_ID = 'angular2/test/compiler/template_compiler_spec';
var THIS_MODULE_REF = moduleRef(`package:${THIS_MODULE_ID}${MODULE_SUFFIX}`);

export function main() {
  describe('TemplateCompiler', () => {
    var compiler: TemplateCompiler;
    var runtimeMetadataResolver: RuntimeMetadataResolver;

    beforeEachProviders(() => TEST_PROVIDERS);
    beforeEach(inject([TemplateCompiler, RuntimeMetadataResolver],
                      (_compiler, _runtimeMetadataResolver) => {
                        compiler = _compiler;
                        runtimeMetadataResolver = _runtimeMetadataResolver;
                      }));

    describe('compile templates', () => {

      function runTests(compile) {
        it('should throw for non components', inject([AsyncTestCompleter], (async) => {
             PromiseWrapper.catchError(PromiseWrapper.wrap(() => compile([NonComponent])), (error) => {
               expect(error.message)
                   .toEqual(
                       `Could not compile '${stringify(NonComponent)}' because it is not a component.`);
               async.done();
             });
           }));

        it('should compile host components', inject([AsyncTestCompleter], (async) => {
             compile([CompWithBindingsAndStyles])
                 .then((humanizedTemplate) => {
                   expect(humanizedTemplate['styles']).toEqual([]);
                   expect(humanizedTemplate['commands'][0]).toEqual('<comp-a>');
                   expect(humanizedTemplate['cd']).toEqual(['elementProperty(title)=someDirValue']);

                   async.done();
                 });
           }));

        it('should compile nested components', inject([AsyncTestCompleter], (async) => {
             compile([CompWithBindingsAndStyles])
                 .then((humanizedTemplate) => {
                   var nestedTemplate = humanizedTemplate['commands'][1];
                   expect(nestedTemplate['styles']).toEqual(['div {color: red}']);
                   expect(nestedTemplate['commands'][0]).toEqual('<a>');
                   expect(nestedTemplate['cd']).toEqual(['elementProperty(href)=someCtxValue']);

                   async.done();
                 });
           }));

        it('should compile recursive components', inject([AsyncTestCompleter], (async) => {
             compile([TreeComp])
                 .then((humanizedTemplate) => {
                   expect(humanizedTemplate['commands'][0]).toEqual('<tree>');
                   expect(humanizedTemplate['commands'][1]['commands'][0]).toEqual('<tree>');
                   expect(humanizedTemplate['commands'][1]['commands'][1]['commands'][0])
                       .toEqual('<tree>');

                   async.done();
                 });
           }));

        it('should pass the right change detector to embedded templates',
           inject([AsyncTestCompleter], (async) => {
             compile([CompWithEmbeddedTemplate])
                 .then((humanizedTemplate) => {
                   expect(humanizedTemplate['commands'][1]['commands'][0]).toEqual('<template>');
                   expect(humanizedTemplate['commands'][1]['commands'][1]['cd'])
                       .toEqual(['elementProperty(href)=someCtxValue']);

                   async.done();
                 });
           }));

        it('should dedup directives', inject([AsyncTestCompleter], (async) => {
             compile([CompWithDupDirectives, TreeComp])
                 .then((humanizedTemplate) => {
                   expect(humanizedTemplate['commands'][1]['commands'][0]).toEqual("<tree>");
                   async.done();

                 });
           }));
      }

      describe('compileHostComponentRuntime', () => {
        function compile(components: Type[]): Promise<any[]> {
          return compiler.compileHostComponentRuntime(components[0])
              .then((compiledHostTemplate) => humanizeTemplate(compiledHostTemplate.template));
        }

        runTests(compile);

        it('should cache components for parallel requests',
           inject([AsyncTestCompleter, XHR], (async, xhr: MockXHR) => {
             xhr.expect('package:angular2/test/compiler/compUrl.html', 'a');
             PromiseWrapper.all([compile([CompWithTemplateUrl]), compile([CompWithTemplateUrl])])
                 .then((humanizedTemplates) => {
                   expect(humanizedTemplates[0]['commands'][1]['commands']).toEqual(['#text(a)']);
                   expect(humanizedTemplates[1]['commands'][1]['commands']).toEqual(['#text(a)']);

                   async.done();
                 });
             xhr.flush();
           }));

        it('should cache components for sequential requests',
           inject([AsyncTestCompleter, XHR], (async, xhr: MockXHR) => {
             xhr.expect('package:angular2/test/compiler/compUrl.html', 'a');
             compile([CompWithTemplateUrl])
                 .then((humanizedTemplate0) => {
                   return compile([CompWithTemplateUrl])
                       .then((humanizedTemplate1) => {
                         expect(humanizedTemplate0['commands'][1]['commands'])
                             .toEqual(['#text(a)']);
                         expect(humanizedTemplate1['commands'][1]['commands'])
                             .toEqual(['#text(a)']);
                         async.done();
                       });
                 });
             xhr.flush();
           }));

        it('should allow to clear the cache',
           inject([AsyncTestCompleter, XHR], (async, xhr: MockXHR) => {
             xhr.expect('package:angular2/test/compiler/compUrl.html', 'a');
             compile([CompWithTemplateUrl])
                 .then((humanizedTemplate) => {
                   compiler.clearCache();
                   xhr.expect('package:angular2/test/compiler/compUrl.html', 'b');
                   var result = compile([CompWithTemplateUrl]);
                   xhr.flush();
                   return result;
                 })
                 .then((humanizedTemplate) => {
                   expect(humanizedTemplate['commands'][1]['commands']).toEqual(['#text(b)']);
                   async.done();
                 });
             xhr.flush();
           }));
      });

      describe('compileTemplatesCodeGen', () => {
        function normalizeComponent(
            component: Type): Promise<NormalizedComponentWithViewDirectives> {
          var compAndViewDirMetas = [runtimeMetadataResolver.getMetadata(component)].concat(
              runtimeMetadataResolver.getViewDirectivesMetadata(component));
          return PromiseWrapper.all(compAndViewDirMetas.map(
                                        meta => compiler.normalizeDirectiveMetadata(meta)))
              .then((normalizedCompAndViewDirMetas: CompileDirectiveMetadata[]) =>
                        new NormalizedComponentWithViewDirectives(
                            normalizedCompAndViewDirMetas[0],
                            normalizedCompAndViewDirMetas.slice(1)));
        }

        function compile(components: Type[]): Promise<any[]> {
          return PromiseWrapper.all(components.map(normalizeComponent))
              .then((normalizedCompWithViewDirMetas: NormalizedComponentWithViewDirectives[]) => {
                var sourceModule = compiler.compileTemplatesCodeGen(normalizedCompWithViewDirMetas);
                var sourceWithImports =
                    testableTemplateModule(sourceModule,
                                           normalizedCompWithViewDirMetas[0].component)
                        .getSourceWithImports();
                return evalModule(sourceWithImports.source, sourceWithImports.imports, null);
              });
        }

        runTests(compile);
      });

    });

    describe('normalizeDirectiveMetadata', () => {
      it('should return the given DirectiveMetadata for non components',
         inject([AsyncTestCompleter], (async) => {
           var meta = runtimeMetadataResolver.getMetadata(NonComponent);
           compiler.normalizeDirectiveMetadata(meta).then(normMeta => {
             expect(normMeta).toBe(meta);
             async.done();
           });
         }));

      it('should normalize the template',
         inject([AsyncTestCompleter, XHR], (async, xhr: MockXHR) => {
           xhr.expect('package:angular2/test/compiler/compUrl.html', 'loadedTemplate');
           compiler.normalizeDirectiveMetadata(
                       runtimeMetadataResolver.getMetadata(CompWithTemplateUrl))
               .then((meta: CompileDirectiveMetadata) => {
                 expect(meta.template.template).toEqual('loadedTemplate');
                 async.done();
               });
           xhr.flush();
         }));

      it('should copy all the other fields', inject([AsyncTestCompleter], (async) => {
           var meta = runtimeMetadataResolver.getMetadata(CompWithBindingsAndStyles);
           compiler.normalizeDirectiveMetadata(meta).then((normMeta: CompileDirectiveMetadata) => {
             expect(normMeta.type).toEqual(meta.type);
             expect(normMeta.isComponent).toEqual(meta.isComponent);
             expect(normMeta.dynamicLoadable).toEqual(meta.dynamicLoadable);
             expect(normMeta.selector).toEqual(meta.selector);
             expect(normMeta.exportAs).toEqual(meta.exportAs);
             expect(normMeta.changeDetection).toEqual(meta.changeDetection);
             expect(normMeta.inputs).toEqual(meta.inputs);
             expect(normMeta.outputs).toEqual(meta.outputs);
             expect(normMeta.hostListeners).toEqual(meta.hostListeners);
             expect(normMeta.hostProperties).toEqual(meta.hostProperties);
             expect(normMeta.hostAttributes).toEqual(meta.hostAttributes);
             expect(normMeta.lifecycleHooks).toEqual(meta.lifecycleHooks);
             async.done();
           });
         }));
    });

    describe('compileStylesheetCodeGen', () => {
      it('should compile stylesheets into code', inject([AsyncTestCompleter], (async) => {
           var cssText = 'div {color: red}';
           var sourceModule =
               compiler.compileStylesheetCodeGen('package:someModuleUrl', cssText)[0];
           var sourceWithImports = testableStylesModule(sourceModule).getSourceWithImports();
           evalModule(sourceWithImports.source, sourceWithImports.imports, null)
               .then(loadedCssText => {
                 expect(loadedCssText).toEqual([cssText]);
                 async.done();
               });

         }));
    });
  });
}

@Component({
  selector: 'comp-a',
  host: {'[title]': 'someProp'},
  moduleId: THIS_MODULE_ID,
  exportAs: 'someExportAs'
})
@View({
  template: '<a [href]="someProp"></a>',
  styles: ['div {color: red}'],
  encapsulation: ViewEncapsulation.None
})
class CompWithBindingsAndStyles {
}

@Component({selector: 'tree', moduleId: THIS_MODULE_ID})
@View({template: '<tree></tree>', directives: [TreeComp], encapsulation: ViewEncapsulation.None})
class TreeComp {
}

@Component({selector: 'comp-wit-dup-tpl', moduleId: THIS_MODULE_ID})
@View({
  template: '<tree></tree>',
  directives: [TreeComp, TreeComp],
  encapsulation: ViewEncapsulation.None
})
class CompWithDupDirectives {
}

@Component({selector: 'comp-url', moduleId: THIS_MODULE_ID})
@View({templateUrl: 'compUrl.html', encapsulation: ViewEncapsulation.None})
class CompWithTemplateUrl {
}

@Component({selector: 'comp-tpl', moduleId: THIS_MODULE_ID})
@View({
  template: '<template><a [href]="someProp"></a></template>',
  encapsulation: ViewEncapsulation.None
})
class CompWithEmbeddedTemplate {
}


@Directive({selector: 'plain', moduleId: THIS_MODULE_ID})
@View({template: ''})
class NonComponent {
}

function testableTemplateModule(sourceModule: SourceModule,
                                normComp: CompileDirectiveMetadata): SourceModule {
  var resultExpression =
      `${THIS_MODULE_REF}humanizeTemplate(Host${normComp.type.name}Template.template)`;
  var testableSource = `${sourceModule.sourceWithModuleRefs}
  ${codeGenValueFn(['_'], resultExpression, '_run')};
  ${codeGenExportVariable('run')}_run;`;
  return new SourceModule(sourceModule.moduleUrl, testableSource);
}

function testableStylesModule(sourceModule: SourceModule): SourceModule {
  var testableSource = `${sourceModule.sourceWithModuleRefs}
  ${codeGenValueFn(['_'], 'STYLES', '_run')};
  ${codeGenExportVariable('run')}_run;`;
  return new SourceModule(sourceModule.moduleUrl, testableSource);
}

// Attention: read by eval!
export function humanizeTemplate(
    template: CompiledComponentTemplate,
    humanizedTemplates: Map<string, {[key: string]: any}> = null): {[key: string]: any} {
  if (isBlank(humanizedTemplates)) {
    humanizedTemplates = new Map<string, {[key: string]: any}>();
  }
  var result = humanizedTemplates.get(template.id);
  if (isPresent(result)) {
    return result;
  }
  var commands = [];
  result = {
    'styles': template.styles,
    'commands': commands,
    'cd': testChangeDetector(template.changeDetectorFactory)
  };
  humanizedTemplates.set(template.id, result);
  visitAllCommands(new CommandHumanizer(commands, humanizedTemplates), template.commands);
  return result;
}

class TestContext implements CompWithBindingsAndStyles, TreeComp, CompWithTemplateUrl,
    CompWithEmbeddedTemplate, CompWithDupDirectives {
  someProp: string;
}


function testChangeDetector(changeDetectorFactory: Function): string[] {
  var ctx = new TestContext();
  ctx.someProp = 'someCtxValue';
  var dir1 = new TestContext();
  dir1.someProp = 'someDirValue';

  var dispatcher = new TestDispatcher([dir1], []);
  var cd = changeDetectorFactory(dispatcher);
  var locals = new Locals(null, MapWrapper.createFromStringMap({'someVar': null}));
  cd.hydrate(ctx, locals, dispatcher, new TestPipes());
  cd.detectChanges();
  return dispatcher.log;
}


class CommandHumanizer implements CommandVisitor {
  constructor(private result: any[],
              private humanizedTemplates: Map<string, {[key: string]: any}>) {}
  visitText(cmd: TextCmd, context: any): any {
    this.result.push(`#text(${cmd.value})`);
    return null;
  }
  visitNgContent(cmd: NgContentCmd, context: any): any { return null; }
  visitBeginElement(cmd: BeginElementCmd, context: any): any {
    this.result.push(`<${cmd.name}>`);
    return null;
  }
  visitEndElement(context: any): any {
    this.result.push('</>');
    return null;
  }
  visitBeginComponent(cmd: BeginComponentCmd, context: any): any {
    this.result.push(`<${cmd.name}>`);
    this.result.push(humanizeTemplate(cmd.templateGetter(), this.humanizedTemplates));
    return null;
  }
  visitEndComponent(context: any): any { return this.visitEndElement(context); }
  visitEmbeddedTemplate(cmd: EmbeddedTemplateCmd, context: any): any {
    this.result.push(`<template>`);
    this.result.push({'cd': testChangeDetector(cmd.changeDetectorFactory)});
    this.result.push(`</template>`);
    return null;
  }
}