///// Angular 2 Test Bed  ////
import {bind, By} from 'angular2/angular2';

import {
  beforeEach, xdescribe, describe, it, xit, // Jasmine wrappers
  beforeEachProviders,
  injectAsync,
  RootTestComponent as RTC,
  TestComponentBuilder as TCB
} from 'angular2/testing';

import {
  expectSelectedHtml,
  expectViewChildHtml,
  expectViewChildClass,
  injectTcb, tick} from '../test-helpers/test-helpers';

///// Testing this component ////
import {HeroesComponent} from './heroes.component';
import {Hero} from './hero';
import {HeroService} from './hero.service';
import {User} from './user';

let hc: HeroesComponent;
let heroData: Hero[]; // fresh heroes for each test
let mockUser: User;
let service: HeroService;

// get the promise from the refresh spy;
// casting required because of inadequate d.ts for Jasmine
let refreshPromise = () => (<any>service.refresh).calls.mostRecent().returnValue;

describe('HeroesComponent (with Angular)', () => {

  beforeEach(() => {
    heroData = [new Hero(1, 'Foo'), new Hero(2, 'Bar'), new Hero(3, 'Baz')];
    mockUser = new User();
  });

  // Set up DI bindings required by component (and its nested components?)
  // else hangs silently forever
  beforeEachProviders(() => [
    bind(HeroService).toClass(HappyHeroService),
    bind(User).toValue(mockUser)
  ]);

  // test-lib bug? first test fails unless this no-op test runs first
  it('ignore this test', () =>  expect(true).toEqual(true)); // hack

  it('can be created and has userName', injectTcb((tcb:TCB) => {
    let template = '';
    return  tcb
      .overrideTemplate(HeroesComponent, template)
      .createAsync(HeroesComponent)
      .then((rootTC: RTC) => {
        hc = rootTC.debugElement.componentInstance;
        expect(hc).toBeDefined();// proof of life
        expect(hc.userName).toEqual(mockUser.name);
      });
  }));

  it('binds view to userName', injectTcb((tcb:TCB) => {
    let template = `<h1>{{userName}}'s Heroes</h1>`;
    return tcb
      .overrideTemplate(HeroesComponent, template)
      .createAsync(HeroesComponent)
      .then((rootTC: RTC) => {
        hc = rootTC.debugElement.componentInstance;

        rootTC.detectChanges(); // trigger component property binding
        expectSelectedHtml(rootTC, 'h1').toMatch(hc.userName);
        expectViewChildHtml(rootTC).toMatch(hc.userName);
      });
  }));

  describe('#onInit', () => {
    let template = '';

    it('HeroService.refresh not called immediately',
      injectTcb([HeroService], (tcb:TCB, heroService:HeroService) => {

      return tcb
        .overrideTemplate(HeroesComponent, template)
        .createAsync(HeroesComponent)
        .then(() => {
          let spy = <jasmine.Spy><any> heroService.refresh;
          expect(spy.calls.count()).toEqual(0);
        });
    }));

    it('onInit calls HeroService.refresh',
      injectTcb([HeroService], (tcb:TCB, heroService:HeroService) => {

      return tcb
        .overrideTemplate(HeroesComponent, template)
        .createAsync(HeroesComponent)
        .then((rootTC: RTC) => {
          hc = rootTC.debugElement.componentInstance;
          let spy = <jasmine.Spy><any> heroService.refresh;
          hc.ngOnInit(); // Angular framework calls when it creates the component
          expect(spy.calls.count()).toEqual(1);
        });
    }));

    it('onInit is called after the test calls detectChanges', injectTcb((tcb:TCB) => {

      return tcb
        .overrideTemplate(HeroesComponent, template)
        .createAsync(HeroesComponent)
        .then((rootTC: RTC) => {
          hc = rootTC.debugElement.componentInstance;
          let spy = spyOn(hc, 'onInit').and.callThrough();

          expect(spy.calls.count()).toEqual(0);
          rootTC.detectChanges();
          expect(spy.calls.count()).toEqual(1);
        });
    }));
  })

  describe('#heroes', () => {
    // focus on the part of the template that displays heroe names
    let template =
      '<ul><li *ngFor="#h of heroes">{{h.name}}</li></ul>';

    it('binds view to heroes', injectTcb((tcb:TCB) => {
      return tcb
        .overrideTemplate(HeroesComponent, template)
        .createAsync(HeroesComponent)
        .then((rootTC: RTC) => {
          // trigger {{heroes}} binding
          rootTC.detectChanges();

          // hc.heroes is still empty; need a JS cycle to get the data
          return rootTC;
        })
        .then((rootTC: RTC) => {
          hc = rootTC.debugElement.componentInstance;
          // now heroes are available for binding
          expect(hc.heroes.length).toEqual(heroData.length);

          rootTC.detectChanges(); // trigger component property binding

          // confirm hero list is displayed by looking for a known hero
          expect(rootTC.debugElement.nativeElement.innerHTML).toMatch(heroData[0].name);
        });
    }));

    // ... add more tests of component behavior affecting the heroes list

  });

  describe('#onSelected', () => {

    it('no hero is selected by default', injectHC(hc => {
      expect(hc.currentHero).not.toBeDefined();
    }));

    it('sets the "currentHero"', injectHC(hc => {
      hc.onSelect(heroData[1]); // select the second hero
      expect(hc.currentHero).toEqual(heroData[1]);
    }));

    it('no hero is selected after onRefresh() called', injectHC(hc => {
      hc.onSelect(heroData[1]); // select the second hero
      hc.onRefresh();
      expect(hc.currentHero).not.toBeDefined();
    }));

    // TODO: Remove `withNgClass=true` ONCE BUG IS FIXED
    xit('the view of the "currentHero" has the "selected" class (NG2 BUG)', injectHC((hc, rootTC) => {
      hc.onSelect(heroData[1]); // select the second hero

      rootTC.detectChanges();

      // The 3rd ViewChild is 2nd hero; the 1st is for the template
      expectViewChildClass(rootTC, 2).toMatch('selected');
    }, true /* true == include ngClass */));

    it('the view of a non-selected hero does NOT have the "selected" class', injectHC((hc, rootTC)  => {
      hc.onSelect(heroData[1]); // select the second hero
      rootTC.detectChanges();
      // The 4th ViewChild is 3rd hero; the 1st is for the template
      expectViewChildClass(rootTC, 4).not.toMatch('selected');
    }));

  });

  // Most #onDelete tests not re-implemented because
  // writing those tests w/in Angular adds little value and
  // is far more painful than writing them to run outside Angular
  // Only bother with the one test that checks the DOM
  describe('#onDeleted', () => {
    let template =
      '<ul><li *ngFor="#h of heroes">{{h.name}}</li></ul>';

    it('the list view does not contain the "deleted" currentHero', injectTcb((tcb:TCB) => {
      return tcb
        .overrideTemplate(HeroesComponent, template)
        .createAsync(HeroesComponent)
        .then((rootTC: RTC) => {
          hc = rootTC.debugElement.componentInstance;
          // trigger {{heroes}} binding
          rootTC.detectChanges();
          return rootTC; // wait for heroes to arrive
        })
        .then((rootTC: RTC) => {
          hc.currentHero = heroData[1];
          hc.onDelete()
          rootTC.detectChanges(); // trigger component property binding

          // confirm hero list is not displayed by looking for removed hero
          expect(rootTC.debugElement.nativeElement.innerHTML).not.toMatch(heroData[1].name);
        });
    }));
  });
});

