/**
 * @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 {CommonModule} from '@angular/common';
import {ChangeDetectorRef, Component, Directive, TemplateRef, ViewChild, ViewContainerRef} from '@angular/core';
import {Input} from '@angular/core/src/metadata';
import {TestBed} from '@angular/core/testing';
import {By} from '@angular/platform-browser';
import {expect} from '@angular/platform-browser/testing/src/matchers';
describe('projection', () => {
  function getElementHtml(element: HTMLElement) {
    return element.innerHTML.replace(//g, '')
        .replace(/\sng-reflect-\S*="[^"]*"/g, '');
  }
  it('should project content', () => {
    @Component({selector: 'child', template: `
`})
    class Child {
    }
    @Component({selector: 'parent', template: 'content '})
    class Parent {
    }
    TestBed.configureTestingModule({declarations: [Parent, Child]});
    const fixture = TestBed.createComponent(Parent);
    fixture.detectChanges();
    expect(fixture.nativeElement.innerHTML).toBe(`content
 is at a template root', () => {
    @Component({
      selector: 'child',
      template: 'content ',
    })
    class Parent {
    }
    TestBed.configureTestingModule({declarations: [Parent, Child]});
    const fixture = TestBed.createComponent(Parent);
    fixture.detectChanges();
    expect(fixture.nativeElement.innerHTML).toBe(`content `);
  });
  it('should project content with siblings', () => {
    @Component({selector: 'child', template: 'beforecontent
after `})
    class Parent {
    }
    TestBed.configureTestingModule({declarations: [Parent, Child]});
    const fixture = TestBed.createComponent(Parent);
    fixture.detectChanges();
    expect(fixture.nativeElement.innerHTML).toBe(`beforecontent
after `);
  });
  it('should be able to re-project content', () => {
    @Component({selector: 'grand-child', template: `
`})
    class GrandChild {
    }
    @Component(
        {selector: 'child', template: `Hello World!Hello World!
`,
    })
    class Child {
    }
    @Component({
      selector: 'projected-comp',
      template: 'content',
    })
    class ProjectedComp {
    }
    @Component({selector: 'parent', template: `
`})
    class Child {
    }
    @Component({selector: 'projected-comp', template: `
`})
    class ProjectedComp {
    }
    @Component({
      selector: 'parent',
      template: `
        
          Some content
Other content `,
    })
    class Parent {
    }
    TestBed.configureTestingModule({declarations: [Parent, Child, ProjectedComp]});
    const fixture = TestBed.createComponent(Parent);
    fixture.detectChanges();
    expect(fixture.nativeElement.innerHTML)
        .toBe(
            `
Some content
Other content
`})
    class Child {
    }
    @Component({selector: 'projected-comp', template: `Before
          A
123
B
456
 `,
    })
    class Parent {
    }
    TestBed.configureTestingModule({declarations: [Parent, Child, ProjectedComp]});
    const fixture = TestBed.createComponent(Parent);
    fixture.detectChanges();
    expect(fixture.nativeElement.innerHTML)
        .toBe(
            '' +
            '
BeforeA
123
After ' +
            '
BeforeB
456
After ' +
            '
`})
    class Child {
    }
    @Component({selector: 'projected-comp', template: `Before
          A
123
B
456
 `,
    })
    class Parent {
    }
    @Component({
      selector: 'app',
      template: `
        **ABC** 
        **DEF** 
     `,
    })
    class App {
    }
    TestBed.configureTestingModule({declarations: [App, Parent, Child, ProjectedComp]});
    const fixture = TestBed.createComponent(App);
    fixture.detectChanges();
    expect(fixture.nativeElement.innerHTML)
        .toBe(
            '' +
            '
BeforeA
**ABC**123
After ' +
            '
BeforeB
456
After ' +
            '
' +
            '
BeforeA
**DEF**123
After ' +
            '
BeforeB
456
After ' +
            '
A
Some textA
Some text-After`);
    childInstance.showing = false;
    fixture.detectChanges();
    expect(getElementHtml(childElement)).toBe(`Before--After`);
    childInstance.showing = true;
    fixture.detectChanges();
    expect(getElementHtml(childElement)).toBe(`Before-A
Some text-After`);
  });
  it('should project into dynamic views with specific selectors', () => {
    @Component({
      selector: 'child',
      template: `
        
           
        -After`
    })
    class Child {
      showing = false;
    }
    @Component({
      selector: 'parent',
      template: `
        
          A
          B 
         
      `
    })
    class Parent {
    }
    TestBed.configureTestingModule({declarations: [Parent, Child], imports: [CommonModule]});
    const fixture = TestBed.createComponent(Parent);
    const childDebugEl = fixture.debugElement.query(By.directive(Child));
    const childInstance = childDebugEl.injector.get(Child);
    childInstance.showing = true;
    fixture.detectChanges();
    expect(getElementHtml(fixture.nativeElement))
        .toBe('B  Before- A
 -AfterB  Before-  -AfterB  Before- A
 -After is in a template that has different declaration/insertion points',
     () => {
       @Component(
           {selector: 'comp', template: `;
       }
       @Directive({selector: '[trigger]'})
       class Trigger {
         @Input() trigger!: Comp;
         constructor(public vcr: ViewContainerRef) {}
         open() {
           this.vcr.createEmbeddedView(this.trigger.template);
         }
       }
       @Component({
         selector: 'parent',
         template: `
        Some content 
      `
       })
       class Parent {
       }
       TestBed.configureTestingModule({declarations: [Parent, Trigger, Comp]});
       const fixture = TestBed.createComponent(Parent);
       const trigger = fixture.debugElement.query(By.directive(Trigger)).injector.get(Trigger);
       fixture.detectChanges();
       expect(getElementHtml(fixture.nativeElement)).toBe(`
          content `})
    class Parent {
    }
    TestBed.configureTestingModule({declarations: [Parent, Child], imports: [CommonModule]});
    const fixture = TestBed.createComponent(Parent);
    fixture.detectChanges();
    expect(getElementHtml(fixture.nativeElement))
        .toBe('
content ({{i}}):
`
    })
    class Child {
    }
    @Component({selector: 'parent', template: `content `})
    class Parent {
    }
    TestBed.configureTestingModule({declarations: [Parent, Child], imports: [CommonModule]});
    const fixture = TestBed.createComponent(Parent);
    fixture.detectChanges();
    expect(getElementHtml(fixture.nativeElement))
        .toBe('(0):
(1):content
Child content
`})
    class NestedComp {
    }
    @Component({
      selector: 'root-comp',
      template: `
          
             
         
      `
    })
    class MyApp {
      items = [1, 2];
    }
    TestBed.configureTestingModule(
        {declarations: [MyApp, RootComp, NestedComp], imports: [CommonModule]});
    const fixture = TestBed.createComponent(MyApp);
    fixture.detectChanges();
    // expecting # of divs to be (items.length - 1), since last element is filtered out by *ngIf,
    // this applies to all other assertions below
    expect(fixture.nativeElement.querySelectorAll('div').length).toBe(1);
    fixture.componentInstance.items = [3, 4, 5];
    fixture.detectChanges();
    expect(fixture.nativeElement.querySelectorAll('div').length).toBe(2);
    fixture.componentInstance.items = [6, 7, 8, 9];
    fixture.detectChanges();
    expect(fixture.nativeElement.querySelectorAll('div').length).toBe(3);
  });
  it('should handle projection into element containers at the view root', () => {
    @Component({
      selector: 'root-comp',
      template: `
        
          
             
         `,
    })
    class RootComp {
      @Input() show: boolean = true;
    }
    @Component({
      selector: 'my-app',
      template: `
      
        content 
       
     
      `
    })
    class Parent {
    }
    TestBed.configureTestingModule({declarations: [Parent, Child]});
    const fixture = TestBed.createComponent(Parent);
    fixture.detectChanges();
    expect(getElementHtml(fixture.nativeElement)).toBe('content ');
  });
  it('should re-project ng-container at the content root', () => {
    @Component({selector: 'grand-child', template: `
       `
    })
    class Child {
    }
    @Component({
      selector: 'parent',
      template: `
      
        content 
       
     
      `
    })
    class Parent {
    }
    TestBed.configureTestingModule({declarations: [Parent, Child, GrandChild]});
    const fixture = TestBed.createComponent(Parent);
    fixture.detectChanges();
    expect(getElementHtml(fixture.nativeElement))
        .toBe('content 
          
`,
      })
      class Child {
      }
      @Component({
        selector: 'parent',
        template: `1 2 1 
2 
          
`,
      })
      class Child {
      }
      @Component({
        selector: 'parent',
        template: `1 2 1 
2 
          
`
      })
      class Child {
      }
      @Component({
        selector: 'parent',
        template:
            `1 2 1 
2 
          
`
      })
      class Child {
      }
      @Component({
        selector: 'parent',
        template: `1 2 1 2 
          
`
      })
      class Child {
      }
      @Component({
        selector: 'parent',
        template:
            `1 remaining more remaining1 
remaining more remaining
          
`
      })
      class Child {
      }
      @Component({
        selector: 'parent',
        template: `1 2 remaining1 remaining
2 
            in child template 
           `
      })
      class Child {
      }
      @Component({selector: 'parent', template: `parent content in child template parent content 
         Title 
          `
      })
      class CardWithTitle {
      }
      @Component({selector: 'parent', template: `content `})
      class Parent {
      }
      TestBed.configureTestingModule({declarations: [Card, CardWithTitle, Parent]});
      const fixture = TestBed.createComponent(Parent);
      fixture.detectChanges();
      expect(getElementHtml(fixture.nativeElement))
          .toEqual(
              'Title should not project
should project
should project
Has title Has title content
content
          
            {{ item }}| 
           
         
      `
    })
    class MyApp {
      items: number[] = [1, 2, 3];
    }
    TestBed.configureTestingModule({declarations: [ChildComp, RootComp, MyApp]});
    const fixture = TestBed.createComponent(MyApp);
    fixture.detectChanges();
    // expecting # of elements to be (items.length - 1), since last element is filtered out by
    // *ngIf, this applies to all other assertions below
    expect(fixture.nativeElement).toHaveText('1|2|');
    fixture.componentInstance.items = [4, 5];
    fixture.detectChanges();
    expect(fixture.nativeElement).toHaveText('4|');
    fixture.componentInstance.items = [6, 7, 8, 9];
    fixture.detectChanges();
    expect(fixture.nativeElement).toHaveText('6|7|8|');
  });
  it('should project content if the change detector has been detached', () => {
    @Component({selector: 'my-comp', template: '
          hello
         
      `
    })
    class MyApp {
    }
    TestBed.configureTestingModule({declarations: [MyComp, MyApp]});
    const fixture = TestBed.createComponent(MyApp);
    fixture.detectChanges();
    expect(fixture.nativeElement).toHaveText('hello');
  });
  it('should support ngProjectAs with a various number of other bindings and attributes', () => {
    @Directive({selector: '[color],[margin]'})
    class ElDecorator {
      @Input() color?: string;
      @Input() margin?: number;
    }
    @Component({
      selector: 'card',
      template: `
        
         Title 
         Subtitle 
         content
         footer
         
      `
    })
    class CardWithTitle {
    }
    TestBed.configureTestingModule({declarations: [Card, CardWithTitle, ElDecorator]});
    const fixture = TestBed.createComponent(CardWithTitle);
    fixture.detectChanges();
    // Compare the text output, because Ivy and ViewEngine produce slightly different HTML.
    expect(fixture.nativeElement.textContent)
        .toContain('Title --- Subtitle --- content --- footer');
  });
  it('should support ngProjectAs on elements (including )', () => {
    @Component({
      selector: 'card',
      template: `
        
         Title 
          
      `
    })
    class CardWithTitle {
    }
    @Component({
      selector: 'app',
      template: `
        content 
      `
    })
    class App {
    }
    TestBed.configureTestingModule({declarations: [Card, CardWithTitle, App]});
    const fixture = TestBed.createComponent(App);
    fixture.detectChanges();
    // Compare the text output, because Ivy and ViewEngine produce slightly different HTML.
    expect(fixture.nativeElement.textContent).toContain('Title --- content');
  });
  it('should not match multiple selectors in ngProjectAs', () => {
    @Component({
      selector: 'card',
      template: `
        
         Title 
         
      `
    })
    class App {
    }
    TestBed.configureTestingModule({declarations: [Card, App]});
    const fixture = TestBed.createComponent(App);
    fixture.detectChanges();
    // Compare the text output, because Ivy and ViewEngine produce slightly different HTML.
    expect(fixture.nativeElement.textContent).not.toContain('Title content');
  });
  it('should preserve ngProjectAs and other attributes on projected element', () => {
    @Component({
      selector: 'projector',
      template: `
          
         
      `
    })
    class Root {
    }
    TestBed.configureTestingModule({
      declarations: [Root, Projector],
    });
    const fixture = TestBed.createComponent(Root);
    fixture.detectChanges();
    const projectedElement = fixture.debugElement.query(By.css('div'));
    const {ngProjectAs, title} = projectedElement.attributes;
    expect(ngProjectAs).toBe('projectMe');
    expect(title).toBe('some title');
  });
  describe('on inline templates (e.g.  *ngIf)', () => {
    it('should work when matching the element name', () => {
      let divDirectives = 0;
      @Component({selector: 'selector-proj', template: 'Hello world!
(SelectorMainComp);
      fixture.detectChanges();
      expect(fixture.nativeElement).toHaveText('Hello world!');
      expect(divDirectives).toEqual(1);
    });
    it('should work when matching attributes', () => {
      let xDirectives = 0;
      @Component({selector: 'selector-proj', template: 'Hello world!
(SelectorMainComp);
      fixture.detectChanges();
      expect(fixture.nativeElement).toHaveText('Hello world!');
      expect(xDirectives).toEqual(1);
    });
    it('should work when matching classes', () => {
      let xDirectives = 0;
      @Component({selector: 'selector-proj', template: 'Hello world!
(SelectorMainComp);
      fixture.detectChanges();
      expect(fixture.nativeElement).toHaveText('Hello world!');
      expect(xDirectives).toEqual(1);
    });
    it('should ignore synthesized attributes (e.g. ngTrackBy)', () => {
      @Component(
          {selector: 'selector-proj', template: '{{item.name}}
{{item.name}}
(SelectorMainComp);
      fixture.detectChanges();
      expect(fixture.nativeElement).toHaveText('inline()ng-template(onetwothree)');
    });
    it('should project template content with `ngProjectAs` defined', () => {
      @Component({
        selector: 'projector-app',
        template: `
          Projected
          
            as element
            as attribute
            as class
           
        `,
      })
      class RootComp {
        show = true;
      }
      TestBed.configureTestingModule({
        declarations: [ProjectorApp, RootComp],
      });
      const fixture = TestBed.createComponent(RootComp);
      fixture.detectChanges();
      let content = fixture.nativeElement.textContent;
      expect(content).toContain('as element');
      expect(content).toContain('as attribute');
      expect(content).toContain('as class');
      fixture.componentInstance.show = false;
      fixture.detectChanges();
      content = fixture.nativeElement.textContent;
      expect(content).not.toContain('as element');
      expect(content).not.toContain('as attribute');
      expect(content).not.toContain('as class');
    });
    describe('on containers', () => {
      it('should work when matching attributes', () => {
        let xDirectives = 0;
        @Component({selector: 'selector-proj', template: 'Hello world! (SelectorMainComp);
        fixture.detectChanges();
        expect(fixture.nativeElement).toHaveText('Hello world!');
        expect(xDirectives).toEqual(1);
      });
      it('should work when matching classes', () => {
        let xDirectives = 0;
        @Component({selector: 'selector-proj', template: 'Hello world! (SelectorMainComp);
        fixture.detectChanges();
        expect(fixture.nativeElement).toHaveText('Hello world!');
        expect(xDirectives).toEqual(1);
      });
    });
  });
});