////// Helpers //////

class HappyHeroService {

  constructor() {
    spyOn(this, 'refresh').and.callThrough();
  }

  heroes: Hero[];

  refresh() {
    this.heroes = [];
    // updates cached heroes after one JavaScript cycle
    return new Promise((resolve, reject) => {
      this.heroes.push(...heroData);
      resolve(this.heroes);
    });
  }
}


// The same setup for every test in the #onSelected suite
// TODO: Remove `withNgClass` and always include in template ONCE BUG IS FIXED
function injectHC(testFn: (hc: HeroesComponent, rootTC?: RTC) => void, withNgClass:boolean = false) {

  // This is the bad boy:   [ngClass]="getSelectedClass(hero)"
  let ngClass = withNgClass ? '[ngClass]="getSelectedClass(hero)"' : '';

  // focus on the part of the template that displays heroes
  let template =
    `<ul><li *ngFor="#hero of heroes"
      ${ngClass}
      (click)="onSelect(hero)">
      ({{hero.id}}) {{hero.name}}
      </li></ul>`;

  return injectTcb((tcb:TCB) => {
    let hc: HeroesComponent;

    return tcb
    .overrideTemplate(HeroesComponent, template)
    .createAsync(HeroesComponent)
    .then((rootTC:RTC) => {
      hc = rootTC.debugElement.componentInstance;
      rootTC.detectChanges();// trigger {{heroes}} binding
      return rootTC;
    })
    .then((rootTC:RTC) => { // wait a tick until heroes are fetched
console.error("WAS THIS FIXED??");
    // CRASHING HERE IF TEMPLATE HAS '[ngClass]="getSelectedClass(hero)"'
    // WITH EXCEPTION:
    //   "Expression 'getSelectedClass(hero) in null' has changed after it was checked."

      rootTC.detectChanges(); // show the list
      testFn(hc, rootTC);
    });
  })
}
